SidebarPlugin.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  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. donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True)
  377. site_address = self.site.address
  378. body.append(_("""
  379. <li>
  380. <label>{_[Site address]}</label><br>
  381. <div class='flex'>
  382. <span class='input text disabled'>{site_address}</span>
  383. """))
  384. if donate_key == False or donate_key == "":
  385. pass
  386. elif (type(donate_key) == str or type(donate_key) == str) and len(donate_key) > 0:
  387. body.append(_("""
  388. </div>
  389. </li>
  390. <li>
  391. <label>{_[Donate]}</label><br>
  392. <div class='flex'>
  393. {donate_key}
  394. """))
  395. else:
  396. body.append(_("""
  397. <a href='bitcoin:{site_address}' class='button' id='button-donate'>{_[Donate]}</a>
  398. """))
  399. body.append(_("""
  400. </div>
  401. </li>
  402. """))
  403. def sidebarRenderOwnedCheckbox(self, body, site):
  404. if self.site.settings["own"]:
  405. checked = "checked='checked'"
  406. else:
  407. checked = ""
  408. body.append(_("""
  409. <h2 class='owned-title'>{_[This is my site]}</h2>
  410. <input type="checkbox" class="checkbox" id="checkbox-owned" {checked}/><div class="checkbox-skin"></div>
  411. """))
  412. def sidebarRenderOwnSettings(self, body, site):
  413. title = site.content_manager.contents.get("content.json", {}).get("title", "")
  414. description = site.content_manager.contents.get("content.json", {}).get("description", "")
  415. body.append(_("""
  416. <li>
  417. <label for='settings-title'>{_[Site title]}</label>
  418. <input type='text' class='text' value="{title}" id='settings-title'/>
  419. </li>
  420. <li>
  421. <label for='settings-description'>{_[Site description]}</label>
  422. <input type='text' class='text' value="{description}" id='settings-description'/>
  423. </li>
  424. <li>
  425. <a href='#Save' class='button' id='button-settings'>{_[Save site settings]}</a>
  426. </li>
  427. """))
  428. def sidebarRenderContents(self, body, site):
  429. has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey"))
  430. if has_privatekey:
  431. tag_privatekey = _("{_[Private key saved.]} <a href='#Forget+private+key' id='privatekey-forget' class='link-right'>{_[Forget]}</a>")
  432. else:
  433. tag_privatekey = _("<a href='#Add+private+key' id='privatekey-add' class='link-right'>{_[Add saved private key]}</a>")
  434. body.append(_("""
  435. <li>
  436. <label>{_[Content publishing]} <small class='label-right'>{tag_privatekey}</small></label>
  437. """.replace("{tag_privatekey}", tag_privatekey)))
  438. # Choose content you want to sign
  439. body.append(_("""
  440. <div class='flex'>
  441. <input type='text' class='text' value="content.json" id='input-contents'/>
  442. <a href='#Sign-and-Publish' id='button-sign-publish' class='button'>{_[Sign and publish]}</a>
  443. <a href='#Sign-or-Publish' id='menu-sign-publish'>\u22EE</a>
  444. </div>
  445. """))
  446. contents = ["content.json"]
  447. contents += list(site.content_manager.contents.get("content.json", {}).get("includes", {}).keys())
  448. body.append(_("<div class='contents'>{_[Choose]}: "))
  449. for content in contents:
  450. body.append(_("<a href='{content}' class='contents-content'>{content}</a> "))
  451. body.append("</div>")
  452. body.append("</li>")
  453. @flag.admin
  454. def actionSidebarGetHtmlTag(self, to):
  455. site = self.site
  456. body = []
  457. body.append("<div>")
  458. body.append("<a href='#Close' class='close'>&times;</a>")
  459. body.append("<h1>%s</h1>" % html.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True))
  460. body.append("<div class='globe loading'></div>")
  461. body.append("<ul class='fields'>")
  462. self.sidebarRenderPeerStats(body, site)
  463. self.sidebarRenderTransferStats(body, site)
  464. self.sidebarRenderFileStats(body, site)
  465. self.sidebarRenderSizeLimit(body, site)
  466. has_optional = self.sidebarRenderOptionalFileStats(body, site)
  467. if has_optional:
  468. self.sidebarRenderOptionalFileSettings(body, site)
  469. self.sidebarRenderDbOptions(body, site)
  470. self.sidebarRenderIdentity(body, site)
  471. self.sidebarRenderControls(body, site)
  472. if site.bad_files:
  473. self.sidebarRenderBadFiles(body, site)
  474. self.sidebarRenderOwnedCheckbox(body, site)
  475. body.append("<div class='settings-owned'>")
  476. self.sidebarRenderOwnSettings(body, site)
  477. self.sidebarRenderContents(body, site)
  478. body.append("</div>")
  479. body.append("</ul>")
  480. body.append("</div>")
  481. body.append("<div class='menu template'>")
  482. body.append("<a href='#'' class='menu-item template'>Template</a>")
  483. body.append("</div>")
  484. self.response(to, "".join(body))
  485. def downloadGeoLiteDb(self, db_path):
  486. import gzip
  487. import shutil
  488. from util import helper
  489. if config.offline:
  490. return False
  491. self.log.info("Downloading GeoLite2 City database...")
  492. self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0])
  493. db_urls = [
  494. "https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz",
  495. "https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz"
  496. ]
  497. for db_url in db_urls:
  498. downloadl_err = None
  499. try:
  500. # Download
  501. response = helper.httpRequest(db_url)
  502. data_size = response.getheader('content-length')
  503. data_recv = 0
  504. data = io.BytesIO()
  505. while True:
  506. buff = response.read(1024 * 512)
  507. if not buff:
  508. break
  509. data.write(buff)
  510. data_recv += 1024 * 512
  511. if data_size:
  512. progress = int(float(data_recv) / int(data_size) * 100)
  513. self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress])
  514. self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell())
  515. data.seek(0)
  516. # Unpack
  517. with gzip.GzipFile(fileobj=data) as gzip_file:
  518. shutil.copyfileobj(gzip_file, open(db_path, "wb"))
  519. self.cmd("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100])
  520. time.sleep(2) # Wait for notify animation
  521. self.log.info("GeoLite2 City database is ready at: %s" % db_path)
  522. return True
  523. except Exception as err:
  524. download_err = err
  525. self.log.error("Error downloading %s: %s" % (db_url, err))
  526. pass
  527. self.cmd("progress", [
  528. "geolite-info",
  529. _["GeoLite2 City database download error: {}!<br>Please download manually and unpack to data dir:<br>{}"].format(download_err, db_urls[0]),
  530. -100
  531. ])
  532. def getLoc(self, geodb, ip):
  533. global loc_cache
  534. if ip in loc_cache:
  535. return loc_cache[ip]
  536. else:
  537. try:
  538. loc_data = geodb.get(ip)
  539. except:
  540. loc_data = None
  541. if not loc_data or "location" not in loc_data:
  542. loc_cache[ip] = None
  543. return None
  544. loc = {
  545. "lat": loc_data["location"]["latitude"],
  546. "lon": loc_data["location"]["longitude"],
  547. }
  548. if "city" in loc_data:
  549. loc["city"] = loc_data["city"]["names"]["en"]
  550. if "country" in loc_data:
  551. loc["country"] = loc_data["country"]["names"]["en"]
  552. loc_cache[ip] = loc
  553. return loc
  554. @util.Noparallel()
  555. def getGeoipDb(self):
  556. db_name = 'GeoLite2-City.mmdb'
  557. sys_db_paths = []
  558. if sys.platform == "linux":
  559. sys_db_paths += ['/usr/share/GeoIP/' + db_name]
  560. data_dir_db_path = os.path.join(config.data_dir, db_name)
  561. db_paths = sys_db_paths + [data_dir_db_path]
  562. for path in db_paths:
  563. if os.path.isfile(path) and os.path.getsize(path) > 0:
  564. return path
  565. self.log.info("GeoIP database not found at [%s]. Downloading to: %s",
  566. " ".join(db_paths), data_dir_db_path)
  567. if self.downloadGeoLiteDb(data_dir_db_path):
  568. return data_dir_db_path
  569. return None
  570. def getPeerLocations(self, peers):
  571. import maxminddb
  572. db_path = self.getGeoipDb()
  573. if not db_path:
  574. self.log.debug("Not showing peer locations: no GeoIP database")
  575. return False
  576. geodb = maxminddb.open_database(db_path)
  577. peers = list(peers.values())
  578. # Place bars
  579. peer_locations = []
  580. placed = {} # Already placed bars here
  581. for peer in peers:
  582. # Height of bar
  583. if peer.connection and peer.connection.last_ping_delay:
  584. ping = round(peer.connection.last_ping_delay * 1000)
  585. else:
  586. ping = None
  587. loc = self.getLoc(geodb, peer.ip)
  588. if not loc:
  589. continue
  590. # Create position array
  591. lat, lon = loc["lat"], loc["lon"]
  592. latlon = "%s,%s" % (lat, lon)
  593. 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
  594. lat += float(128 - int(peer.ip.split(".")[-2])) / 50
  595. lon += float(128 - int(peer.ip.split(".")[-1])) / 50
  596. latlon = "%s,%s" % (lat, lon)
  597. placed[latlon] = True
  598. peer_location = {}
  599. peer_location.update(loc)
  600. peer_location["lat"] = lat
  601. peer_location["lon"] = lon
  602. peer_location["ping"] = ping
  603. peer_locations.append(peer_location)
  604. # Append myself
  605. for ip in self.site.connection_server.ip_external_list:
  606. my_loc = self.getLoc(geodb, ip)
  607. if my_loc:
  608. my_loc["ping"] = 0
  609. peer_locations.append(my_loc)
  610. return peer_locations
  611. @flag.admin
  612. @flag.async_run
  613. def actionSidebarGetPeers(self, to):
  614. try:
  615. peer_locations = self.getPeerLocations(self.site.peers)
  616. globe_data = []
  617. ping_times = [
  618. peer_location["ping"]
  619. for peer_location in peer_locations
  620. if peer_location["ping"]
  621. ]
  622. if ping_times:
  623. ping_avg = sum(ping_times) / float(len(ping_times))
  624. else:
  625. ping_avg = 0
  626. for peer_location in peer_locations:
  627. if peer_location["ping"] == 0: # Me
  628. height = -0.135
  629. elif peer_location["ping"]:
  630. height = min(0.20, math.log(1 + peer_location["ping"] / ping_avg, 300))
  631. else:
  632. height = -0.03
  633. globe_data += [peer_location["lat"], peer_location["lon"], height]
  634. self.response(to, globe_data)
  635. except Exception as err:
  636. self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err))
  637. self.response(to, {"error": str(err)})
  638. @flag.admin
  639. @flag.no_multiuser
  640. def actionSiteSetOwned(self, to, owned):
  641. if self.site.address == config.updatesite:
  642. return {"error": "You can't change the ownership of the updater site"}
  643. self.site.settings["own"] = bool(owned)
  644. self.site.updateWebsocket(owned=owned)
  645. return "ok"
  646. @flag.admin
  647. @flag.no_multiuser
  648. def actionSiteRecoverPrivatekey(self, to):
  649. from Crypt import CryptBitcoin
  650. site_data = self.user.sites[self.site.address]
  651. if site_data.get("privatekey"):
  652. return {"error": "This site already has saved privated key"}
  653. address_index = self.site.content_manager.contents.get("content.json", {}).get("address_index")
  654. if not address_index:
  655. return {"error": "No address_index in content.json"}
  656. privatekey = CryptBitcoin.hdPrivatekey(self.user.master_seed, address_index)
  657. privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey)
  658. if privatekey_address == self.site.address:
  659. site_data["privatekey"] = privatekey
  660. self.user.save()
  661. self.site.updateWebsocket(recover_privatekey=True)
  662. return "ok"
  663. else:
  664. return {"error": "Unable to deliver private key for this site from current user's master_seed"}
  665. @flag.admin
  666. @flag.no_multiuser
  667. def actionUserSetSitePrivatekey(self, to, privatekey):
  668. site_data = self.user.sites[self.site.address]
  669. site_data["privatekey"] = privatekey
  670. self.site.updateWebsocket(set_privatekey=bool(privatekey))
  671. self.user.save()
  672. return "ok"
  673. @flag.admin
  674. @flag.no_multiuser
  675. def actionSiteSetAutodownloadoptional(self, to, owned):
  676. self.site.settings["autodownloadoptional"] = bool(owned)
  677. self.site.worker_manager.removeSolvedFileTasks()
  678. @flag.no_multiuser
  679. @flag.admin
  680. def actionDbReload(self, to):
  681. self.site.storage.closeDb()
  682. self.site.storage.getDb()
  683. return self.response(to, "ok")
  684. @flag.no_multiuser
  685. @flag.admin
  686. def actionDbRebuild(self, to):
  687. try:
  688. self.site.storage.rebuildDb()
  689. except Exception as err:
  690. return self.response(to, {"error": str(err)})
  691. return self.response(to, "ok")