SidebarPlugin.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  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='/ZeroNet-Internal/Zip?address={site.address}' id='link-zip' class='link-right' download='site.zip'>{_[Save as .zip]}</a>
  158. </small>
  159. </label>
  160. <ul class='graph graph-stacked'>
  161. """))
  162. extensions = (
  163. ("html", "yellow"),
  164. ("css", "orange"),
  165. ("js", "purple"),
  166. ("Image", "green"),
  167. ("json", "darkblue"),
  168. ("User data", "blue"),
  169. ("Other", "white"),
  170. ("Total", "black")
  171. )
  172. # Collect stats
  173. size_filetypes = {}
  174. size_total = 0
  175. contents = site.content_manager.listContents() # Without user files
  176. for inner_path in contents:
  177. content = site.content_manager.contents[inner_path]
  178. if "files" not in content or content["files"] is None:
  179. continue
  180. for file_name, file_details in list(content["files"].items()):
  181. size_total += file_details["size"]
  182. ext = file_name.split(".")[-1]
  183. size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"]
  184. # Get user file sizes
  185. size_user_content = site.content_manager.contents.execute(
  186. "SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?",
  187. {"not__inner_path": contents}
  188. ).fetchone()["size"]
  189. if not size_user_content:
  190. size_user_content = 0
  191. size_filetypes["User data"] = size_user_content
  192. size_total += size_user_content
  193. # The missing difference is content.json sizes
  194. if "json" in size_filetypes:
  195. size_filetypes["json"] += max(0, site.settings["size"] - size_total)
  196. size_total = size_other = site.settings["size"]
  197. # Bar
  198. for extension, color in extensions:
  199. if extension == "Total":
  200. continue
  201. if extension == "Other":
  202. size = max(0, size_other)
  203. elif extension == "Image":
  204. size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
  205. size_other -= size
  206. else:
  207. size = size_filetypes.get(extension, 0)
  208. size_other -= size
  209. if size_total == 0:
  210. percent = 0
  211. else:
  212. percent = 100 * (float(size) / size_total)
  213. percent = math.floor(percent * 100) / 100 # Floor to 2 digits
  214. body.append(
  215. """<li style='width: %.2f%%' class='%s back-%s' title="%s"></li>""" %
  216. (percent, _[extension], color, _[extension])
  217. )
  218. # Legend
  219. body.append("</ul><ul class='graph-legend'>")
  220. for extension, color in extensions:
  221. if extension == "Other":
  222. size = max(0, size_other)
  223. elif extension == "Image":
  224. size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0)
  225. elif extension == "Total":
  226. size = size_total
  227. else:
  228. size = size_filetypes.get(extension, 0)
  229. if extension == "js":
  230. title = "javascript"
  231. else:
  232. title = extension
  233. if size > 1024 * 1024 * 10: # Format as mB is more than 10mB
  234. size_formatted = "%.0fMB" % (size / 1024 / 1024)
  235. else:
  236. size_formatted = "%.0fkB" % (size / 1024)
  237. body.append("<li class='color-%s'><span>%s:</span><b>%s</b></li>" % (color, _[title], size_formatted))
  238. body.append("</ul></li>")
  239. def sidebarRenderSizeLimit(self, body, site):
  240. free_space = helper.getFreeSpace() / 1024 / 1024
  241. size = float(site.settings["size"]) / 1024 / 1024
  242. size_limit = site.getSizeLimit()
  243. percent_used = size / size_limit
  244. body.append(_("""
  245. <li>
  246. <label>{_[Size limit]} <small>({_[limit used]}: {percent_used:.0%}, {_[free space]}: {free_space:,.0f}MB)</small></label>
  247. <input type='text' class='text text-num' value="{size_limit}" id='input-sitelimit'/><span class='text-post'>MB</span>
  248. <a href='#Set' class='button' id='button-sitelimit'>{_[Set]}</a>
  249. </li>
  250. """))
  251. def sidebarRenderOptionalFileStats(self, body, site):
  252. size_total = float(site.settings["size_optional"])
  253. size_downloaded = float(site.settings["optional_downloaded"])
  254. if not size_total:
  255. return False
  256. percent_downloaded = size_downloaded / size_total
  257. size_formatted_total = size_total / 1024 / 1024
  258. size_formatted_downloaded = size_downloaded / 1024 / 1024
  259. body.append(_("""
  260. <li>
  261. <label>{_[Optional files]}</label>
  262. <ul class='graph'>
  263. <li style='width: 100%' class='total back-black' title="{_[Total size]}"></li>
  264. <li style='width: {percent_downloaded:.0%}' class='connected back-green' title='{_[Downloaded files]}'></li>
  265. </ul>
  266. <ul class='graph-legend'>
  267. <li class='color-green'><span>{_[Downloaded]}:</span><b>{size_formatted_downloaded:.2f}MB</b></li>
  268. <li class='color-black'><span>{_[Total]}:</span><b>{size_formatted_total:.2f}MB</b></li>
  269. </ul>
  270. </li>
  271. """))
  272. return True
  273. def sidebarRenderOptionalFileSettings(self, body, site):
  274. if self.site.settings.get("autodownloadoptional"):
  275. checked = "checked='checked'"
  276. else:
  277. checked = ""
  278. body.append(_("""
  279. <li>
  280. <label>{_[Help distribute added optional files]}</label>
  281. <input type="checkbox" class="checkbox" id="checkbox-autodownloadoptional" {checked}/><div class="checkbox-skin"></div>
  282. """))
  283. if hasattr(config, "autodownload_bigfile_size_limit"):
  284. autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit))
  285. body.append(_("""
  286. <div class='settings-autodownloadoptional'>
  287. <label>{_[Auto download big file size limit]}</label>
  288. <input type='text' class='text text-num' value="{autodownload_bigfile_size_limit}" id='input-autodownload_bigfile_size_limit'/><span class='text-post'>MB</span>
  289. <a href='#Set' class='button' id='button-autodownload_bigfile_size_limit'>{_[Set]}</a>
  290. <a href='#Download+previous' class='button' id='button-autodownload_previous'>{_[Download previous files]}</a>
  291. </div>
  292. """))
  293. body.append("</li>")
  294. def sidebarRenderBadFiles(self, body, site):
  295. body.append(_("""
  296. <li>
  297. <label>{_[Needs to be updated]}:</label>
  298. <ul class='filelist'>
  299. """))
  300. i = 0
  301. for bad_file, tries in site.bad_files.items():
  302. i += 1
  303. body.append(_("""<li class='color-red' title="{bad_file_path} ({tries})">{bad_filename}</li>""", {
  304. "bad_file_path": bad_file,
  305. "bad_filename": helper.getFilename(bad_file),
  306. "tries": _.pluralize(tries, "{} try", "{} tries")
  307. }))
  308. if i > 30:
  309. break
  310. if len(site.bad_files) > 30:
  311. num_bad_files = len(site.bad_files) - 30
  312. body.append(_("""<li class='color-red'>{_[+ {num_bad_files} more]}</li>""", nested=True))
  313. body.append("""
  314. </ul>
  315. </li>
  316. """)
  317. def sidebarRenderDbOptions(self, body, site):
  318. if site.storage.db:
  319. inner_path = site.storage.getInnerPath(site.storage.db.db_path)
  320. size = float(site.storage.getSize(inner_path)) / 1024
  321. feeds = len(site.storage.db.schema.get("feeds", {}))
  322. else:
  323. inner_path = _["No database found"]
  324. size = 0.0
  325. feeds = 0
  326. body.append(_("""
  327. <li>
  328. <label>{_[Database]} <small>({size:.2f}kB, {_[search feeds]}: {_[{feeds} query]})</small></label>
  329. <div class='flex'>
  330. <input type='text' class='text disabled' value="{inner_path}" disabled='disabled'/>
  331. <a href='#Reload' id="button-dbreload" class='button'>{_[Reload]}</a>
  332. <a href='#Rebuild' id="button-dbrebuild" class='button'>{_[Rebuild]}</a>
  333. </div>
  334. </li>
  335. """, nested=True))
  336. def sidebarRenderIdentity(self, body, site):
  337. auth_address = self.user.getAuthAddress(self.site.address, create=False)
  338. rules = self.site.content_manager.getRules("data/users/%s/content.json" % auth_address)
  339. if rules and rules.get("max_size"):
  340. quota = rules["max_size"] / 1024
  341. try:
  342. content = site.content_manager.contents["data/users/%s/content.json" % auth_address]
  343. used = len(json.dumps(content)) + sum([file["size"] for file in list(content["files"].values())])
  344. except:
  345. used = 0
  346. used = used / 1024
  347. else:
  348. quota = used = 0
  349. body.append(_("""
  350. <li>
  351. <label>{_[Identity address]} <small>({_[limit used]}: {used:.2f}kB / {quota:.2f}kB)</small></label>
  352. <div class='flex'>
  353. <span class='input text disabled'>{auth_address}</span>
  354. <a href='#Change' class='button' id='button-identity'>{_[Change]}</a>
  355. </div>
  356. </li>
  357. """))
  358. def sidebarRenderControls(self, body, site):
  359. auth_address = self.user.getAuthAddress(self.site.address, create=False)
  360. if self.site.settings["serving"]:
  361. class_pause = ""
  362. class_resume = "hidden"
  363. else:
  364. class_pause = "hidden"
  365. class_resume = ""
  366. body.append(_("""
  367. <li>
  368. <label>{_[Site control]}</label>
  369. <a href='#Update' class='button noupdate' id='button-update'>{_[Update]}</a>
  370. <a href='#Pause' class='button {class_pause}' id='button-pause'>{_[Pause]}</a>
  371. <a href='#Resume' class='button {class_resume}' id='button-resume'>{_[Resume]}</a>
  372. <a href='#Delete' class='button noupdate' id='button-delete'>{_[Delete]}</a>
  373. </li>
  374. """))
  375. donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True)
  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. """))
  383. if donate_key == False or donate_key == "":
  384. pass
  385. elif (type(donate_key) == str or type(donate_key) == str) and len(donate_key) > 0:
  386. body.append(_("""
  387. </div>
  388. </li>
  389. <li>
  390. <label>{_[Donate]}</label><br>
  391. <div class='flex'>
  392. {donate_key}
  393. """))
  394. else:
  395. body.append(_("""
  396. <a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a>
  397. """))
  398. body.append(_("""
  399. </div>
  400. </li>
  401. """))
  402. def sidebarRenderOwnedCheckbox(self, body, site):
  403. if self.site.settings["own"]:
  404. checked = "checked='checked'"
  405. else:
  406. checked = ""
  407. body.append(_("""
  408. <h2 class='owned-title'>{_[This is my site]}</h2>
  409. <input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
  410. """))
  411. def sidebarRenderOwnSettings(self, body, site):
  412. title = site.content_manager.contents.get("content.json", {}).get("title", "")
  413. description = site.content_manager.contents.get("content.json", {}).get("description", "")
  414. body.append(_("""
  415. <li>
  416. <label for='settings-title'>{_[Site title]}</label>
  417. <input type='text' class='text' value="{title}" id='settings-title'/>
  418. </li>
  419. <li>
  420. <label for='settings-description'>{_[Site description]}</label>
  421. <input type='text' class='text' value="{description}" id='settings-description'/>
  422. </li>
  423. <li>
  424. <a href='#Save' class='button' id='button-settings'>{_[Save site settings]}</a>
  425. </li>
  426. """))
  427. def sidebarRenderContents(self, body, site):
  428. has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey"))
  429. if has_privatekey:
  430. tag_privatekey = _("{_[Private key saved.]} <a href='#Forget+private+key' id='privatekey-forget' class='link-right'>{_[Forget]}</a>")
  431. else:
  432. tag_privatekey = _("<a href='#Add+private+key' id='privatekey-add' class='link-right'>{_[Add saved private key]}</a>")
  433. body.append(_("""
  434. <li>
  435. <label>{_[Content publishing]} <small class='label-right'>{tag_privatekey}</small></label>
  436. """.replace("{tag_privatekey}", tag_privatekey)))
  437. # Choose content you want to sign
  438. body.append(_("""
  439. <div class='flex'>
  440. <input type='text' class='text' value="content.json" id='input-contents'/>
  441. <a href='#Sign-and-Publish' id='button-sign-publish' class='button'>{_[Sign and publish]}</a>
  442. <a href='#Sign-or-Publish' id='menu-sign-publish'>\u22EE</a>
  443. </div>
  444. """))
  445. contents = ["content.json"]
  446. contents += list(site.content_manager.contents.get("content.json", {}).get("includes", {}).keys())
  447. body.append(_("<div class='contents'>{_[Choose]}: "))
  448. for content in contents:
  449. body.append(_("<a href='{content}' class='contents-content'>{content}</a> "))
  450. body.append("</div>")
  451. body.append("</li>")
  452. @flag.admin
  453. def actionSidebarGetHtmlTag(self, to):
  454. site = self.site
  455. body = []
  456. body.append("<div>")
  457. body.append("<a href='#Close' class='close'>&times;</a>")
  458. body.append("<h1>%s</h1>" % html.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True))
  459. body.append("<div class='globe loading'></div>")
  460. body.append("<ul class='fields'>")
  461. self.sidebarRenderPeerStats(body, site)
  462. self.sidebarRenderTransferStats(body, site)
  463. self.sidebarRenderFileStats(body, site)
  464. self.sidebarRenderSizeLimit(body, site)
  465. has_optional = self.sidebarRenderOptionalFileStats(body, site)
  466. if has_optional:
  467. self.sidebarRenderOptionalFileSettings(body, site)
  468. self.sidebarRenderDbOptions(body, site)
  469. self.sidebarRenderIdentity(body, site)
  470. self.sidebarRenderControls(body, site)
  471. if site.bad_files:
  472. self.sidebarRenderBadFiles(body, site)
  473. self.sidebarRenderOwnedCheckbox(body, site)
  474. body.append("<div class='settings-owned'>")
  475. self.sidebarRenderOwnSettings(body, site)
  476. self.sidebarRenderContents(body, site)
  477. body.append("</div>")
  478. body.append("</ul>")
  479. body.append("</div>")
  480. body.append("<div class='menu template'>")
  481. body.append("<a href='#'' class='menu-item template'>Template</a>")
  482. body.append("</div>")
  483. self.response(to, "".join(body))
  484. def downloadGeoLiteDb(self, db_path):
  485. import gzip
  486. import shutil
  487. from util import helper
  488. if config.offline:
  489. return False
  490. self.log.info("Downloading GeoLite2 City database...")
  491. self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
  492. db_urls = [
  493. "https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz",
  494. "https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
  495. ]
  496. for db_url in db_urls:
  497. downloadl_err = None
  498. try:
  499. # Download
  500. response = helper.httpRequest(db_url)
  501. data_size = response.getheader('content-length')
  502. data_recv = 0
  503. data = io.BytesIO()
  504. while True:
  505. buff = response.read(1024 * 512)
  506. if not buff:
  507. break
  508. data.write(buff)
  509. data_recv += 1024 * 512
  510. if data_size:
  511. progress = int(float(data_recv) / int(data_size) * 100)
  512. self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
  513. self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
  514. data.seek(0)
  515. # Unpack
  516. with gzip.GzipFile(fileobj=data) as gzip_file:
  517. shutil.copyfileobj(gzip_file, open(db_path, "wb"))
  518. self.cmd("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100])
  519. time.sleep(2) # Wait for notify animation
  520. self.log.info("GeoLite2 City database is ready at: %s" % db_path)
  521. return True
  522. except Exception as err:
  523. download_err = err
  524. self.log.error("Error downloading %s: %s" % (db_url, err))
  525. pass
  526. self.cmd("progress", [
  527. "geolite-info",
  528. _["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(download_err, db_urls[0]),
  529. -100
  530. ])
  531. def getLoc(self, geodb, ip):
  532. global loc_cache
  533. if ip in loc_cache:
  534. return loc_cache[ip]
  535. else:
  536. try:
  537. loc_data = geodb.get(ip)
  538. except:
  539. loc_data = None
  540. if not loc_data or "location" not in loc_data:
  541. loc_cache[ip] = None
  542. return None
  543. loc = {
  544. "lat": loc_data["location"]["latitude"],
  545. "lon": loc_data["location"]["longitude"],
  546. }
  547. if "city" in loc_data:
  548. loc["city"] = loc_data["city"]["names"]["en"]
  549. if "country" in loc_data:
  550. loc["country"] = loc_data["country"]["names"]["en"]
  551. loc_cache[ip] = loc
  552. return loc
  553. @util.Noparallel()
  554. def getGeoipDb(self):
  555. db_name = 'GeoLite2-City.mmdb'
  556. sys_db_paths = []
  557. if sys.platform == "linux":
  558. sys_db_paths += ['/usr/share/GeoIP/' + db_name]
  559. data_dir_db_path = os.path.join(config.data_dir, db_name)
  560. db_paths = sys_db_paths + [data_dir_db_path]
  561. for path in db_paths:
  562. if os.path.isfile(path) and os.path.getsize(path) > 0:
  563. return path
  564. self.log.info("GeoIP database not found at [%s]. Downloading to: %s",
  565. " ".join(db_paths), data_dir_db_path)
  566. if self.downloadGeoLiteDb(data_dir_db_path):
  567. return data_dir_db_path
  568. return None
  569. def getPeerLocations(self, peers):
  570. import maxminddb
  571. db_path = self.getGeoipDb()
  572. if not db_path:
  573. self.log.debug("Not showing peer locations: no GeoIP database")
  574. return False
  575. geodb = maxminddb.open_database(db_path)
  576. peers = list(peers.values())
  577. # Place bars
  578. peer_locations = []
  579. placed = {} # Already placed bars here
  580. for peer in peers:
  581. # Height of bar
  582. if peer.connection and peer.connection.last_ping_delay:
  583. ping = round(peer.connection.last_ping_delay * 1000)
  584. else:
  585. ping = None
  586. loc = self.getLoc(geodb, peer.ip)
  587. if not loc:
  588. continue
  589. # Create position array
  590. lat, lon = loc["lat"], loc["lon"]
  591. latlon = "%s,%s" % (lat, lon)
  592. 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
  593. lat += float(128 - int(peer.ip.split(".")[-2])) / 50
  594. lon += float(128 - int(peer.ip.split(".")[-1])) / 50
  595. latlon = "%s,%s" % (lat, lon)
  596. placed[latlon] = True
  597. peer_location = {}
  598. peer_location.update(loc)
  599. peer_location["lat"] = lat
  600. peer_location["lon"] = lon
  601. peer_location["ping"] = ping
  602. peer_locations.append(peer_location)
  603. # Append myself
  604. for ip in self.site.connection_server.ip_external_list:
  605. my_loc = self.getLoc(geodb, ip)
  606. if my_loc:
  607. my_loc["ping"] = 0
  608. peer_locations.append(my_loc)
  609. return peer_locations
  610. @flag.admin
  611. @flag.async_run
  612. def actionSidebarGetPeers(self, to):
  613. try:
  614. peer_locations = self.getPeerLocations(self.site.peers)
  615. globe_data = []
  616. ping_times = [
  617. peer_location["ping"]
  618. for peer_location in peer_locations
  619. if peer_location["ping"]
  620. ]
  621. if ping_times:
  622. ping_avg = sum(ping_times) / float(len(ping_times))
  623. else:
  624. ping_avg = 0
  625. for peer_location in peer_locations:
  626. if peer_location["ping"] == 0: # Me
  627. height = -0.135
  628. elif peer_location["ping"]:
  629. height = min(0.20, math.log(1 + peer_location["ping"] / ping_avg, 300))
  630. else:
  631. height = -0.03
  632. globe_data += [peer_location["lat"], peer_location["lon"], height]
  633. self.response(to, globe_data)
  634. except Exception as err:
  635. self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
  636. self.response(to, {"error": str(err)})
  637. @flag.admin
  638. @flag.no_multiuser
  639. def actionSiteSetOwned(self, to, owned):
  640. if self.site.address == config.updatesite:
  641. return {"error": "You can't change the ownership of the updater site"}
  642. self.site.settings["own"] = bool(owned)
  643. self.site.updateWebsocket(owned=owned)
  644. return "ok"
  645. @flag.admin
  646. @flag.no_multiuser
  647. def actionSiteRecoverPrivatekey(self, to):
  648. from Crypt import CryptBitcoin
  649. site_data = self.user.sites[self.site.address]
  650. if site_data.get("privatekey"):
  651. return {"error": "This site already has saved privated key"}
  652. address_index = self.site.content_manager.contents.get("content.json", {}).get("address_index")
  653. if not address_index:
  654. return {"error": "No address_index in content.json"}
  655. privatekey = CryptBitcoin.hdPrivatekey(self.user.master_seed, address_index)
  656. privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey)
  657. if privatekey_address == self.site.address:
  658. site_data["privatekey"] = privatekey
  659. self.user.save()
  660. self.site.updateWebsocket(recover_privatekey=True)
  661. return "ok"
  662. else:
  663. return {"error": "Unable to deliver private key for this site from current user's master_seed"}
  664. @flag.admin
  665. @flag.no_multiuser
  666. def actionUserSetSitePrivatekey(self, to, privatekey):
  667. site_data = self.user.sites[self.site.address]
  668. site_data["privatekey"] = privatekey
  669. self.site.updateWebsocket(set_privatekey=bool(privatekey))
  670. self.user.save()
  671. return "ok"
  672. @flag.admin
  673. @flag.no_multiuser
  674. def actionSiteSetAutodownloadoptional(self, to, owned):
  675. self.site.settings["autodownloadoptional"] = bool(owned)
  676. self.site.worker_manager.removeSolvedFileTasks()
  677. @flag.no_multiuser
  678. @flag.admin
  679. def actionDbReload(self, to):
  680. self.site.storage.closeDb()
  681. self.site.storage.getDb()
  682. return self.response(to, "ok")
  683. @flag.no_multiuser
  684. @flag.admin
  685. def actionDbRebuild(self, to):
  686. try:
  687. self.site.storage.rebuildDb()
  688. except Exception as err:
  689. return self.response(to, {"error": str(err)})
  690. return self.response(to, "ok")