SidebarPlugin.py 31 KB

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