Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
1import re
2import os
3import html
4import sys
5import math
6import time
7import json
8import io
9import urllib
10import urllib.parse
11
12import gevent
13
14import util
15from Config import config
16from Plugin import PluginManager
17from Debug import Debug
18from Translate import Translate
19from util import helper
20from util.Flag import flag
21from .ZipStream import ZipStream
22
23plugin_dir = os.path.dirname(__file__)
24media_dir = plugin_dir + "/media"
25
26loc_cache = {}
27if "_" not in locals():
28 _ = Translate(plugin_dir + "/languages/")
29
30
31@PluginManager.registerTo("UiRequest")
32class UiRequestPlugin(object):
33 # Inject our resources to end of original file streams
34 def actionUiMedia(self, path):
35 if path == "/uimedia/all.js" or path == "/uimedia/all.css":
36 # First yield the original file and header
37 body_generator = super(UiRequestPlugin, self).actionUiMedia(path)
38 for part in body_generator:
39 yield part
40
41 # Append our media file to the end
42 ext = re.match(".*(js|css)$", path).group(1)
43 plugin_media_file = "%s/all.%s" % (media_dir, ext)
44 if config.debug:
45 # If debugging merge *.css to all.css and *.js to all.js
46 from Debug import DebugMedia
47 DebugMedia.merge(plugin_media_file)
48 if ext == "js":
49 yield _.translateData(open(plugin_media_file).read()).encode("utf8")
50 else:
51 for part in self.actionFile(plugin_media_file, send_header=False):
52 yield part
53 elif path.startswith("/uimedia/globe/"): # Serve WebGL globe files
54 file_name = re.match(".*/(.*)", path).group(1)
55 plugin_media_file = "%s_globe/%s" % (media_dir, file_name)
56 if config.debug and path.endswith("all.js"):
57 # If debugging merge *.css to all.css and *.js to all.js
58 from Debug import DebugMedia
59 DebugMedia.merge(plugin_media_file)
60 for part in self.actionFile(plugin_media_file):
61 yield part
62 else:
63 for part in super(UiRequestPlugin, self).actionUiMedia(path):
64 yield part
65
66 def actionZip(self):
67 address = self.get["address"]
68 site = self.server.site_manager.get(address)
69 if not site:
70 return self.error404("Site not found")
71
72 title = site.content_manager.contents.get("content.json", {}).get("title", "")
73 filename = "%s-backup-%s.zip" % (title, time.strftime("%Y-%m-%d_%H_%M"))
74 filename_quoted = urllib.parse.quote(filename)
75 self.sendHeader(content_type="application/zip", extra_headers={'Content-Disposition': 'attachment; filename="%s"' % filename_quoted})
76
77 return self.streamZip(site.storage.getPath("."))
78
79 def streamZip(self, dir_path):
80 zs = ZipStream(dir_path)
81 while 1:
82 data = zs.read()
83 if not data:
84 break
85 yield data
86
87
88@PluginManager.registerTo("UiWebsocket")
89class UiWebsocketPlugin(object):
90 def sidebarRenderPeerStats(self, body, site):
91 connected = len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected])
92 connectable = len([peer_id for peer_id in list(site.peers.keys()) if not peer_id.endswith(":0")])
93 onion = len([peer_id for peer_id in list(site.peers.keys()) if ".onion" in peer_id])
94 local = len([peer for peer in list(site.peers.values()) if helper.isPrivateIp(peer.ip)])
95 peers_total = len(site.peers)
96
97 # Add myself
98 if site.isServing():
99 peers_total += 1
100 if any(site.connection_server.port_opened.values()):
101 connectable += 1
102 if site.connection_server.tor_manager.start_onions:
103 onion += 1
104
105 if peers_total:
106 percent_connected = float(connected) / peers_total
107 percent_connectable = float(connectable) / peers_total
108 percent_onion = float(onion) / peers_total
109 else:
110 percent_connectable = percent_connected = percent_onion = 0
111
112 if local:
113 local_html = _("<li class='color-yellow'><span>{_[Local]}:</span><b>{local}</b></li>")
114 else:
115 local_html = ""
116
117 peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)]
118 peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip)
119 copy_link = "http://127.0.0.1:43110/%s/?zeronet_peers=%s" % (
120 site.content_manager.contents.get("content.json", {}).get("domain", site.address),
121 ",".join(peer_ips)
122 )
123
124 body.append(_("""
125 <li>
126 <label>
127 {_[Peers]}
128 <small class="label-right"><a href='{copy_link}' id='link-copypeers' class='link-right'>{_[Copy to clipboard]}</a></small>
129 </label>
130 <ul class='graph'>
131 <li style='width: 100%' class='total back-black' title="{_[Total peers]}"></li>
132 <li style='width: {percent_connectable:.0%}' class='connectable back-blue' title='{_[Connectable peers]}'></li>
133 <li style='width: {percent_onion:.0%}' class='connected back-purple' title='{_[Onion]}'></li>
134 <li style='width: {percent_connected:.0%}' class='connected back-green' title='{_[Connected peers]}'></li>
135 </ul>
136 <ul class='graph-legend'>
137 <li class='color-green'><span>{_[Connected]}:</span><b>{connected}</b></li>
138 <li class='color-blue'><span>{_[Connectable]}:</span><b>{connectable}</b></li>
139 <li class='color-purple'><span>{_[Onion]}:</span><b>{onion}</b></li>
140 {local_html}
141 <li class='color-black'><span>{_[Total]}:</span><b>{peers_total}</b></li>
142 </ul>
143 </li>
144 """.replace("{local_html}", local_html)))
145
146 def sidebarRenderTransferStats(self, body, site):
147 recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024
148 sent = float(site.settings.get("bytes_sent", 0)) / 1024 / 1024
149 transfer_total = recv + sent
150 if transfer_total:
151 percent_recv = recv / transfer_total
152 percent_sent = sent / transfer_total
153 else:
154 percent_recv = 0.5
155 percent_sent = 0.5
156
157 body.append(_("""
158 <li>
159 <label>{_[Data transfer]}</label>
160 <ul class='graph graph-stacked'>
161 <li style='width: {percent_recv:.0%}' class='received back-yellow' title="{_[Received bytes]}"></li>
162 <li style='width: {percent_sent:.0%}' class='sent back-green' title="{_[Sent bytes]}"></li>
163 </ul>
164 <ul class='graph-legend'>
165 <li class='color-yellow'><span>{_[Received]}:</span><b>{recv:.2f}MB</b></li>
166 <li class='color-green'<span>{_[Sent]}:</span><b>{sent:.2f}MB</b></li>
167 </ul>
168 </li>
169 """))
170
171 def sidebarRenderFileStats(self, body, site):
172 body.append(_("""
173 <li>
174 <label>
175 {_[Files]}
176 <a href='/list/{site.address}' class='link-right link-outline' id="browse-files">{_[Browse files]}</a>
177 <small class="label-right">
178 <a href='/ZeroNet-Internal/Zip?address={site.address}' id='link-zip' class='link-right' download='site.zip'>{_[Save as .zip]}</a>
179 </small>
180 </label>
181 <ul class='graph graph-stacked'>
182 """))
183
184 extensions = (
185 ("html", "yellow"),
186 ("css", "orange"),
187 ("js", "purple"),
188 ("Image", "green"),
189 ("json", "darkblue"),
190 ("User data", "blue"),
191 ("Other", "white"),
192 ("Total", "black")
193 )
194 # Collect stats
195 size_filetypes = {}
196 size_total = 0
197 contents = site.content_manager.listContents() # Without user files
198 for inner_path in contents:
199 content = site.content_manager.contents[inner_path]
200 if "files" not in content or content["files"] is None:
201 continue
202 for file_name, file_details in list(content["files"].items()):
203 size_total += file_details["size"]
204 ext = file_name.split(".")[-1]
205 size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"]
206
207 # Get user file sizes
208 size_user_content = site.content_manager.contents.execute(
209 "SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?",
210 {"not__inner_path": contents}
211 ).fetchone()["size"]
212 if not size_user_content:
213 size_user_content = 0
214 size_filetypes["User data"] = size_user_content
215 size_total += size_user_content
216
217 # The missing difference is content.json sizes
218 if "json" in size_filetypes:
219 size_filetypes["json"] += max(0, site.settings["size"] - size_total)
220 size_total = size_other = site.settings["size"]
221
222 # Bar
223 for extension, color in extensions:
224 if extension == "Total":
225 continue
226 if extension == "Other":
227 size = max(0, size_other)
228 elif extension == "Image":
229 size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
230 size_other -= size
231 else:
232 size = size_filetypes.get(extension, 0)
233 size_other -= size
234 if size_total == 0:
235 percent = 0
236 else:
237 percent = 100 * (float(size) / size_total)
238 percent = math.floor(percent * 100) / 100 # Floor to 2 digits
239 body.append(
240 """<li style='width: %.2f%%' class='%s back-%s' title="%s"></li>""" %
241 (percent, _[extension], color, _[extension])
242 )
243
244 # Legend
245 body.append("</ul><ul class='graph-legend'>")
246 for extension, color in extensions:
247 if extension == "Other":
248 size = max(0, size_other)
249 elif extension == "Image":
250 size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
251 elif extension == "Total":
252 size = size_total
253 else:
254 size = size_filetypes.get(extension, 0)
255
256 if extension == "js":
257 title = "javascript"
258 else:
259 title = extension
260
261 if size > 1024 * 1024 * 10: # Format as mB is more than 10mB
262 size_formatted = "%.0fMB" % (size / 1024 / 1024)
263 else:
264 size_formatted = "%.0fkB" % (size / 1024)
265
266 body.append("<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, _[title], size_formatted))
267
268 body.append("</ul></li>")
269
270 def sidebarRenderSizeLimit(self, body, site):
271 free_space = helper.getFreeSpace() / 1024 / 1024
272 size = float(site.settings["size"]) / 1024 / 1024
273 size_limit = site.getSizeLimit()
274 percent_used = size / size_limit
275
276 body.append(_("""
277 <li>
278 <label>{_[Size limit]} <small>({_[limit used]}: {percent_used:.0%}, {_[free space]}: {free_space:,.0f}MB)</small></label>
279 <input type='text' class='text text-num' value="{size_limit}" id='input-sitelimit'/><span class='text-post'>MB</span>
280 <a href='#Set' class='button' id='button-sitelimit'>{_[Set]}</a>
281 </li>
282 """))
283
284 def sidebarRenderOptionalFileStats(self, body, site):
285 size_total = float(site.settings["size_optional"])
286 size_downloaded = float(site.settings["optional_downloaded"])
287
288 if not size_total:
289 return False
290
291 percent_downloaded = size_downloaded / size_total
292
293 size_formatted_total = size_total / 1024 / 1024
294 size_formatted_downloaded = size_downloaded / 1024 / 1024
295
296 body.append(_("""
297 <li>
298 <label>{_[Optional files]}</label>
299 <ul class='graph'>
300 <li style='width: 100%' class='total back-black' title="{_[Total size]}"></li>
301 <li style='width: {percent_downloaded:.0%}' class='connected back-green' title='{_[Downloaded files]}'></li>
302 </ul>
303 <ul class='graph-legend'>
304 <li class='color-green'><span>{_[Downloaded]}:</span><b>{size_formatted_downloaded:.2f}MB</b></li>
305 <li class='color-black'><span>{_[Total]}:</span><b>{size_formatted_total:.2f}MB</b></li>
306 </ul>
307 </li>
308 """))
309
310 return True
311
312 def sidebarRenderOptionalFileSettings(self, body, site):
313 if self.site.settings.get("autodownloadoptional"):
314 checked = "checked='checked'"
315 else:
316 checked = ""
317
318 body.append(_("""
319 <li>
320 <label>{_[Help distribute added optional files]}</label>
321 <input type="checkbox" class="checkbox" id="checkbox-autodownloadoptional" {checked}/><div class="checkbox-skin"></div>
322 """))
323
324 if hasattr(config, "autodownload_bigfile_size_limit"):
325 autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit))
326 body.append(_("""
327 <div class='settings-autodownloadoptional'>
328 <label>{_[Auto download big file size limit]}</label>
329 <input type='text' class='text text-num' value="{autodownload_bigfile_size_limit}" id='input-autodownload_bigfile_size_limit'/><span class='text-post'>MB</span>
330 <a href='#Set' class='button' id='button-autodownload_bigfile_size_limit'>{_[Set]}</a>
331 <a href='#Download+previous' class='button' id='button-autodownload_previous'>{_[Download previous files]}</a>
332 </div>
333 """))
334 body.append("</li>")
335
336 def sidebarRenderBadFiles(self, body, site):
337 body.append(_("""
338 <li>
339 <label>{_[Needs to be updated]}:</label>
340 <ul class='filelist'>
341 """))
342
343 i = 0
344 for bad_file, tries in site.bad_files.items():
345 i += 1
346 body.append(_("""<li class='color-red' title="{bad_file_path} ({tries})">{bad_filename}</li>""", {
347 "bad_file_path": bad_file,
348 "bad_filename": helper.getFilename(bad_file),
349 "tries": _.pluralize(tries, "{} try", "{} tries")
350 }))
351 if i > 30:
352 break
353
354 if len(site.bad_files) > 30:
355 num_bad_files = len(site.bad_files) - 30
356 body.append(_("""<li class='color-red'>{_[+ {num_bad_files} more]}</li>""", nested=True))
357
358 body.append("""
359 </ul>
360 </li>
361 """)
362
363 def sidebarRenderDbOptions(self, body, site):
364 if site.storage.db:
365 inner_path = site.storage.getInnerPath(site.storage.db.db_path)
366 size = float(site.storage.getSize(inner_path)) / 1024
367 feeds = len(site.storage.db.schema.get("feeds", {}))
368 else:
369 inner_path = _["No database found"]
370 size = 0.0
371 feeds = 0
372
373 body.append(_("""
374 <li>
375 <label>{_[Database]} <small>({size:.2f}kB, {_[search feeds]}: {_[{feeds} query]})</small></label>
376 <div class='flex'>
377 <input type='text' class='text disabled' value="{inner_path}" disabled='disabled'/>
378 <a href='#Reload' id="button-dbreload" class='button'>{_[Reload]}</a>
379 <a href='#Rebuild' id="button-dbrebuild" class='button'>{_[Rebuild]}</a>
380 </div>
381 </li>
382 """, nested=True))
383
384 def sidebarRenderIdentity(self, body, site):
385 auth_address = self.user.getAuthAddress(self.site.address, create=False)
386 rules = self.site.content_manager.getRules("data/users/%s/content.json" % auth_address)
387 if rules and rules.get("max_size"):
388 quota = rules["max_size"] / 1024
389 try:
390 content = site.content_manager.contents["data/users/%s/content.json" % auth_address]
391 used = len(json.dumps(content)) + sum([file["size"] for file in list(content["files"].values())])
392 except:
393 used = 0
394 used = used / 1024
395 else:
396 quota = used = 0
397
398 body.append(_("""
399 <li>
400 <label>{_[Identity address]} <small>({_[limit used]}: {used:.2f}kB / {quota:.2f}kB)</small></label>
401 <div class='flex'>
402 <span class='input text disabled'>{auth_address}</span>
403 <a href='#Change' class='button' id='button-identity'>{_[Change]}</a>
404 </div>
405 </li>
406 """))
407
408 def sidebarRenderControls(self, body, site):
409 auth_address = self.user.getAuthAddress(self.site.address, create=False)
410 if self.site.settings["serving"]:
411 class_pause = ""
412 class_resume = "hidden"
413 else:
414 class_pause = "hidden"
415 class_resume = ""
416
417 body.append(_("""
418 <li>
419 <label>{_[Site control]}</label>
420 <a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
421 <a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
422 <a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
423 <a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
424 </li>
425 """))
426
427 donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True)
428 site_address = self.site.address
429 body.append(_("""
430 <li>
431 <label>{_[Site address]}</label><br>
432 <div class='flex'>
433 <span class='input text disabled'>{site_address}</span>
434 """))
435 if donate_key == False or donate_key == "":
436 pass
437 elif (type(donate_key) == str or type(donate_key) == str) and len(donate_key) > 0:
438 body.append(_("""
439 </div>
440 </li>
441 <li>
442 <label>{_[Donate]}</label><br>
443 <div class='flex'>
444 {donate_key}
445 """))
446 else:
447 body.append(_("""
448 <a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a>
449 """))
450 body.append(_("""
451 </div>
452 </li>
453 """))
454
455 def sidebarRenderOwnedCheckbox(self, body, site):
456 if self.site.settings["own"]:
457 checked = "checked='checked'"
458 else:
459 checked = ""
460
461 body.append(_("""
462 <h2 class='owned-title'>{_[This is my site]}</h2>
463 <input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
464 """))
465
466 def sidebarRenderOwnSettings(self, body, site):
467 title = site.content_manager.contents.get("content.json", {}).get("title", "")
468 description = site.content_manager.contents.get("content.json", {}).get("description", "")
469
470 body.append(_("""
471 <li>
472 <label for='settings-title'>{_[Site title]}</label>
473 <input type='text' class='text' value="{title}" id='settings-title'/>
474 </li>
475
476 <li>
477 <label for='settings-description'>{_[Site description]}</label>
478 <input type='text' class='text' value="{description}" id='settings-description'/>
479 </li>
480
481 <li>
482 <a href='#Save' class='button' id='button-settings'>{_[Save site settings]}</a>
483 </li>
484 """))
485
486 def sidebarRenderContents(self, body, site):
487 has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey"))
488 if has_privatekey:
489 tag_privatekey = _("{_[Private key saved.]} <a href='#Forget+private+key' id='privatekey-forget' class='link-right'>{_[Forget]}</a>")
490 else:
491 tag_privatekey = _("<a href='#Add+private+key' id='privatekey-add' class='link-right'>{_[Add saved private key]}</a>")
492
493 body.append(_("""
494 <li>
495 <label>{_[Content publishing]} <small class='label-right'>{tag_privatekey}</small></label>
496 """.replace("{tag_privatekey}", tag_privatekey)))
497
498 # Choose content you want to sign
499 body.append(_("""
500 <div class='flex'>
501 <input type='text' class='text' value="content.json" id='input-contents'/>
502 <a href='#Sign-and-Publish' id='button-sign-publish' class='button'>{_[Sign and publish]}</a>
503 <a href='#Sign-or-Publish' id='menu-sign-publish'>\u22EE</a>
504 </div>
505 """))
506
507 contents = ["content.json"]
508 contents += list(site.content_manager.contents.get("content.json", {}).get("includes", {}).keys())
509 body.append(_("<div class='contents'>{_[Choose]}: "))
510 for content in contents:
511 body.append(_("<a href='{content}' class='contents-content'>{content}</a> "))
512 body.append("</div>")
513 body.append("</li>")
514
515 @flag.admin
516 def actionSidebarGetHtmlTag(self, to):
517 site = self.site
518
519 body = []
520
521 body.append("<div>")
522 body.append("<a href='#Close' class='close'>×</a>")
523 body.append("<h1>%s</h1>" % html.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True))
524
525 body.append("<div class='globe loading'></div>")
526
527 body.append("<ul class='fields'>")
528
529 self.sidebarRenderPeerStats(body, site)
530 self.sidebarRenderTransferStats(body, site)
531 self.sidebarRenderFileStats(body, site)
532 self.sidebarRenderSizeLimit(body, site)
533 has_optional = self.sidebarRenderOptionalFileStats(body, site)
534 if has_optional:
535 self.sidebarRenderOptionalFileSettings(body, site)
536 self.sidebarRenderDbOptions(body, site)
537 self.sidebarRenderIdentity(body, site)
538 self.sidebarRenderControls(body, site)
539 if site.bad_files:
540 self.sidebarRenderBadFiles(body, site)
541
542 self.sidebarRenderOwnedCheckbox(body, site)
543 body.append("<div class='settings-owned'>")
544 self.sidebarRenderOwnSettings(body, site)
545 self.sidebarRenderContents(body, site)
546 body.append("</div>")
547 body.append("</ul>")
548 body.append("</div>")
549
550 body.append("<div class='menu template'>")
551 body.append("<a href='#'' class='menu-item template'>Template</a>")
552 body.append("</div>")
553
554 self.response(to, "".join(body))
555
556 def downloadGeoLiteDb(self, db_path):
557 import gzip
558 import shutil
559 from util import helper
560
561 if config.offline:
562 return False
563
564 self.log.info("Downloading GeoLite2 City database...")
565 self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
566 db_urls = [
567 "https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz",
568 "https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
569 ]
570 for db_url in db_urls:
571 downloadl_err = None
572 try:
573 # Download
574 response = helper.httpRequest(db_url)
575 data_size = response.getheader('content-length')
576 data_recv = 0
577 data = io.BytesIO()
578 while True:
579 buff = response.read(1024 * 512)
580 if not buff:
581 break
582 data.write(buff)
583 data_recv += 1024 * 512
584 if data_size:
585 progress = int(float(data_recv) / int(data_size) * 100)
586 self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
587 self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
588 data.seek(0)
589
590 # Unpack
591 with gzip.GzipFile(fileobj=data) as gzip_file:
592 shutil.copyfileobj(gzip_file, open(db_path, "wb"))
593
594 self.cmd("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100])
595 time.sleep(2) # Wait for notify animation
596 self.log.info("GeoLite2 City database is ready at: %s" % db_path)
597 return True
598 except Exception as err:
599 download_err = err
600 self.log.error("Error downloading %s: %s" % (db_url, err))
601 pass
602 self.cmd("progress", [
603 "geolite-info",
604 _["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(download_err, db_urls[0]),
605 -100
606 ])
607
608 def getLoc(self, geodb, ip):
609 global loc_cache
610
611 if ip in loc_cache:
612 return loc_cache[ip]
613 else:
614 try:
615 loc_data = geodb.get(ip)
616 except:
617 loc_data = None
618
619 if not loc_data or "location" not in loc_data:
620 loc_cache[ip] = None
621 return None
622
623 loc = {
624 "lat": loc_data["location"]["latitude"],
625 "lon": loc_data["location"]["longitude"],
626 }
627 if "city" in loc_data:
628 loc["city"] = loc_data["city"]["names"]["en"]
629
630 if "country" in loc_data:
631 loc["country"] = loc_data["country"]["names"]["en"]
632
633 loc_cache[ip] = loc
634 return loc
635
636 @util.Noparallel()
637 def getGeoipDb(self):
638 db_name = 'GeoLite2-City.mmdb'
639
640 sys_db_paths = []
641 if sys.platform == "linux":
642 sys_db_paths += ['/usr/share/GeoIP/' + db_name]
643
644 data_dir_db_path = os.path.join(config.data_dir, db_name)
645
646 db_paths = sys_db_paths + [data_dir_db_path]
647
648 for path in db_paths:
649 if os.path.isfile(path) and os.path.getsize(path) > 0:
650 return path
651
652 self.log.info("GeoIP database not found at [%s]. Downloading to: %s",
653 " ".join(db_paths), data_dir_db_path)
654 if self.downloadGeoLiteDb(data_dir_db_path):
655 return data_dir_db_path
656 return None
657
658 def getPeerLocations(self, peers):
659 import maxminddb
660
661 db_path = self.getGeoipDb()
662 if not db_path:
663 self.log.debug("Not showing peer locations: no GeoIP database")
664 return False
665
666 geodb = maxminddb.open_database(db_path)
667
668 peers = list(peers.values())
669 # Place bars
670 peer_locations = []
671 placed = {} # Already placed bars here
672 for peer in peers:
673 # Height of bar
674 if peer.connection and peer.connection.last_ping_delay:
675 ping = round(peer.connection.last_ping_delay * 1000)
676 else:
677 ping = None
678 loc = self.getLoc(geodb, peer.ip)
679
680 if not loc:
681 continue
682 # Create position array
683 lat, lon = loc["lat"], loc["lon"]
684 latlon = "%s,%s" % (lat, lon)
685 if latlon in placed and helper.getIpType(peer.ip) == "ipv4": # Dont place more than 1 bar to same place, fake repos using ip address last two part
686 lat += float(128 - int(peer.ip.split(".")[-2])) / 50
687 lon += float(128 - int(peer.ip.split(".")[-1])) / 50
688 latlon = "%s,%s" % (lat, lon)
689 placed[latlon] = True
690 peer_location = {}
691 peer_location.update(loc)
692 peer_location["lat"] = lat
693 peer_location["lon"] = lon
694 peer_location["ping"] = ping
695
696 peer_locations.append(peer_location)
697
698 # Append myself
699 for ip in self.site.connection_server.ip_external_list:
700 my_loc = self.getLoc(geodb, ip)
701 if my_loc:
702 my_loc["ping"] = 0
703 peer_locations.append(my_loc)
704
705 return peer_locations
706
707 @flag.admin
708 @flag.async_run
709 def actionSidebarGetPeers(self, to):
710 try:
711 peer_locations = self.getPeerLocations(self.site.peers)
712 globe_data = []
713 ping_times = [
714 peer_location["ping"]
715 for peer_location in peer_locations
716 if peer_location["ping"]
717 ]
718 if ping_times:
719 ping_avg = sum(ping_times) / float(len(ping_times))
720 else:
721 ping_avg = 0
722
723 for peer_location in peer_locations:
724 if peer_location["ping"] == 0: # Me
725 height = -0.135
726 elif peer_location["ping"]:
727 height = min(0.20, math.log(1 + peer_location["ping"] / ping_avg, 300))
728 else:
729 height = -0.03
730
731 globe_data += [peer_location["lat"], peer_location["lon"], height]
732
733 self.response(to, globe_data)
734 except Exception as err:
735 self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
736 self.response(to, {"error": str(err)})
737
738 @flag.admin
739 @flag.no_multiuser
740 def actionSiteSetOwned(self, to, owned):
741 if self.site.address == config.updatesite:
742 return {"error": "You can't change the ownership of the updater site"}
743
744 self.site.settings["own"] = bool(owned)
745 self.site.updateWebsocket(owned=owned)
746 return "ok"
747
748 @flag.admin
749 @flag.no_multiuser
750 def actionSiteRecoverPrivatekey(self, to):
751 from Crypt import CryptBitcoin
752
753 site_data = self.user.sites[self.site.address]
754 if site_data.get("privatekey"):
755 return {"error": "This site already has saved privated key"}
756
757 address_index = self.site.content_manager.contents.get("content.json", {}).get("address_index")
758 if not address_index:
759 return {"error": "No address_index in content.json"}
760
761 privatekey = CryptBitcoin.hdPrivatekey(self.user.master_seed, address_index)
762 privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey)
763
764 if privatekey_address == self.site.address:
765 site_data["privatekey"] = privatekey
766 self.user.save()
767 self.site.updateWebsocket(recover_privatekey=True)
768 return "ok"
769 else:
770 return {"error": "Unable to deliver private key for this site from current user's master_seed"}
771
772 @flag.admin
773 @flag.no_multiuser
774 def actionUserSetSitePrivatekey(self, to, privatekey):
775 site_data = self.user.sites[self.site.address]
776 site_data["privatekey"] = privatekey
777 self.site.updateWebsocket(set_privatekey=bool(privatekey))
778 self.user.save()
779
780 return "ok"
781
782 @flag.admin
783 @flag.no_multiuser
784 def actionSiteSetAutodownloadoptional(self, to, owned):
785 self.site.settings["autodownloadoptional"] = bool(owned)
786 self.site.worker_manager.removeSolvedFileTasks()
787
788 @flag.no_multiuser
789 @flag.admin
790 def actionDbReload(self, to):
791 self.site.storage.closeDb()
792 self.site.storage.getDb()
793
794 return self.response(to, "ok")
795
796 @flag.no_multiuser
797 @flag.admin
798 def actionDbRebuild(self, to):
799 try:
800 self.site.storage.rebuildDb()
801 except Exception as err:
802 return self.response(to, {"error": str(err)})
803
804
805 return self.response(to, "ok")