diff --git a/README.md b/README.md index 069ab358..bd377827 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/ * [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz) * Unpack anywhere * Run `ZeroNet.cmd` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux) +* On OSX you may need to make the application executable via `chmod +x ZeroNet.app` If you get "classic environment no longer supported" error on OS X: Open a Terminal window and drop ZeroNet.app on it @@ -187,4 +188,4 @@ Site:13DNDk..bhC2 Successfuly published to 3 peers * More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/ * Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet) -* Email: hello@noloop.me +* Email: hello@zeronet.io (PGP: CB9613AE) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index 4440e718..3fb58cfd 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -5,6 +5,7 @@ from Plugin import PluginManager from Translate import Translate from util import RateLimit from util import helper +from Debug import Debug try: import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible except Exception: @@ -19,6 +20,7 @@ if "merger_db" not in locals().keys(): # To keep merger_sites between module re if "_" not in locals(): _ = Translate("plugins/MergerSite/languages/") + # Check if the site has permission to this merger site def checkMergerPath(address, inner_path): merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path) @@ -32,7 +34,10 @@ def checkMergerPath(address, inner_path): inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path) return merged_address, inner_path else: - raise Exception("Merger site (%s) does not have permission for merged site: %s" % (merger_type, merged_address)) + raise Exception( + "Merger site (%s) does not have permission for merged site: %s (%s)" % + (merger_type, merged_address, merged_db.get(merged_address)) + ) else: raise Exception("No merger (%s) permission to load:
%s (%s not in %s)" % ( address, inner_path, merger_type, merger_db.get(address, [])) @@ -184,7 +189,8 @@ class UiWebsocketPlugin(object): def actionPermissionAdd(self, to, permission): super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission) - self.site.storage.rebuildDb() + if permission.startswith("Merger"): + self.site.storage.rebuildDb() @PluginManager.registerTo("UiRequest") @@ -269,7 +275,6 @@ class SitePlugin(object): for ws in merger_site.websockets: ws.event("siteChanged", self, {"event": ["file_done", inner_path]}) - def fileFailed(self, inner_path): super(SitePlugin, self).fileFailed(inner_path) @@ -294,7 +299,11 @@ class SiteManagerPlugin(object): return for site in self.sites.itervalues(): # Update merged sites - merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type") + try: + merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type") + except Exception, err: + self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err))) + continue if merged_type: merged_db[site.address] = merged_type @@ -303,7 +312,10 @@ class SiteManagerPlugin(object): if not permission.startswith("Merger:"): continue if merged_type: - self.log.error("Removing permission %s from %s: Merger and merged at the same time." % (permission, site.address)) + self.log.error( + "Removing permission %s from %s: Merger and merged at the same time." % + (permission, site.address) + ) site.settings["permissions"].remove(permission) continue merger_type = permission.replace("Merger:", "") diff --git a/plugins/MergerSite/languages/fr.json b/plugins/MergerSite/languages/fr.json new file mode 100644 index 00000000..9d59fde9 --- /dev/null +++ b/plugins/MergerSite/languages/fr.json @@ -0,0 +1,5 @@ +{ + "Add %s new site?": "Ajouter le site %s ?", + "Added %s new site": "Site %s ajouté", + "Site deleted: %s": "Site %s supprimé" +} diff --git a/plugins/MergerSite/languages/tr.json b/plugins/MergerSite/languages/tr.json new file mode 100644 index 00000000..5afb3942 --- /dev/null +++ b/plugins/MergerSite/languages/tr.json @@ -0,0 +1,5 @@ +{ + "Add %s new site?": "%s sitesi eklensin mi?", + "Added %s new site": "%s sitesi eklendi", + "Site deleted: %s": "%s sitesi silindi" +} diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index 84a824fb..a083f621 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -157,7 +157,10 @@ class ContentDbPlugin(object): def setContentFilesOptional(self, site, content_inner_path, content, cur=None): if not cur: cur = self - cur.execute("BEGIN") + try: + cur.execute("BEGIN") + except Exception, err: + self.log.warning("Transaction begin error %s %s: %s" % (site, content_inner_path, Debug.formatException(err))) num = 0 site_id = self.site_ids[site.address] @@ -190,8 +193,10 @@ class ContentDbPlugin(object): num += 1 if cur == self: - cur.execute("END") - + try: + cur.execute("END") + except Exception, err: + self.log.warning("Transaction end error %s %s: %s" % (site, content_inner_path, Debug.formatException(err))) return num def setContent(self, site, inner_path, content, size=0): diff --git a/plugins/OptionalManager/languages/fr.json b/plugins/OptionalManager/languages/fr.json new file mode 100644 index 00000000..47a563dc --- /dev/null +++ b/plugins/OptionalManager/languages/fr.json @@ -0,0 +1,7 @@ +{ + "Pinned %s files": "Fichiers %s épinglés", + "Removed pin from %s files": "Fichiers %s ne sont plus épinglés", + "You started to help distribute %s.
Directory: %s": "Vous avez commencé à aider à distribuer %s.
Dossier : %s", + "Help distribute all new optional files on site %s": "Aider à distribuer tous les fichiers optionnels du site %s", + "Yes, I want to help!": "Oui, je veux aider !" +} diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index c9e71f13..08ed66df 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -191,7 +191,7 @@ class UiWebsocketPlugin(object): size = max(0, size_other) elif extension == "Image": size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0) - elif extension == "total": + elif extension == "Total": size = size_total else: size = size_filetypes.get(extension, 0) @@ -411,12 +411,11 @@ class UiWebsocketPlugin(object): # Choose content you want to sign contents = ["content.json"] contents += site.content_manager.contents.get("content.json", {}).get("includes", {}).keys() - if len(contents) > 1: - body.append(_(u"
{_[Choose]}: ")) - for content in contents: - content = cgi.escape(content, True) - body.append(_("{content} ")) - body.append("
") + body.append(_(u"
{_[Choose]}: ")) + for content in contents: + content = cgi.escape(content, True) + body.append(_("{content} ")) + body.append("
") body.append(_(u"""
@@ -561,7 +560,7 @@ class UiWebsocketPlugin(object): globe_data += (lat, lon, ping) # Append myself loc = geodb.get(config.ip_external) - if loc: + if loc and loc.get("location"): lat, lon = (loc["location"]["latitude"], loc["location"]["longitude"]) globe_data += (lat, lon, -0.135) diff --git a/plugins/Sidebar/languages/es.json b/plugins/Sidebar/languages/es.json new file mode 100644 index 00000000..b9e98c46 --- /dev/null +++ b/plugins/Sidebar/languages/es.json @@ -0,0 +1,79 @@ +{ + "Peers": "Pares", + "Connected": "Conectados", + "Connectable": "Conectables", + "Connectable peers": "Pares conectables", + + "Data transfer": "Transferencia de datos", + "Received": "Recibidos", + "Received bytes": "Bytes recibidos", + "Sent": "Enviados", + "Sent bytes": "Bytes envidados", + + "Files": "Ficheros", + "Total": "Total", + "Image": "Imagen", + "Other": "Otro", + "User data": "Datos del usuario", + + "Size limit": "Límite de tamaño", + "limit used": "Límite utilizado", + "free space": "Espacio libre", + "Set": "Establecer", + + "Optional files": "Ficheros opcionales", + "Downloaded": "Descargado", + "Download and help distribute all files": "Descargar y ayudar a distribuir todos los ficheros", + "Total size": "Tamaño total", + "Downloaded files": "Ficheros descargados", + + "Database": "Base de datos", + "search feeds": "Fuentes de búsqueda", + "{feeds} query": "{feeds} consulta", + "Reload": "Recargar", + "Rebuild": "Reconstruir", + "No database found": "No se ha encontrado la base de datos", + + "Identity address": "Dirección de la identidad", + "Change": "Cambiar", + + "Update": "Actualizar", + "Pause": "Pausar", + "Resume": "Reanudar", + "Delete": "Borrar", + + "Site address": "Dirección del sitio", + "Donate": "Donar", + + "Missing files": "Ficheros perdidos", + "{} try": "{} intento", + "{} tries": "{} intentos", + "+ {num_bad_files} more": "+ {num_bad_files} más", + + "This is my site": "Este es mi sitio", + "Site title": "Título del sitio", + "Site description": "Descripción del sitio", + "Save site settings": "Guardar la configuración del sitio", + + "Content publishing": "Publicación del contenido", + "Choose": "Elegir", + "Sign": "Firmar", + "Publish": "Publicar", + "This function is disabled on this proxy": "Esta función está deshabilitada en este proxy", + "GeoLite2 City database download error: {}!
Please download manually and unpack to data dir:
{}": "¡Error de la base de datos GeoLite2: {}!
Por favor, descárgalo manualmente y descomprime al directorio de datos:
{}", + "Downloading GeoLite2 City database (one time only, ~20MB)...": "Descargando la base de datos de GeoLite2 (una única vez, ~20MB)...", + "GeoLite2 City database downloaded!": "¡Base de datos de GeoLite2 descargada!", + + "Are you sure?": "¿Estás seguro?", + "Site storage limit modified!": "¡Límite de almacenamiento del sitio modificado!", + "Database schema reloaded!": "¡Esquema de la base de datos recargado!", + "Database rebuilding....": "Reconstruyendo la base de datos...", + "Database rebuilt!": "¡Base de datos reconstruida!", + "Site updated!": "¡Sitio actualizado!", + "Delete this site": "Borrar este sitio", + "File write error: ": "Error de escritura de fichero:", + "Site settings saved!": "¡Configuración del sitio guardada!", + "Enter your private key:": "Introduce tu clave privada:", + " Signed!": " ¡firmado!", + "WebGL not supported": "WebGL no está soportado" +} diff --git a/plugins/Sidebar/languages/pl.json b/plugins/Sidebar/languages/pl.json new file mode 100644 index 00000000..93268507 --- /dev/null +++ b/plugins/Sidebar/languages/pl.json @@ -0,0 +1,82 @@ +{ + "Peers": "Użytkownicy równorzędni", + "Connected": "Połączony", + "Connectable": "Możliwy do podłączenia", + "Connectable peers": "Połączeni użytkownicy równorzędni", + + "Data transfer": "Transfer danych", + "Received": "Odebrane", + "Received bytes": "Odebrany bajty", + "Sent": "Wysłane", + "Sent bytes": "Wysłane bajty", + + "Files": "Pliki", + "Total": "Sumarycznie", + "Image": "Obraz", + "Other": "Inne", + "User data": "Dane użytkownika", + + "Size limit": "Rozmiar limitu", + "limit used": "zużyty limit", + "free space": "wolna przestrzeń", + "Set": "Ustaw", + + "Optional files": "Pliki opcjonalne", + "Downloaded": "Ściągnięte", + "Download and help distribute all files": "Ściągnij i pomóż rozpowszechniać wszystkie pliki", + "Total size": "Rozmiar sumaryczny", + "Downloaded files": "Ściągnięte pliki", + + "Database": "Baza danych", + "search feeds": "przeszukaj zasoby", + "{feeds} query": "{feeds} pytanie", + "Reload": "Odśwież", + "Rebuild": "Odbuduj", + "No database found": "Nie odnaleziono bazy danych", + + "Identity address": "Adres identyfikacyjny", + "Change": "Zmień", + + "Site control": "Kontrola strony", + "Update": "Zaktualizuj", + "Pause": "Wstrzymaj", + "Resume": "Wznów", + "Delete": "Skasuj", + "Are you sure?": "Jesteś pewien?", + + "Site address": "Adres strony", + "Donate": "Wspomóż", + + "Missing files": "Brakujące pliki", + "{} try": "{} próba", + "{} tries": "{} próby", + "+ {num_bad_files} more": "+ {num_bad_files} więcej", + + "This is my site": "To moja strona", + "Site title": "Tytuł strony", + "Site description": "Opis strony", + "Save site settings": "Zapisz ustawienia strony", + + "Content publishing": "Publikowanie treści", + "Choose": "Wybierz", + "Sign": "Podpisz", + "Publish": "Opublikuj", + + "This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy", + "GeoLite2 City database download error: {}!
Please download manually and unpack to data dir:
{}": "Błąd ściągania bazy danych GeoLite2 City: {}!
Proszę ściągnąć ją recznie i wypakować do katalogu danych:
{}", + "Downloading GeoLite2 City database (one time only, ~20MB)...": "Ściąganie bazy danych GeoLite2 City (tylko jednorazowo, ok. 20MB)...", + "GeoLite2 City database downloaded!": "Baza danych GeoLite2 City ściagnięta!", + + "Are you sure?": "Jesteś pewien?", + "Site storage limit modified!": "Limit pamięci strony zmodyfikowany!", + "Database schema reloaded!": "Schemat bazy danych załadowany ponownie!", + "Database rebuilding....": "Przebudowywanie bazy danych...", + "Database rebuilt!": "Baza danych przebudowana!", + "Site updated!": "Strona zaktualizowana!", + "Delete this site": "Usuń tę stronę", + "File write error: ": "Błąd zapisu pliku: ", + "Site settings saved!": "Ustawienia strony zapisane!", + "Enter your private key:": "Wpisz swój prywatny klucz:", + " Signed!": " Podpisane!", + "WebGL not supported": "WebGL nie jest obsługiwany" +} diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index c26c8008..44af3e3f 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -37,6 +37,8 @@ class Sidebar extends Class # Detect dragging @fixbutton.on "mousedown touchstart", (e) => + if e.button > 0 # Right or middle click + return e.preventDefault() # Disable previous listeners @@ -330,7 +332,7 @@ class Sidebar extends Class data["title"] = $("#settings-title").val() data["description"] = $("#settings-description").val() json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t'))) - wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw)], (res) => + wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) => if res != "ok" # fileWrite failed wrapper.notifications.add "file-write", "error", "File write error: #{res}" else diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 058a46c9..c995a02e 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -237,6 +237,9 @@ window.initScrollable = function () { */ this.fixbutton.on("mousedown touchstart", (function(_this) { return function(e) { + if (e.button > 0) { + return; + } e.preventDefault(); _this.fixbutton.off("click touchstop touchcancel"); _this.fixbutton.off("mousemove touchmove"); @@ -569,7 +572,7 @@ window.initScrollable = function () { data["title"] = $("#settings-title").val(); data["description"] = $("#settings-description").val(); json_raw = unescape(encodeURIComponent(JSON.stringify(data, void 0, '\t'))); - return wrapper.ws.cmd("fileWrite", ["content.json", btoa(json_raw)], function(res) { + return wrapper.ws.cmd("fileWrite", ["content.json", btoa(json_raw), true], function(res) { if (res !== "ok") { return wrapper.notifications.add("file-write", "error", "File write error: " + res); } else { diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 75a0d4c5..426d17fa 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -130,7 +130,7 @@ class UiRequestPlugin(object): # Db yield "

Db:
" for db in sys.modules["Db.Db"].opened_dbs: - yield "- %.3fs: %s
" % (time.time() - db.last_query_time, db.db_path) + yield "- %.3fs: %s
" % (time.time() - db.last_query_time, db.db_path.encode("utf8")) # Sites yield "

Sites:" @@ -220,7 +220,7 @@ class UiRequestPlugin(object): objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)] yield "
Greenlets (%s):
" % len(objs) for obj in objs: - yield " - %.1fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj))) + yield " - %.1fkb: %s
" % (self.getObjSize(obj, hpy), cgi.escape(repr(obj).encode("utf8"))) from Worker import Worker objs = [obj for obj in gc.get_objects() if isinstance(obj, Worker)] @@ -401,7 +401,10 @@ class UiRequestPlugin(object): except Exception, err: output("
! Error: %s
" % err) taken = time.time() - s - multipler = standard / taken + if taken > 0: + multipler = standard / taken + else: + multipler = 99 if multipler < 0.3: speed = "Sloooow" elif multipler < 0.5: diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index a9254447..a3da23ff 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -1,4 +1,3 @@ -import time import os import sys import atexit @@ -12,6 +11,7 @@ allow_reload = False # No source reload supported in this plugin if "_" not in locals(): _ = Translate("plugins/Trayicon/languages/") + @PluginManager.registerTo("Actions") class ActionsPlugin(object): @@ -54,23 +54,26 @@ class ActionsPlugin(object): (_["ZeroNet Github"], lambda: self.opensite("https://github.com/HelloZeroNet/ZeroNet")), (_["Report bug/request feature"], lambda: self.opensite("https://github.com/HelloZeroNet/ZeroNet/issues")), "--", - (_["!Open ZeroNet"], lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage) )), + (_["!Open ZeroNet"], lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage))), "--", (_["Quit"], self.quit), ) - - icon.clicked = lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage) ) + icon.clicked = lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage)) + self.quit_servers_event = gevent.threadpool.ThreadResult( + lambda res: gevent.spawn_later(0.1, self.quitServers) + ) # Fix gevent thread switch error gevent.threadpool.start_new_thread(icon._run, ()) # Start in real thread (not gevent compatible) super(ActionsPlugin, self).main() icon._die = True def quit(self): self.icon.die() - time.sleep(0.1) - sys.exit() - # self.main.ui_server.stop() - # self.main.file_server.stop() + self.quit_servers_event.set(True) + + def quitServers(self): + self.main.ui_server.stop() + self.main.file_server.stop() def opensite(self, url): import webbrowser @@ -115,19 +118,33 @@ class ActionsPlugin(object): def formatAutorun(self): args = sys.argv[:] - args.insert(0, sys.executable) + + if not getattr(sys, 'frozen', False): # Not frozen + args.insert(0, sys.executable) + cwd = os.getcwd().decode(sys.getfilesystemencoding()) + else: + cwd = os.path.dirname(sys.executable).decode(sys.getfilesystemencoding()) + if sys.platform == 'win32': - args = ['"%s"' % arg for arg in args] + args = ['"%s"' % arg for arg in args if arg] cmd = " ".join(args) # Dont open browser on autorun cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip() + cmd += ' --open_browser ""' + cmd = cmd.decode(sys.getfilesystemencoding()) - return "@echo off\ncd /D %s\n%s" % (os.getcwd(), cmd) + return u""" + @echo off + chcp 65001 + set PYTHONIOENCODING=utf-8 + cd /D \"%s\" + %s + """ % (cwd, cmd) def isAutorunEnabled(self): path = self.getAutorunPath() - return os.path.isfile(path) and open(path).read() == self.formatAutorun() + return os.path.isfile(path) and open(path).read().decode("utf8") == self.formatAutorun() def titleAutorun(self): translate = _["Start ZeroNet when Windows starts"] @@ -140,4 +157,4 @@ class ActionsPlugin(object): if self.isAutorunEnabled(): os.unlink(self.getAutorunPath()) else: - open(self.getAutorunPath(), "w").write(self.formatAutorun()) + open(self.getAutorunPath(), "w").write(self.formatAutorun().encode("utf8")) diff --git a/plugins/Trayicon/languages/fr.json b/plugins/Trayicon/languages/fr.json new file mode 100644 index 00000000..ec335318 --- /dev/null +++ b/plugins/Trayicon/languages/fr.json @@ -0,0 +1,14 @@ +{ + "ZeroNet Twitter": "ZeroNet Twitter", + "ZeroNet Reddit": "ZeroNet Reddit", + "ZeroNet Github": "ZeroNet Github", + "Report bug/request feature": "Rapport d'erreur/Demanger une fonctionnalité", + "!Open ZeroNet": "!Ouvrir ZeroNet", + "Quit": "Quitter", + "(active)": "(actif)", + "(passive)": "(passif)", + "Connections: %s": "Connexions: %s", + "Received: %.2f MB | Sent: %.2f MB": "Reçu: %.2f MB | Envoyé: %.2f MB", + "Show console window": "Afficher la console", + "Start ZeroNet when Windows starts": "Lancer ZeroNet au démarrage de Windows" +} diff --git a/plugins/Trayicon/languages/tr.json b/plugins/Trayicon/languages/tr.json new file mode 100644 index 00000000..077b8ddd --- /dev/null +++ b/plugins/Trayicon/languages/tr.json @@ -0,0 +1,14 @@ +{ + "ZeroNet Twitter": "ZeroNet Twitter", + "ZeroNet Reddit": "ZeroNet Reddit", + "ZeroNet Github": "ZeroNet Github", + "Report bug/request feature": "Hata bildir/geliştirme taleb et", + "!Open ZeroNet": "!ZeroNet'i Aç", + "Quit": "Kapat", + "(active)": "(aktif)", + "(passive)": "(pasif)", + "Connections: %s": "Bağlantı sayısı: %s", + "Received: %.2f MB | Sent: %.2f MB": "Gelen: %.2f MB | Gönderilen: %.2f MB", + "Show console window": "Konsolu aç", + "Start ZeroNet when Windows starts": "ZeroNet'i açılışta otomatik başlat" +} diff --git a/plugins/disabled-Bootstrapper/BootstrapperDb.py b/plugins/disabled-Bootstrapper/BootstrapperDb.py index 44cf58c9..94270363 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperDb.py +++ b/plugins/disabled-Bootstrapper/BootstrapperDb.py @@ -130,7 +130,7 @@ class BootstrapperDb(Db): where = "hash_id = :hashid" if onions: - onions_escaped = ["'%s'" % re.sub("[^a-z0-9,]", "", onion) for onion in onions] + onions_escaped = ["'%s'" % re.sub("[^a-z0-9,]", "", onion) for onion in onions if type(onion) is str] where += " AND (onion NOT IN (%s) OR onion IS NULL)" % ",".join(onions_escaped) elif ip4: where += " AND (NOT (ip4 = :ip4 AND port = :port) OR ip4 IS NULL)" diff --git a/src/Config.py b/src/Config.py index 1e7e5a5b..449cdcf0 100644 --- a/src/Config.py +++ b/src/Config.py @@ -2,6 +2,7 @@ import argparse import sys import os import locale +import re import ConfigParser @@ -9,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.1" - self.rev = 1766 + self.rev = 1848 self.argv = argv self.action = None self.config_file = "zeronet.conf" @@ -37,7 +38,7 @@ class Config(object): "udp://tracker.coppersurfer.tk:6969", "udp://tracker.leechers-paradise.org:6969", "udp://9.rarbg.com:2710", - "http://tracker.tordb.ml:6881/announce", + "http://tracker.opentrackr.org:1337/announce", "http://explodie.org:6969/announce", "http://tracker1.wasabii.com.tw:6969/announce" ] @@ -55,6 +56,35 @@ class Config(object): use_openssl = True + if repr(1483108852.565) != "1483108852.565": + fix_float_decimals = True + else: + fix_float_decimals = False + + this_file = os.path.abspath(__file__).replace("\\", "/") + + if this_file.endswith("/Contents/Resources/core/src/Config.py"): + # Running as ZeroNet.app + if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")): + # Runnig from non-writeable directory, put data to Application Support + start_dir = os.path.expanduser("~/Library/Application Support/ZeroNet").decode(sys.getfilesystemencoding()) + else: + # Running from writeable directory put data next to .app + start_dir = re.sub("/[^/]+/Contents/Resources/core/src/Config.py", "", this_file).decode(sys.getfilesystemencoding()) + config_file = start_dir + "/zeronet.conf" + data_dir = start_dir + "/data" + log_dir = start_dir + "/log" + elif this_file.endswith("/core/src/Config.py"): + # Running as exe or source is at Application Support directory, put var files to outside of core dir + start_dir = this_file.replace("/core/src/Config.py", "").decode(sys.getfilesystemencoding()) + config_file = start_dir + "/zeronet.conf" + data_dir = start_dir + "/data" + log_dir = start_dir + "/log" + else: + config_file = "zeronet.conf" + data_dir = "data" + log_dir = "log" + # Main action = self.subparsers.add_parser("main", help='Start UiServer and FileServer (default)') @@ -76,6 +106,7 @@ class Config(object): action.add_argument('privatekey', help='Private key (default: ask on execute)', nargs='?') action.add_argument('--inner_path', help='File you want to sign (default: content.json)', default="content.json", metavar="inner_path") + action.add_argument('--remove_missing_optional', help='Remove optional files that is not present in the directory', action='store_true') action.add_argument('--publish', help='Publish site after the signing', action='store_true') # SitePublish @@ -134,9 +165,9 @@ class Config(object): self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true') - self.parser.add_argument('--config_file', help='Path of config file', default="zeronet.conf", metavar="path") - self.parser.add_argument('--data_dir', help='Path of data directory', default="data", metavar="path") - self.parser.add_argument('--log_dir', help='Path of logging directory', default="log", metavar="path") + self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path") + self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path") + self.parser.add_argument('--log_dir', help='Path of logging directory', default=log_dir, metavar="path") self.parser.add_argument('--language', help='Web interface language', default=language, metavar='language') self.parser.add_argument('--ui_ip', help='Web interface bind address', default="127.0.0.1", metavar='ip') @@ -149,7 +180,7 @@ class Config(object): self.parser.add_argument('--updatesite', help='Source code update site', default='1UPDatEDxnvHDo7TXvq6AEBARfNkyfxsp', metavar='address') self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='size') - self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=10, type=int, metavar='connected_limit') + self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=6, type=int, metavar='connected_limit') self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip') self.parser.add_argument('--fileserver_port', help='FileServer bind port', default=15441, type=int, metavar='port') @@ -173,6 +204,8 @@ class Config(object): type='bool', choices=[True, False], default=False) self.parser.add_argument("--msgpack_purepython", help='Use less memory, but a bit more CPU power', type='bool', choices=[True, False], default=True) + self.parser.add_argument("--fix_float_decimals", help='Fix content.json modification date float precision on verification', + type='bool', choices=[True, False], default=fix_float_decimals) self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript, metavar='executable_path') @@ -180,6 +213,7 @@ class Config(object): self.parser.add_argument('--tor', help='enable: Use only for Tor peers, always: Use Tor for every connection', choices=["disable", "enable", "always"], default='enable') self.parser.add_argument('--tor_controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051') self.parser.add_argument('--tor_proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050') + self.parser.add_argument('--tor_password', help='Tor controller password', metavar='password') self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev)) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 5fe06a97..7f171fd6 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -78,6 +78,11 @@ class Connection(object): def badAction(self, weight=1): self.bad_actions += weight + if self.bad_actions > 40: + self.close() + elif self.bad_actions > 20: + time.sleep(5) + def goodAction(self): self.bad_actions = 0 diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 9d5d859c..c55b125f 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -55,7 +55,7 @@ class ConnectionServer: if port: # Listen server on a port self.pool = Pool(1000) # do not accept more than 1000 connections self.stream_server = StreamServer( - (ip.replace("*", ""), port), self.handleIncomingConnection, spawn=self.pool, backlog=500 + (ip.replace("*", "0.0.0.0"), port), self.handleIncomingConnection, spawn=self.pool, backlog=500 ) if request_handler: self.handleRequest = request_handler diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 13142c13..6a23439c 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -212,7 +212,7 @@ class ContentManager(object): # Update the content self.contents[content_inner_path] = new_content except Exception, err: - self.log.warning("Content.json parse error: %s" % Debug.formatException(err)) + self.log.warning("%s parse error: %s" % (content_inner_path, Debug.formatException(err))) return [], [] # Content.json parse error # Add changed files to bad files @@ -315,6 +315,7 @@ class ContentManager(object): if back: back["content_inner_path"] = content_inner_path back["optional"] = False + back["relative_path"] = "/".join(inner_path_parts) return back # Check in optional files @@ -323,6 +324,7 @@ class ContentManager(object): if back: back["content_inner_path"] = content_inner_path back["optional"] = True + back["relative_path"] = "/".join(inner_path_parts) return back # Return the rules if user dir @@ -424,7 +426,7 @@ class ContentManager(object): # Get diffs for changed files def getDiffs(self, inner_path, limit=30 * 1024, update_files=True): if inner_path not in self.contents: - return None + return {} diffs = {} content_inner_path_dir = helper.getDirname(inner_path) for file_relative_path in self.contents[inner_path].get("files", {}): @@ -491,7 +493,7 @@ class ContentManager(object): # Create and sign a content.json # Return: The new content if filewrite = False - def sign(self, inner_path="content.json", privatekey=None, filewrite=True, update_changed_files=False, extend=None): + def sign(self, inner_path="content.json", privatekey=None, filewrite=True, update_changed_files=False, extend=None, remove_missing_optional=False): if inner_path in self.contents: content = self.contents[inner_path] if self.contents[inner_path].get("cert_sign", False) is None and self.site.storage.isFile(inner_path): @@ -523,6 +525,11 @@ class ContentManager(object): helper.getDirname(inner_path), content.get("ignore"), content.get("optional") ) + if not remove_missing_optional: + for file_inner_path, file_details in content.get("files_optional", {}).iteritems(): + if file_inner_path not in files_optional_node: + files_optional_node[file_inner_path] = file_details + # Find changed files files_merged = files_node.copy() files_merged.update(files_optional_node) @@ -547,7 +554,7 @@ class ContentManager(object): elif "files_optional" in new_content: del new_content["files_optional"] - new_content["modified"] = time.time() # Add timestamp + new_content["modified"] = int(time.time()) # Add timestamp if inner_path == "content.json": new_content["zeronet_version"] = config.version new_content["signs_required"] = content.get("signs_required", 1) @@ -768,8 +775,18 @@ class ContentManager(object): del(new_content["sign"]) # The file signed without the sign if "signs" in new_content: del(new_content["signs"]) # The file signed without the signs + sign_content = json.dumps(new_content, sort_keys=True) # Dump the json to string to remove whitepsace + # Fix float representation error on Android + modified = new_content["modified"] + if config.fix_float_decimals and type(modified) is float and not str(modified).endswith(".0"): + modified_fixed = "{:.6f}".format(modified).strip("0.") + sign_content = sign_content.replace( + '"modified": %s' % repr(modified), + '"modified": %s' % modified_fixed + ) + if not self.verifyContent(inner_path, new_content): return False # Content not valid (files too large, invalid files) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 61d96acc..b30c7e49 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -70,13 +70,14 @@ class CryptConnectionManager: return True # Files already exits import subprocess + cmd = "%s req -x509 -newkey rsa:2048 -sha256 -batch -keyout %s -out %s -nodes -config %s" % helper.shellquote( + self.openssl_bin, + config.data_dir+"/key-rsa.pem", + config.data_dir+"/cert-rsa.pem", + self.openssl_env["OPENSSL_CONF"] + ) proc = subprocess.Popen( - "%s req -x509 -newkey rsa:2048 -sha256 -batch -keyout %s -out %s -nodes -config %s" % helper.shellquote( - self.openssl_bin, - config.data_dir+"/key-rsa.pem", - config.data_dir+"/cert-rsa.pem", - self.openssl_env["OPENSSL_CONF"] - ), + cmd.encode(sys.getfilesystemencoding()), shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env ) back = proc.stdout.read().strip() diff --git a/src/Db/Db.py b/src/Db/Db.py index 8609a7f4..be702e22 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -55,7 +55,7 @@ class Db(object): self.log.debug("Created Db path: %s" % self.db_dir) if not os.path.isfile(self.db_path): self.log.debug("Db file not exist yet: %s" % self.db_path) - self.conn = sqlite3.connect(self.db_path) + self.conn = sqlite3.connect(self.db_path, check_same_thread=False) self.conn.row_factory = sqlite3.Row self.conn.isolation_level = None self.cur = self.getCursor() diff --git a/src/Debug/DebugReloader.py b/src/Debug/DebugReloader.py index 02dfffcb..ce30ab3c 100644 --- a/src/Debug/DebugReloader.py +++ b/src/Debug/DebugReloader.py @@ -40,9 +40,11 @@ class DebugReloader: if ( not evt.path or "%s/" % config.data_dir in evt.path or (not evt.path.endswith("py") and not evt.path.endswith("json")) or + "Test" in evt.path or time.time() - self.last_chaged < 1 ): return False # Ignore *.pyc changes and no reload within 1 sec time.sleep(0.1) # Wait for lock release + logging.debug("Changed: %s, reloading source." % evt.path) self.callback() self.last_chaged = time.time() diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index c0c28e64..cdbf2302 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -173,7 +173,9 @@ class FileRequest(object): file.seek(params["location"]) file.read_bytes = FILE_BUFF file_size = os.fstat(file.fileno()).st_size - assert params["location"] <= file_size, "Bad file location" + if params["location"] > file_size: + self.connection.badAction(5) + raise Exception("Bad file location") back = { "body": file, @@ -212,7 +214,9 @@ class FileRequest(object): file.seek(params["location"]) file_size = os.fstat(file.fileno()).st_size stream_bytes = min(FILE_BUFF, file_size - params["location"]) - assert stream_bytes >= 0, "Stream bytes out of range" + if stream_bytes < 0: + self.connection.badAction(5) + raise Exception("Bad file location") back = { "size": file_size, diff --git a/src/Site/Site.py b/src/Site/Site.py index d2527a79..e5a9cfb9 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -398,7 +398,7 @@ class Site(object): gevent.joinall(content_threads) # Publish worker - def publisher(self, inner_path, peers, published, limit, event_done=None, diffs={}): + def publisher(self, inner_path, peers, published, limit, diffs={}, event_done=None, cb_progress=None): file_size = self.storage.getSize(inner_path) content_json_modified = self.content_manager.contents[inner_path]["modified"] body = self.storage.read(inner_path) @@ -457,6 +457,8 @@ class Site(object): if result and "ok" in result: published.append(peer) + if cb_progress and len(published) <= limit: + cb_progress(len(published), limit) self.log.info("[OK] %s: %s %s/%s" % (peer.key, result["ok"], len(published), limit)) else: if result == {"exception": "Timeout"}: @@ -466,7 +468,7 @@ class Site(object): # Update content.json on peers @util.Noparallel() - def publish(self, limit="default", inner_path="content.json", diffs={}): + def publish(self, limit="default", inner_path="content.json", diffs={}, cb_progress=None): published = [] # Successfully published (Peer) publishers = [] # Publisher threads @@ -498,7 +500,7 @@ class Site(object): event_done = gevent.event.AsyncResult() for i in range(min(len(peers), limit, threads)): - publisher = gevent.spawn(self.publisher, inner_path, peers, published, limit, event_done, diffs) + publisher = gevent.spawn(self.publisher, inner_path, peers, published, limit, diffs, event_done, cb_progress) publishers.append(publisher) event_done.get() # Wait for done @@ -522,7 +524,7 @@ class Site(object): return len(published) # Copy this site - def clone(self, address, privatekey=None, address_index=None, overwrite=False): + def clone(self, address, privatekey=None, address_index=None, root_inner_path="", overwrite=False): import shutil new_site = SiteManager.site_manager.need(address, all_file=False) default_dirs = [] # Dont copy these directories (has -default version) @@ -530,16 +532,20 @@ class Site(object): if "-default" in dir_name: default_dirs.append(dir_name.replace("-default", "")) - self.log.debug("Cloning to %s, ignore dirs: %s" % (address, default_dirs)) + self.log.debug("Cloning to %s, ignore dirs: %s, root: %s" % (address, default_dirs, root_inner_path)) # Copy root content.json if not new_site.storage.isFile("content.json") and not overwrite: # Content.json not exist yet, create a new one from source site - content_json = self.storage.loadJson("content.json") + if self.storage.isFile(root_inner_path + "/content.json-default"): + content_json = self.storage.loadJson(root_inner_path + "/content.json-default") + else: + content_json = self.storage.loadJson("content.json") if "domain" in content_json: del content_json["domain"] content_json["title"] = "my" + content_json["title"] content_json["cloned_from"] = self.address + content_json["clone_root"] = root_inner_path content_json["files"] = {} if address_index: content_json["address_index"] = address_index # Site owner's BIP32 index @@ -553,17 +559,29 @@ class Site(object): for file_relative_path in sorted(content["files"].keys()): file_inner_path = helper.getDirname(content_inner_path) + file_relative_path # Relative to content.json file_inner_path = file_inner_path.strip("/") # Strip leading / + if not file_inner_path.startswith(root_inner_path): + self.log.debug("[SKIP] %s (not in clone root)" % file_inner_path) + continue if file_inner_path.split("/")[0] in default_dirs: # Dont copy directories that has -default postfixed alternative self.log.debug("[SKIP] %s (has default alternative)" % file_inner_path) continue file_path = self.storage.getPath(file_inner_path) # Copy the file normally to keep the -default postfixed dir and file to allow cloning later - file_path_dest = new_site.storage.getPath(file_inner_path) + if root_inner_path: + file_inner_path_dest = re.sub("^%s/" % re.escape(root_inner_path), "", file_inner_path) + file_path_dest = new_site.storage.getPath(file_inner_path_dest) + else: + file_inner_path_dest = file_inner_path + file_path_dest = new_site.storage.getPath(file_inner_path) + self.log.debug("[COPY] %s to %s..." % (file_inner_path, file_path_dest)) dest_dir = os.path.dirname(file_path_dest) if not os.path.isdir(dest_dir): os.makedirs(dest_dir) + if file_inner_path_dest == "content.json-default": # Don't copy root content.json-default + continue + shutil.copy(file_path, file_path_dest) # If -default in path, create a -default less copy of the file diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 6f35d13f..8a2a6a5b 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -36,7 +36,13 @@ class SiteManager(object): for address, settings in json.load(open("%s/sites.json" % config.data_dir)).iteritems(): if address not in self.sites and os.path.isfile("%s/%s/content.json" % (config.data_dir, address)): s = time.time() - self.sites[address] = Site(address, settings=settings) + try: + site = Site(address, settings=settings) + site.content_manager.contents.get("content.json") + except Exception, err: + self.log.debug("Error loading site %s: %s" % (address, err)) + continue + self.sites[address] = site self.log.debug("Loaded site %s in %.3fs" % (address, time.time() - s)) added += 1 address_found.append(address) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index f9d7c187..1c7b6151 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -19,8 +19,8 @@ from Plugin import PluginManager class SiteStorage(object): def __init__(self, site, allow_create=True): self.site = site - self.directory = "%s/%s" % (config.data_dir, self.site.address) # Site data diretory - self.allowed_dir = os.path.abspath(self.directory.decode(sys.getfilesystemencoding())) # Only serve file within this dir + self.directory = u"%s/%s" % (config.data_dir, self.site.address) # Site data diretory + self.allowed_dir = os.path.abspath(self.directory) # Only serve file within this dir self.log = site.log self.db = None # Db class self.db_checked = False # Checked db tables since startup @@ -166,8 +166,11 @@ class SiteStorage(object): with open(file_path, "wb") as file: shutil.copyfileobj(content, file) # Write buff to disk else: # Simple string - with open(file_path, "wb") as file: - file.write(content) + if inner_path == "content.json" and os.path.isfile(file_path): + helper.atomicWrite(file_path, content) + else: + with open(file_path, "wb") as file: + file.write(content) del content self.onUpdated(inner_path) @@ -275,11 +278,10 @@ class SiteStorage(object): if not inner_path: return self.directory - file_path = u"%s/%s" % (self.directory, inner_path) + if ".." in inner_path: + raise Exception(u"File not allowed: %s" % inner_path) - if ".." in file_path: - raise Exception(u"File not allowed: %s" % file_path) - return file_path + return u"%s/%s" % (self.directory, inner_path) # Get site dir relative path def getInnerPath(self, path): @@ -415,8 +417,8 @@ class SiteStorage(object): os.unlink(path) break except Exception, err: - self.log.error("Error removing %s: %s, try #%s" % (path, err, retry)) - time.sleep(float(retry)/10) + self.log.error("Error removing %s: %s, try #%s" % (path, err, retry)) + time.sleep(float(retry) / 10) self.onUpdated(inner_path, False) self.log.debug("Deleting empty dirs...") diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index df62aec4..78065c00 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -108,10 +108,10 @@ class TestContent: assert len(site.content_manager.hashfield) == 0 site.content_manager.contents["content.json"]["optional"] = "((data/img/zero.*))" - content_optional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False) + content_optional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False, remove_missing_optional=True) del site.content_manager.contents["content.json"]["optional"] - content_nooptional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False) + content_nooptional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False, remove_missing_optional=True) assert len(content_nooptional.get("files_optional", {})) == 0 # No optional files if no pattern assert len(content_optional["files_optional"]) > 0 diff --git a/src/Test/TestNoparallel.py b/src/Test/TestNoparallel.py index abc4c767..736bf4ea 100644 --- a/src/Test/TestNoparallel.py +++ b/src/Test/TestNoparallel.py @@ -1,10 +1,7 @@ import time -import gevent -from gevent import monkey -monkey.patch_all() - import util +import gevent class ExampleClass(object): def __init__(self): diff --git a/src/Test/TestRateLimit.py b/src/Test/TestRateLimit.py index a823d88b..b0a91ba0 100644 --- a/src/Test/TestRateLimit.py +++ b/src/Test/TestRateLimit.py @@ -1,8 +1,6 @@ import time import gevent -from gevent import monkey -monkey.patch_all() from util import RateLimit diff --git a/src/Test/TestWeb.py b/src/Test/TestWeb.py index 72a34a5a..5cc6825c 100644 --- a/src/Test/TestWeb.py +++ b/src/Test/TestWeb.py @@ -18,14 +18,14 @@ class WaitForPageLoad(object): self.old_page = self.browser.find_element_by_tag_name('html') def __exit__(self, *args): - WebDriverWait(self.browser, 20).until(staleness_of(self.old_page)) + WebDriverWait(self.browser, 5).until(staleness_of(self.old_page)) @pytest.mark.usefixtures("resetSettings") @pytest.mark.webtest class TestWeb: def testFileSecurity(self, site_url): - assert "Forbidden" in urllib.urlopen("%s/media/./sites.json" % site_url).read() + assert "Not Found" in urllib.urlopen("%s/media/./sites.json" % site_url).read() assert "Forbidden" in urllib.urlopen("%s/media/../config.py" % site_url).read() assert "Forbidden" in urllib.urlopen("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../sites.json" % site_url).read() assert "Forbidden" in urllib.urlopen("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/..//sites.json" % site_url).read() @@ -34,10 +34,6 @@ class TestWeb: assert "Forbidden" in urllib.urlopen("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/..//sites.json" % site_url).read() assert "Forbidden" in urllib.urlopen("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../../zeronet.py" % site_url).read() - def testHomepage(self, browser, site_url): - browser.get("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr" % site_url) - assert browser.title == "ZeroHello - ZeroNet" - def testLinkSecurity(self, browser, site_url): browser.get("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/test/security.html" % site_url) assert browser.title == "ZeroHello - ZeroNet" diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 905aa2b0..1388bd11 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -49,7 +49,7 @@ if os.path.isfile("%s-temp/content.db" % config.data_dir): import gevent from gevent import monkey -monkey.patch_all(thread=False) +monkey.patch_all(thread=False, subprocess=False) from Site import Site from Site import SiteManager @@ -164,7 +164,9 @@ def user(): def browser(): try: from selenium import webdriver + print "Starting phantomjs..." browser = webdriver.PhantomJS(executable_path=PHANTOMJS_PATH, service_log_path=os.path.devnull) + print "Set window size..." browser.set_window_size(1400, 1000) except Exception, err: raise pytest.skip("Test requires selenium + phantomjs: %s" % err) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index b01ad794..b91cd937 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -14,7 +14,10 @@ from Config import config from Crypt import CryptRsa from Site import SiteManager from lib.PySocks import socks -from gevent.coros import RLock +try: + from gevent.coros import RLock +except: + from gevent.lock import RLock from util import helper from Debug import Debug @@ -75,7 +78,9 @@ class TorManager: self.log.info("Starting Tor client %s..." % self.tor_exe) tor_dir = os.path.dirname(self.tor_exe) - self.tor_process = subprocess.Popen(r"%s -f torrc" % self.tor_exe, cwd=tor_dir, close_fds=True) + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + self.tor_process = subprocess.Popen(r"%s -f torrc" % self.tor_exe, cwd=tor_dir, close_fds=True, startupinfo=startupinfo) for wait in range(1,10): # Wait for startup time.sleep(wait * 0.5) self.enabled = True @@ -152,22 +157,26 @@ class TorManager: try: with self.lock: conn.connect((self.ip, self.port)) - res_protocol = self.send("PROTOCOLINFO", conn) - - version = re.search('Tor="([0-9\.]+)', res_protocol).group(1) - # Version 0.2.7.5 required because ADD_ONION support - assert float(version.replace(".", "0", 2)) >= 207.5, "Tor version >=0.2.7.5 required, found: %s" % version # Auth cookie file + res_protocol = self.send("PROTOCOLINFO", conn) cookie_match = re.search('COOKIEFILE="(.*?)"', res_protocol) if cookie_match: - cookie_file = cookie_match.group(1) + cookie_file = cookie_match.group(1).decode("string-escape") auth_hex = binascii.b2a_hex(open(cookie_file, "rb").read()) res_auth = self.send("AUTHENTICATE %s" % auth_hex, conn) + elif config.tor_password: + res_auth = self.send('AUTHENTICATE "%s"' % config.tor_password, conn) else: res_auth = self.send("AUTHENTICATE", conn) assert "250 OK" in res_auth, "Authenticate error %s" % res_auth + + # Version 0.2.7.5 required because ADD_ONION support + res_version = self.send("GETINFO version", conn) + version = re.search('version=([0-9\.]+)', res_version).group(1) + assert float(version.replace(".", "0", 2)) >= 207.5, "Tor version >=0.2.7.5 required, found: %s" % version + self.status = u"Connected (%s)" % res_auth self.conn = conn except Exception, err: @@ -232,10 +241,12 @@ class TorManager: if not conn: conn = self.conn self.log.debug("> %s" % cmd) + back = "" for retry in range(2): try: - conn.send("%s\r\n" % cmd) - back = conn.recv(1024 * 64).decode("utf8", "ignore") + conn.sendall("%s\r\n" % cmd) + while not back.endswith("250 OK\r\n"): + back += conn.recv(1024 * 64).decode("utf8", "ignore") break except Exception, err: self.log.error("Tor send error: %s, reconnecting..." % err) diff --git a/src/Translate/languages/es.json b/src/Translate/languages/es.json new file mode 100644 index 00000000..659dc0e9 --- /dev/null +++ b/src/Translate/languages/es.json @@ -0,0 +1,51 @@ +{ + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "¡Felicidades! tu puerto {0} está abierto.
¡Eres un miembro completo de la red Zeronet!", + "Tor mode active, every connection using Onion route.": "Modo Tor activado, cada conexión usa una ruta Onion.", + "Successfully started Tor onion hidden services.": "Tor ha iniciado satisfactoriamente la ocultación de los servicios onion.", + "Unable to start hidden services, please check your config.": "No se puedo iniciar los servicios ocultos, por favor comprueba tu configuración.", + "For faster connections open {0} port on your router.": "Para conexiones más rápidas abre el puerto {0} en tu router.", + "Your connection is restricted. Please, open {0} port on your router": "Tu conexión está limitada. Por favor, abre el puerto {0} en tu router", + "or configure Tor to become full member of ZeroNet network.": "o configura Tor para convertirte en un miembro completo de la red ZeroNet.", + + "Select account you want to use in this site:": "Selecciona la cuenta que quieres utilizar en este sitio:", + "currently selected": "actualmente seleccionada", + "Unique to site": "Única para el sitio", + + "Content signing failed": "Firma del contenido fallida", + "Content publish queued for {0:.0f} seconds.": "Publicación de contenido en cola durante {0:.0f} segundos.", + "Content published to {0} peers.": "Contenido publicado para {0} pares.", + "No peers found, but your content is ready to access.": "No se ha encontrado pares, pero tu contenido está listo para ser accedido.", + "Your network connection is restricted. Please, open {0} port": "Tu conexión de red está restringida. Por favor, abre el puerto{0}", + "on your router to make your site accessible for everyone.": "en tu router para hacer tu sitio accesible a todo el mundo.", + "Content publish failed.": "Publicación de contenido fallida.", + "This file still in sync, if you write it now, then the previous content may be lost.": "Este archivo está aún sincronizado, si le escribes ahora el contenido previo podría perderse.", + "Write content anyway": "Escribir el contenido de todas formas", + "New certificate added:": "Nuevo certificado añadido:", + "You current certificate:": "Tu certificado actual:", + "Change it to {auth_type}/{auth_user_name}@{domain}": "Cambia esto a {auth_type}/{auth_user_name}@{domain}", + "Certificate changed to: {auth_type}/{auth_user_name}@{domain}.": "Certificado cambiado a: {auth_type}/{auth_user_name}@{domain}.", + "Site cloned": "Sitio clonado", + + "You have successfully changed the web interface's language!": "¡Has cambiado con éxito el idioma de la interfaz web!", + "Due to the browser's caching, the full transformation could take some minute.": "Debido a la caché del navegador, la transformación completa podría llevar unos minutos.", + + "Connection with UiServer Websocket was lost. Reconnecting...": "Se perdió la conexión con UiServer Websocket. Reconectando...", + "Connection with UiServer Websocket recovered.": "Conexión con UiServer Websocket recuperada.", + "UiServer Websocket error, please reload the page.": "Error de UiServer Websocket, por favor recarga la página.", + "   Connecting...": "   Conectando...", + "Site size: ": "Tamaño del sitio: ", + "MB is larger than default allowed ": "MB es más grande de lo permitido por defecto", + "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "Abre tu sitio and establece el límite de tamaño a \" + site_info.next_size_limit + \"MBs", + " files needs to be downloaded": " Los archivos necesitan ser descargados", + " downloaded": " descargados", + " download failed": " descarga fallida", + "Peers found: ": "Pares encontrados: ", + "No peers found": "No se han encontrado pares", + "Running out of size limit (": "Superando el tamaño límite (", + "Set limit to \" + site_info.next_size_limit + \"MB": "Establece ellímite a \" + site_info.next_size_limit + \"MB ändern", + "Site size limit changed to {0}MB": "Límite de tamaño del sitio cambiado a {0}MBs", + " New version of this page has just released.
Reload to see the modified content.": " Se ha publicado una nueva versión de esta página .
Recarga para ver el contenido modificado.", + "This site requests permission:": "Este sitio solicita permiso:", + "Grant": "Conceder" + +} diff --git a/src/Translate/languages/pl.json b/src/Translate/languages/pl.json new file mode 100644 index 00000000..e3087c73 --- /dev/null +++ b/src/Translate/languages/pl.json @@ -0,0 +1,51 @@ +{ + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gratulacje, twój port {0} jest otwarty.
Jesteś pełnoprawnym użytkownikiem sieci ZeroNet!", + "Tor mode active, every connection using Onion route.": "Tryb Tor aktywny, każde połączenie przy użyciu trasy Cebulowej.", + "Successfully started Tor onion hidden services.": "Pomyślnie zainicjowano ukryte usługi cebulowe Tor.", + "Unable to start hidden services, please check your config.": "Niezdolny do uruchomienia ukrytych usług, proszę sprawdź swoją konfigurację.", + "For faster connections open {0} port on your router.": "Dla szybszego połączenia otwórz {0} port w swoim routerze.", + "Your connection is restricted. Please, open {0} port on your router": "Połączenie jest ograniczone. Proszę, otwórz port {0} w swoim routerze", + "or configure Tor to become full member of ZeroNet network.": "bądź skonfiguruj Tora by stać się pełnoprawnym użytkownikiem sieci ZeroNet.", + + "Select account you want to use in this site:": "Wybierz konto którego chcesz użyć na tej stronie:", + "currently selected": "aktualnie wybrany", + "Unique to site": "Unikatowy dla strony", + + "Content signing failed": "Podpisanie treści zawiodło", + "Content publish queued for {0:.0f} seconds.": "Publikacja treści wstrzymana na {0:.0f} sekund(y).", + "Content published to {0} peers.": "Treść opublikowana do {0} uzytkowników równorzednych.", + "No peers found, but your content is ready to access.": "Nie odnaleziono użytkowników równorzędnych, ale twoja treść jest dostępna.", + "Your network connection is restricted. Please, open {0} port": "Twoje połączenie sieciowe jest ograniczone. Proszę, otwórz port {0}", + "on your router to make your site accessible for everyone.": "w swoim routerze, by twoja strona mogłabyć dostępna dla wszystkich.", + "Content publish failed.": "Publikacja treści zawiodła.", + "This file still in sync, if you write it now, then the previous content may be lost.": "Ten plik wciąż się synchronizuje, jeśli zapiszesz go teraz, poprzednia treść może zostać utracona.", + "Write content anyway": "Zapisz treść mimo wszystko", + "New certificate added:": "Nowy certyfikat dodany:", + "You current certificate:": "Twój aktualny certyfikat: ", + "Change it to {auth_type}/{auth_user_name}@{domain}": "Zmień na {auth_type}/{auth_user_name}@{domain}-ra", + "Certificate changed to: {auth_type}/{auth_user_name}@{domain}.": "Certyfikat zmieniony na {auth_type}/{auth_user_name}@{domain}-ra.", + "Site cloned": "Strona sklonowana", + + "You have successfully changed the web interface's language!": "Pomyślnie zmieniono język interfejsu stron!", + "Due to the browser's caching, the full transformation could take some minute.": "Ze względu na buforowanie przeglądarki, pełna zmiana może zająć parę minutę.", + + "Connection with UiServer Websocket was lost. Reconnecting...": "Połączenie z UiServer Websocket zostało przerwane. Ponowne łączenie...", + "Connection with UiServer Websocket recovered.": "Połączenie z UiServer Websocket przywrócone.", + "UiServer Websocket error, please reload the page.": "Błąd UiServer Websocket, prosze odświeżyć stronę.", + "   Connecting...": "   Łączenie...", + "Site size: ": "Rozmiar strony: ", + "MB is larger than default allowed ": "MB jest większy niż domyślnie dozwolony ", + "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "Otwórz stronę i ustaw limit na \" + site_info.next_size_limit + \"MBów", + " files needs to be downloaded": " pliki muszą zostać ściągnięte", + " downloaded": " ściągnięte", + " download failed": " ściąganie nie powiodło się", + "Peers found: ": "Odnaleziono użytkowników równorzednych: ", + "No peers found": "Nie odnaleziono użytkowników równorzędnych", + "Running out of size limit (": "Limit rozmiaru na wyczerpaniu (", + "Set limit to \" + site_info.next_size_limit + \"MB": "Ustaw limit na \" + site_info.next_size_limit + \"MBów", + "Site size limit changed to {0}MB": "Rozmiar limitu strony zmieniony na {0}MBów", + " New version of this page has just released.
Reload to see the modified content.": "Nowa wersja tej strony właśnie została wydana.
Odśwież by zobaczyć nową, zmodyfikowaną treść strony.", + "This site requests permission:": "Ta strona wymaga uprawnień:", + "Grant": "Przyznaj uprawnienia" + +} diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 046e55c6..af404c31 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -49,7 +49,10 @@ class UiRequest(object): path = re.sub("^http://", "/", path) # Remove begining http for chrome extension .bit access if self.env["REQUEST_METHOD"] == "OPTIONS": - content_type = self.getContentType(path) + if "/" not in path.strip("/"): + content_type = self.getContentType("index.html") + else: + content_type = self.getContentType(path) self.sendHeader(content_type=content_type) return "" @@ -64,8 +67,6 @@ class UiRequest(object): # uimedia within site dir (for chrome extension) path = re.sub(".*?/uimedia/", "/uimedia/", path) return self.actionUiMedia(path) - elif path.startswith("/media"): - return self.actionSiteMedia(path) # Websocket elif path == "/Websocket": return self.actionWebsocket() @@ -93,14 +94,21 @@ class UiRequest(object): def isProxyRequest(self): return self.env["PATH_INFO"].startswith("http://") + def isWebSocketRequest(self): + return self.env.get("HTTP_UPGRADE") == "websocket" + def isAjaxRequest(self): return self.env.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest" # Get mime by filename def getContentType(self, file_name): content_type = mimetypes.guess_type(file_name)[0] + + if file_name.endswith(".css"): # Force correct css content type + content_type = "text/css" + if not content_type: - if file_name.endswith("json"): # Correct json header + if file_name.endswith(".json"): # Correct json header content_type = "application/json" else: content_type = "application/octet-stream" @@ -140,6 +148,7 @@ class UiRequest(object): headers.append(("Keep-Alive", "max=25, timeout=30")) if content_type != "text/html": headers.append(("Access-Control-Allow-Origin", "*")) # Allow json access on non-html files + headers.append(("X-Frame-Options", "SAMEORIGIN")) # headers.append(("Content-Security-Policy", "default-src 'self' data: 'unsafe-inline' ws://127.0.0.1:* http://127.0.0.1:* wss://tracker.webtorrent.io; sandbox allow-same-origin allow-top-navigation allow-scripts")) # Only local connections if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access @@ -191,6 +200,14 @@ class UiRequest(object): if self.isAjaxRequest(): return self.error403("Ajax request not allowed to load wrapper") # No ajax allowed on wrapper + if self.isWebSocketRequest(): + return self.error403("WebSocket request not allowed to load wrapper") # No websocket + + if "text/html" not in self.env.get("HTTP_ACCEPT", ""): + return self.error403("Invalid Accept header to load wrapper") + if "prefetch" in self.env.get("HTTP_X_MOZ", "") or "prefetch" in self.env.get("HTTP_PURPOSE", ""): + return self.error403("Prefetch not allowed to load wrapper") + site = SiteManager.site_manager.get(address) if ( @@ -339,14 +356,8 @@ class UiRequest(object): if path_parts: # Looks like a valid path address = path_parts["address"] file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"]) - allowed_dir = os.path.abspath("%s/%s" % (config.data_dir, address)) # Only files within data/sitehash allowed - data_dir = os.path.abspath(config.data_dir) # No files from data/ allowed - if ( - ".." in file_path or - not os.path.dirname(os.path.abspath(file_path)).startswith(allowed_dir) or - allowed_dir == data_dir - ): # File not in allowed path - return self.error403() + if ".." in path_parts["inner_path"]: # File not in allowed path + return self.error403("Invalid file path") else: if config.debug and file_path.split("/")[-1].startswith("all."): # If debugging merge *.css to all.css and *.js to all.js @@ -359,7 +370,10 @@ class UiRequest(object): elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect return self.actionRedirect("./{0}/".format(path_parts["inner_path"].split("/")[-1])) else: # File not exists, try to download - site = SiteManager.site_manager.need(address, all_file=False) + if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading + return self.error404(path_parts["inner_path"]) + + site = SiteManager.site_manager.need(address) if path_parts["inner_path"].endswith("favicon.ico"): # Default favicon for all sites return self.actionFile("src/Ui/media/img/favicon.ico") diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 745ec512..5592596d 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -3,6 +3,7 @@ import time import cgi import socket import sys +import gevent from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIHandler @@ -121,7 +122,8 @@ class UiServer: browser = webbrowser.get() else: browser = webbrowser.get(config.open_browser) - browser.open("http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage), new=2) + url = "http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage) + gevent.spawn_later(0.3, browser.open, url, new=2) self.server = WSGIServer((self.ip.replace("*", ""), self.port), handler, handler_class=UiWSGIHandler, log=self.log) self.server.sockets = {} diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index ff147a81..7408e9a3 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -32,6 +32,12 @@ class UiWebsocket(object): self.channels = [] # Channels joined to self.sending = False # Currently sending to client self.send_queue = [] # Messages to send to client + self.admin_commands = ( + "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteClone", + "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "certSet", "configSet", + "actionPermissionAdd", "actionPermissionRemove" + ) + self.async_commands = ("fileGet", "fileList") # Start listener loop def start(self): @@ -152,6 +158,11 @@ class UiWebsocket(object): permissions.append("ADMIN") return permissions + def asyncWrapper(self, func): + def wrapper(*args, **kwargs): + gevent.spawn(func, *args, **kwargs) + return wrapper + # Handle incoming messages def handleRequest(self, data): req = json.loads(data) @@ -160,16 +171,10 @@ class UiWebsocket(object): params = req.get("params") self.permissions = self.getPermissions(req["id"]) - admin_commands = ( - "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteClone", - "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "certSet", "configSet", - "actionPermissionAdd", "actionPermissionRemove" - ) - if cmd == "response": # It's a response to a command return self.actionResponse(req["to"], req["result"]) - elif cmd in admin_commands and "ADMIN" not in self.permissions: # Admin commands - return self.response(req["id"], {"error:", "You don't have permission to run %s" % cmd}) + elif cmd in self.admin_commands and "ADMIN" not in self.permissions: # Admin commands + return self.response(req["id"], {"error": "You don't have permission to run %s" % cmd}) else: # Normal command func_name = "action" + cmd[0].upper() + cmd[1:] func = getattr(self, func_name, None) @@ -177,6 +182,10 @@ class UiWebsocket(object): self.response(req["id"], {"error": "Unknown command: %s" % cmd}) return + # Execute in parallel + if cmd in self.async_commands: + func = self.asyncWrapper(func) + # Support calling as named, unnamed parameters and raw first argument too if type(params) is dict: func(req["id"], **params) @@ -278,7 +287,7 @@ class UiWebsocket(object): self.response(to, ret) # Sign content.json - def actionSiteSign(self, to, privatekey=None, inner_path="content.json", response_ok=True, update_changed_files=False): + def actionSiteSign(self, to, privatekey=None, inner_path="content.json", response_ok=True, update_changed_files=False, remove_missing_optional=False): self.log.debug("Signing: %s" % inner_path) site = self.site extend = {} # Extended info for signing @@ -311,7 +320,7 @@ class UiWebsocket(object): # Reload content.json, ignore errors to make it up-to-date site.content_manager.loadContent(inner_path, add_bad_files=False, force=True) # Sign using private key sent by user - signed = site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files) + signed = site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional) if not signed: self.cmd("notification", ["error", _["Content signing failed"]]) self.response(to, {"error": "Site sign failed"}) @@ -346,6 +355,7 @@ class UiWebsocket(object): thread.linked = True if called_instantly: # Allowed to call instantly # At the end callback with request id and thread + self.cmd("progress", ["publish", _["Content published to {0}/{1} peers."].format(0, 5), 0]) thread.link(lambda thread: self.cbSitePublish(to, self.site, thread, notification, callback=notification)) else: self.cmd( @@ -357,15 +367,27 @@ class UiWebsocket(object): thread.link(lambda thread: self.cbSitePublish(to, self.site, thread, notification, callback=False)) def doSitePublish(self, site, inner_path): + def cbProgress(published, limit): + progress = int(float(published) / limit * 100) + self.cmd("progress", [ + "publish", + _["Content published to {0}/{1} peers."].format(published, limit), + progress + ]) diffs = site.content_manager.getDiffs(inner_path) - return site.publish(limit=5, inner_path=inner_path, diffs=diffs) + back = site.publish(limit=5, inner_path=inner_path, diffs=diffs, cb_progress=cbProgress) + if back == 0: # Failed to publish to anyone + self.cmd("progress", ["publish", _["Content publish failed."], -100]) + else: + cbProgress(back, back) + return back # Callback of site publish def cbSitePublish(self, to, site, thread, notification=True, callback=True): published = thread.value if published > 0: # Successfully published if notification: - self.cmd("notification", ["done", _["Content published to {0} peers."].format(published), 5000]) + # self.cmd("notification", ["done", _["Content published to {0} peers."].format(published), 5000]) site.updateWebsocket() # Send updated site data to local websocket clients if callback: self.response(to, "ok") @@ -388,7 +410,6 @@ class UiWebsocket(object): else: if notification: - self.cmd("notification", ["error", _["Content publish failed."]]) self.response(to, {"error": "Content publish failed."}) # Write a file to disk @@ -448,6 +469,16 @@ class UiWebsocket(object): ): return self.response(to, {"error": "Forbidden, you can only modify your own files"}) + file_info = self.site.content_manager.getFileInfo(inner_path) + if file_info.get("optional"): + self.log.debug("Deleting optional file: %s" % inner_path) + relative_path = file_info["relative_path"] + content_json = self.site.storage.loadJson(file_info["content_inner_path"]) + if relative_path in content_json.get("files_optional", {}): + del content_json["files_optional"][relative_path] + self.site.storage.writeJson(file_info["content_inner_path"], content_json) + self.site.content_manager.loadContent(file_info["content_inner_path"], add_bad_files=False, force=True) + try: self.site.storage.delete(inner_path) except Exception, err: @@ -468,6 +499,10 @@ class UiWebsocket(object): # self.log.debug("FileQuery %s %s done in %s" % (dir_inner_path, query, time.time()-s)) return self.response(to, rows) + # List files in directory + def actionFileList(self, to, inner_path): + return self.response(to, list(self.site.storage.list(inner_path))) + # Sql query def actionDbQuery(self, to, query, params=None, wait_for=None): if config.debug: @@ -487,14 +522,18 @@ class UiWebsocket(object): return self.response(to, rows) # Return file content - def actionFileGet(self, to, inner_path, required=True): + def actionFileGet(self, to, inner_path, required=True, format="text", timeout=300): try: if required or inner_path in self.site.bad_files: - self.site.needFile(inner_path, priority=6) + with gevent.Timeout(timeout): + self.site.needFile(inner_path, priority=6) body = self.site.storage.read(inner_path) except Exception, err: self.log.debug("%s fileGet error: %s" % (inner_path, err)) body = None + if body and format == "base64": + import base64 + body = base64.b64encode(body) return self.response(to, body) def actionFileRules(self, to, inner_path): @@ -514,13 +553,13 @@ class UiWebsocket(object): if res is True: self.cmd( "notification", - ["done", _("{_[New certificate added:]} {auth_type}/{auth_user_name}@{domain}.")] + ["done", _("{_[New certificate added]:} {auth_type}/{auth_user_name}@{domain}.")] ) self.response(to, "ok") elif res is False: # Display confirmation of change cert_current = self.user.certs[domain] - body = _("{_[You current certificate:]} {cert_current[auth_type]}/{cert_current[auth_user_name]}@{domain}") + body = _("{_[Your current certificate]:} {cert_current[auth_type]}/{cert_current[auth_user_name]}@{domain}") self.cmd( "confirm", [body, _("Change it to {auth_type}/{auth_user_name}@{domain}")], @@ -599,11 +638,13 @@ class UiWebsocket(object): if permission not in self.site.settings["permissions"]: self.site.settings["permissions"].append(permission) self.site.saveSettings() + self.site.updateWebsocket(permission_added=permission) self.response(to, "ok") def actionPermissionRemove(self, to, permission): self.site.settings["permissions"].remove(permission) self.site.saveSettings() + self.site.updateWebsocket(permission_removed=permission) self.response(to, "ok") # Set certificate that used for authenticate user for site @@ -681,12 +722,12 @@ class UiWebsocket(object): else: self.response(to, {"error": "Unknown site: %s" % address}) - def actionSiteClone(self, to, address): + def actionSiteClone(self, to, address, root_inner_path=""): self.cmd("notification", ["info", "Cloning site..."]) site = self.server.sites.get(address) # Generate a new site from user's bip32 seed new_address, new_address_index, new_site_data = self.user.getNewSiteData() - new_site = site.clone(new_address, new_site_data["privatekey"], address_index=new_address_index) + new_site = site.clone(new_address, new_site_data["privatekey"], address_index=new_address_index, root_inner_path=root_inner_path) new_site.settings["own"] = True new_site.saveSettings() self.cmd("notification", ["done", _["Site cloned"] + "" % new_address]) diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index 1a7f94fa..00c66761 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -21,12 +21,16 @@ class Notifications # Create element elem = $(".notification.template", @elem).clone().removeClass("template") elem.addClass("notification-#{type}").addClass("notification-#{id}") + if type == "progress" + elem.addClass("notification-done") # Update text if type == "error" $(".notification-icon", elem).html("!") else if type == "done" $(".notification-icon", elem).html("
") + else if type == "progress" + $(".notification-icon", elem).html("
") else if type == "ask" $(".notification-icon", elem).html("?") else @@ -64,6 +68,8 @@ class Notifications $(".select", elem).on "click", => @close elem + return elem + close: (elem) -> elem.stop().animate {"width": 0, "opacity": 0}, 700, "easeInOutCubic" diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index b076d077..927c8f0f 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -53,6 +53,8 @@ class Wrapper if "-" in message.params[0] # - in first param: message id defined [id, type] = message.params[0].split("-") @notifications.add(id, type, message.params[1], message.params[2]) + else if cmd == "progress" # Display notification + @actionProgress(message) else if cmd == "prompt" # Prompt input @displayPrompt message.params[0], message.params[1], message.params[2], (res) => @ws.response message.id, res @@ -109,6 +111,8 @@ class Wrapper @actionConfirm(message) else if cmd == "wrapperPrompt" # Prompt input @actionPrompt(message) + else if cmd == "wrapperProgress" # Progress bar + @actionProgress(message) else if cmd == "wrapperSetViewport" # Set the viewport @actionSetViewport(message) else if cmd == "wrapperSetTitle" @@ -131,6 +135,8 @@ class Wrapper @actionOpenWindow(message.params) else if cmd == "wrapperPermissionAdd" @actionPermissionAdd(message) + else if cmd == "wrapperRequestFullscreen" + @actionRequestFullscreen() else # Send to websocket if message.id < 1000000 @ws.send(message) # Pass message to websocket @@ -141,7 +147,7 @@ class Wrapper if query == null query = window.location.search back = window.location.pathname - if back.slice(-1) != "/" + if back.match /^\/[^\/]+$/ # Add / after site address if called without it back += "/" if query.replace("?", "") back += "?"+query.replace("?", "") @@ -168,6 +174,21 @@ class Wrapper w.opener = null w.location = params[0] + actionRequestFullscreen: -> + if "Fullscreen" in @site_info.settings.permissions + elem = document.getElementById("inner-iframe") + request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen + request_fullscreen.call(elem) + setTimeout ( => + if window.innerHeight != screen.height # Fullscreen failed, probably only allowed on click + @displayConfirm "This site requests permission:" + " Fullscreen", "Grant", => + request_fullscreen.call(elem) + ), 100 + else + @displayConfirm "This site requests permission:" + " Fullscreen", "Grant", => + @site_info.settings.permissions.push("Fullscreen") + @actionRequestFullscreen() + @ws.cmd "permissionAdd", "Fullscreen" actionPermissionAdd: (message) -> permission = message.params @@ -180,8 +201,6 @@ class Wrapper body = $(""+message.params[1]+"") @notifications.add("notification-#{message.id}", message.params[0], body, message.params[2]) - - displayConfirm: (message, caption, cb) -> body = $(""+message+"") button = $("#{caption}") # Add confirm button @@ -232,6 +251,49 @@ class Wrapper @displayPrompt message.params[0], type, caption, (res) => @sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm + actionProgress: (message) -> + message.params = @toHtmlSafe(message.params) # Escape html + percent = Math.min(100, message.params[2])/100 + offset = 75-(percent*75) + circle = """ +
+ + +
+ """ + body = ""+message.params[1]+"" + circle + elem = $(".notification-#{message.params[0]}") + if elem.length + width = $(".body .message", elem).outerWidth() + $(".body .message", elem).html(message.params[1]) + if $(".body .message", elem).css("width") == "" + $(".body .message", elem).css("width", width) + $(".body .circle-fg", elem).css("stroke-dashoffset", offset) + else + elem = @notifications.add(message.params[0], "progress", $(body)) + if percent > 0 + $(".body .circle-bg", elem).css {"animation-play-state": "paused", "stroke-dasharray": "180px"} + + if $(".notification-icon", elem).data("done") + return false + else if message.params[2] >= 100 # Done + $(".circle-fg", elem).css("transition", "all 0.3s ease-in-out") + setTimeout (-> + $(".notification-icon", elem).css {transform: "scale(1)", opacity: 1} + $(".notification-icon .icon-success", elem).css {transform: "rotate(45deg) scale(1)"} + ), 300 + setTimeout (=> + @notifications.close elem + ), 3000 + $(".notification-icon", elem).data("done", true) + else if message.params[2] < 0 # Error + $(".body .circle-fg", elem).css("stroke", "#ec6f47").css("transition", "transition: all 0.3s ease-in-out") + setTimeout (=> + $(".notification-icon", elem).css {transform: "scale(1)", opacity: 1} + elem.removeClass("notification-done").addClass("notification-error") + $(".notification-icon .icon-success", elem).removeClass("icon-success").html("!") + ), 300 + $(".notification-icon", elem).data("done", true) actionSetViewport: (message) -> @@ -362,7 +424,7 @@ class Wrapper if site_info.content window.document.title = site_info.content.title+" - ZeroNet" @log "Required file done, setting title to", window.document.title - if not $(".loadingscreen").length # Loading screen already removed (loaded +2sec) + if not window.show_loadingscreen @notifications.add("modified", "info", "New version of this page has just released.
Reload to see the modified content.") # File failed downloading else if site_info.event[0] == "file_failed" diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index 13681a26..4b84d4b2 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -45,7 +45,7 @@ a { color: black } color: #4F4F4F; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ } .notification-icon { - display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 1; + display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 2; text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white; } .notification .body { @@ -82,6 +82,18 @@ a { color: black } .notification .input { padding: 6px; border: 1px solid #DDD; margin-left: 10px; border-bottom: 2px solid #DDD; border-radius: 1px; margin-right: -11px; transition: all 0.3s } .notification .input:focus { border-color: #95a5a6; outline: none } +/* Notification progress */ +.notification .circle { width: 50px; height: 50px; position: absolute; left: -50px; top: 0px; background-color: #e2e9ec; z-index: 1; background: linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef); } +.notification .circle-svg { margin-left: 10px; margin-top: 10px; transform: rotateZ(-90deg); } +.notification .circle-bg { stroke: #FFF; stroke-width: 2px; animation: rolling 0.4s infinite linear; stroke-dasharray: 40px; transition: all 1s } +.notification .circle-fg { stroke-dashoffset: 200; stroke: #2ecc71; stroke-width: 2px; stroke-dasharray: 75px; transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); } +.notification-progress .notification-icon { opacity: 0; transform: scale(0); transition: all 0.3s ease-in-out } +.notification-progress .icon-success { transform: rotate(45deg) scale(0); transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); } +@keyframes rolling { + 0% { stroke-dashoffset: 80px } + 100% { stroke-dashoffset: 0px } +} + /* Icons (based on http://nicolasgallagher.com/pure-css-gui-icons/demo/) */ .icon-success { left:6px; width:5px; height:12px; border-width:0 5px 5px 0; border-style:solid; border-color:white; margin-left: 20px; margin-top: 15px; transform:rotate(45deg) } diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index 0321f9a4..e960a0b9 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -50,7 +50,7 @@ a { color: black } color: #4F4F4F; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ } .notification-icon { - display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 1; + display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 2; text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white; } .notification .body { @@ -87,6 +87,27 @@ a { color: black } .notification .input { padding: 6px; border: 1px solid #DDD; margin-left: 10px; border-bottom: 2px solid #DDD; -webkit-border-radius: 1px; -moz-border-radius: 1px; -o-border-radius: 1px; -ms-border-radius: 1px; border-radius: 1px ; margin-right: -11px; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s } .notification .input:focus { border-color: #95a5a6; outline: none } +/* Notification progress */ +.notification .circle { width: 50px; height: 50px; position: absolute; left: -50px; top: 0px; background-color: #e2e9ec; z-index: 1; background: -webkit-linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef);background: -moz-linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef);background: -o-linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef);background: -ms-linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef);background: linear-gradient(405deg, rgba(226, 233, 236, 0.8), #efefef); } +.notification .circle-svg { margin-left: 10px; margin-top: 10px; -webkit-transform: rotateZ(-90deg); -moz-transform: rotateZ(-90deg); -o-transform: rotateZ(-90deg); -ms-transform: rotateZ(-90deg); transform: rotateZ(-90deg) ; } +.notification .circle-bg { stroke: #FFF; stroke-width: 2px; -webkit-animation: rolling 0.4s infinite linear; -moz-animation: rolling 0.4s infinite linear; -o-animation: rolling 0.4s infinite linear; -ms-animation: rolling 0.4s infinite linear; animation: rolling 0.4s infinite linear ; stroke-dasharray: 40px; -webkit-transition: all 1s ; -moz-transition: all 1s ; -o-transition: all 1s ; -ms-transition: all 1s ; transition: all 1s } +.notification .circle-fg { stroke-dashoffset: 200; stroke: #2ecc71; stroke-width: 2px; stroke-dasharray: 75px; -webkit-transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); -moz-transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); -o-transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); -ms-transition: all 5s cubic-bezier(0.19, 1, 0.22, 1); transition: all 5s cubic-bezier(0.19, 1, 0.22, 1) ; } +.notification-progress .notification-icon { opacity: 0; -webkit-transform: scale(0); -moz-transform: scale(0); -o-transform: scale(0); -ms-transform: scale(0); transform: scale(0) ; -webkit-transition: all 0.3s ease-in-out ; -moz-transition: all 0.3s ease-in-out ; -o-transition: all 0.3s ease-in-out ; -ms-transition: all 0.3s ease-in-out ; transition: all 0.3s ease-in-out } +.notification-progress .icon-success { -webkit-transform: rotate(45deg) scale(0); -moz-transform: rotate(45deg) scale(0); -o-transform: rotate(45deg) scale(0); -ms-transform: rotate(45deg) scale(0); transform: rotate(45deg) scale(0) ; -webkit-transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); -moz-transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); -o-transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); -ms-transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) ; } +@keyframes rolling { + 0% { stroke-dashoffset: 80px } + 100% { stroke-dashoffset: 0px } +} +@-webkit-keyframes rolling { + 0% { stroke-dashoffset: 80px } + 100% { stroke-dashoffset: 0px } +} +@-moz-keyframes rolling { + 0% { stroke-dashoffset: 80px } + 100% { stroke-dashoffset: 0px } +} + + /* Icons (based on http://nicolasgallagher.com/pure-css-gui-icons/demo/) */ .icon-success { left:6px; width:5px; height:12px; border-width:0 5px 5px 0; border-style:solid; border-color:white; margin-left: 20px; margin-top: 15px; transform:rotate(45deg) } diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 38149779..f597fe02 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -688,10 +688,15 @@ jQuery.extend( jQuery.easing, } elem = $(".notification.template", this.elem).clone().removeClass("template"); elem.addClass("notification-" + type).addClass("notification-" + id); + if (type === "progress") { + elem.addClass("notification-done"); + } if (type === "error") { $(".notification-icon", elem).html("!"); } else if (type === "done") { $(".notification-icon", elem).html("
"); + } else if (type === "progress") { + $(".notification-icon", elem).html("
"); } else if (type === "ask") { $(".notification-icon", elem).html("?"); } else { @@ -735,11 +740,12 @@ jQuery.extend( jQuery.easing, return false; }; })(this)); - return $(".select", elem).on("click", (function(_this) { + $(".select", elem).on("click", (function(_this) { return function() { return _this.close(elem); }; })(this)); + return elem; }; Notifications.prototype.close = function(elem) { @@ -848,6 +854,8 @@ jQuery.extend( jQuery.easing, _ref = message.params[0].split("-"), id = _ref[0], type = _ref[1]; } return this.notifications.add(id, type, message.params[1], message.params[2]); + } else if (cmd === "progress") { + return this.actionProgress(message); } else if (cmd === "prompt") { return this.displayPrompt(message.params[0], message.params[1], message.params[2], (function(_this) { return function(res) { @@ -915,6 +923,8 @@ jQuery.extend( jQuery.easing, return this.actionConfirm(message); } else if (cmd === "wrapperPrompt") { return this.actionPrompt(message); + } else if (cmd === "wrapperProgress") { + return this.actionProgress(message); } else if (cmd === "wrapperSetViewport") { return this.actionSetViewport(message); } else if (cmd === "wrapperSetTitle") { @@ -941,6 +951,8 @@ jQuery.extend( jQuery.easing, return this.actionOpenWindow(message.params); } else if (cmd === "wrapperPermissionAdd") { return this.actionPermissionAdd(message); + } else if (cmd === "wrapperRequestFullscreen") { + return this.actionRequestFullscreen(); } else { if (message.id < 1000000) { return this.ws.send(message); @@ -959,7 +971,7 @@ jQuery.extend( jQuery.easing, query = window.location.search; } back = window.location.pathname; - if (back.slice(-1) !== "/") { + if (back.match(/^\/[^\/]+$/)) { back += "/"; } if (query.replace("?", "")) { @@ -992,6 +1004,32 @@ jQuery.extend( jQuery.easing, } }; + Wrapper.prototype.actionRequestFullscreen = function() { + var elem, request_fullscreen; + if (__indexOf.call(this.site_info.settings.permissions, "Fullscreen") >= 0) { + elem = document.getElementById("inner-iframe"); + request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen; + request_fullscreen.call(elem); + return setTimeout(((function(_this) { + return function() { + if (window.innerHeight !== screen.height) { + return _this.displayConfirm("This site requests permission:" + " Fullscreen", "Grant", function() { + return request_fullscreen.call(elem); + }); + } + }; + })(this)), 100); + } else { + return this.displayConfirm("This site requests permission:" + " Fullscreen", "Grant", (function(_this) { + return function() { + _this.site_info.settings.permissions.push("Fullscreen"); + _this.actionRequestFullscreen(); + return _this.ws.cmd("permissionAdd", "Fullscreen"); + }; + })(this)); + } + }; + Wrapper.prototype.actionPermissionAdd = function(message) { var permission; permission = message.params; @@ -1099,6 +1137,65 @@ jQuery.extend( jQuery.easing, })(this)); }; + Wrapper.prototype.actionProgress = function(message) { + var body, circle, elem, offset, percent, width; + message.params = this.toHtmlSafe(message.params); + percent = Math.min(100, message.params[2]) / 100; + offset = 75 - (percent * 75); + circle = "
\n \n \n
"; + body = "" + message.params[1] + "" + circle; + elem = $(".notification-" + message.params[0]); + if (elem.length) { + width = $(".body .message", elem).outerWidth(); + $(".body .message", elem).html(message.params[1]); + if ($(".body .message", elem).css("width") === "") { + $(".body .message", elem).css("width", width); + } + $(".body .circle-fg", elem).css("stroke-dashoffset", offset); + } else { + elem = this.notifications.add(message.params[0], "progress", $(body)); + } + if (percent > 0) { + $(".body .circle-bg", elem).css({ + "animation-play-state": "paused", + "stroke-dasharray": "180px" + }); + } + if ($(".notification-icon", elem).data("done")) { + return false; + } else if (message.params[2] >= 100) { + $(".circle-fg", elem).css("transition", "all 0.3s ease-in-out"); + setTimeout((function() { + $(".notification-icon", elem).css({ + transform: "scale(1)", + opacity: 1 + }); + return $(".notification-icon .icon-success", elem).css({ + transform: "rotate(45deg) scale(1)" + }); + }), 300); + setTimeout(((function(_this) { + return function() { + return _this.notifications.close(elem); + }; + })(this)), 3000); + return $(".notification-icon", elem).data("done", true); + } else if (message.params[2] < 0) { + $(".body .circle-fg", elem).css("stroke", "#ec6f47").css("transition", "transition: all 0.3s ease-in-out"); + setTimeout(((function(_this) { + return function() { + $(".notification-icon", elem).css({ + transform: "scale(1)", + opacity: 1 + }); + elem.removeClass("notification-done").addClass("notification-error"); + return $(".notification-icon .icon-success", elem).removeClass("icon-success").html("!"); + }; + })(this)), 300); + return $(".notification-icon", elem).data("done", true); + } + }; + Wrapper.prototype.actionSetViewport = function(message) { this.log("actionSetViewport", message); if ($("#viewport").length > 0) { @@ -1273,7 +1370,7 @@ jQuery.extend( jQuery.easing, window.document.title = site_info.content.title + " - ZeroNet"; this.log("Required file done, setting title to", window.document.title); } - if (!$(".loadingscreen").length) { + if (!window.show_loadingscreen) { this.notifications.add("modified", "info", "New version of this page has just released.
Reload to see the modified content."); } } diff --git a/src/lib/opensslVerify/HashInfo.txt b/src/lib/opensslVerify/HashInfo.txt index f5308e27..1fe6e619 100644 Binary files a/src/lib/opensslVerify/HashInfo.txt and b/src/lib/opensslVerify/HashInfo.txt differ diff --git a/src/lib/opensslVerify/OpenSSL License.txt b/src/lib/opensslVerify/OpenSSL License.txt index 97234459..3090896c 100644 --- a/src/lib/opensslVerify/OpenSSL License.txt +++ b/src/lib/opensslVerify/OpenSSL License.txt @@ -12,7 +12,7 @@ --------------- /* ==================================================================== - * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. + * Copyright (c) 1998-2016 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions diff --git a/src/lib/opensslVerify/ReadMe.txt b/src/lib/opensslVerify/ReadMe.txt index 352ccef6..beaf580e 100644 --- a/src/lib/opensslVerify/ReadMe.txt +++ b/src/lib/opensslVerify/ReadMe.txt @@ -1,10 +1,10 @@ ============================================================================= -OpenSSL v1.0.2a Precompiled Binaries for Win32 +OpenSSL v1.0.2j Precompiled Binaries for Win32 ----------------------------------------------------------------------------- *** Release Information *** -Release Date: Mrz 20, 2015 +Release Date: Okt 01, 2016 Author: Frederik A. Winkelsdorf (opendec.wordpress.com) for the Indy Project (www.indyproject.org) @@ -15,7 +15,7 @@ Dependencies: The libraries have no noteworthy dependencies Installation: Copy both DLL files into your application directory -Supported OS: Windows 2000 up to Windows 8 +Supported OS: Windows 2000 up to Windows 10 ----------------------------------------------------------------------------- @@ -42,10 +42,10 @@ SOFTWARE AND/OR PATENTS. *** Build Information Win32 *** Built with: Microsoft Visual C++ 2008 Express Edition - The Netwide Assembler (NASM) v2.11.05 Win32 - Strawberry Perl v5.20.0.1 Win32 Portable + The Netwide Assembler (NASM) v2.11.08 Win32 + Strawberry Perl v5.22.0.1 Win32 Portable Windows PowerShell - FinalBuilder 7 Embarcadero Edition + FinalBuilder 7 Commands: perl configure VC-WIN32 ms\do_nasm diff --git a/src/lib/opensslVerify/libeay32.dll b/src/lib/opensslVerify/libeay32.dll index 6359cc5a..dd7b6379 100644 Binary files a/src/lib/opensslVerify/libeay32.dll and b/src/lib/opensslVerify/libeay32.dll differ diff --git a/src/lib/opensslVerify/openssl.exe b/src/lib/opensslVerify/openssl.exe index 1f5127e5..0bcd58b0 100644 Binary files a/src/lib/opensslVerify/openssl.exe and b/src/lib/opensslVerify/openssl.exe differ diff --git a/src/lib/opensslVerify/opensslVerify.py b/src/lib/opensslVerify/opensslVerify.py index 8103bea0..8c9bffe7 100644 --- a/src/lib/opensslVerify/opensslVerify.py +++ b/src/lib/opensslVerify/opensslVerify.py @@ -196,11 +196,13 @@ def openLibrary(): global ssl try: if sys.platform.startswith("win"): - dll_path = "src/lib/opensslVerify/libeay32.dll" + dll_path = os.path.dirname(os.path.abspath(__file__)) + "/" + "libeay32.dll" elif sys.platform == "cygwin": dll_path = "/bin/cygcrypto-1.0.0.dll" - elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle + elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX dll_path = "../lib/libcrypto.so" + elif os.path.isfile("/opt/lib/libcrypto.so.1.0.0"): # For optware and entware + dll_path = "/opt/lib/libcrypto.so.1.0.0" else: dll_path = "/usr/local/ssl/lib/libcrypto.so" ssl = _OpenSSL(dll_path) @@ -456,7 +458,7 @@ if __name__ == "__main__": sign = btctools.ecdsa_sign("hello", priv) # HGbib2kv9gm9IJjDt1FXbXFczZi35u0rZR3iPUIt5GglDDCeIQ7v8eYXVNIaLoJRI4URGZrhwmsYQ9aVtRTnTfQ= s = time.time() - for i in range(100): + for i in range(1000): pubkey = getMessagePubkey("hello", sign) verified = btctools.pubkey_to_address(pubkey) == address print "100x Verified", verified, time.time() - s diff --git a/src/lib/opensslVerify/ssleay32.dll b/src/lib/opensslVerify/ssleay32.dll index b8b86115..8f57f6b3 100644 Binary files a/src/lib/opensslVerify/ssleay32.dll and b/src/lib/opensslVerify/ssleay32.dll differ diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py index 12953788..694451ab 100644 --- a/src/lib/pyelliptic/openssl.py +++ b/src/lib/pyelliptic/openssl.py @@ -434,10 +434,10 @@ def openLibrary(): global OpenSSL try: if sys.platform.startswith("win"): - dll_path = "src/lib/opensslVerify/libeay32.dll" + dll_path = os.path.normpath(os.path.dirname(__file__) + "/../opensslVerify/" + "libeay32.dll") elif sys.platform == "cygwin": dll_path = "/bin/cygcrypto-1.0.0.dll" - elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle + elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX dll_path = "../lib/libcrypto.so" else: dll_path = "/usr/local/ssl/lib/libcrypto.so" diff --git a/src/main.py b/src/main.py index 220599e5..5f894271 100644 --- a/src/main.py +++ b/src/main.py @@ -30,14 +30,22 @@ if not config.arguments: # Config parse failed, show the help screen and exit # Create necessary files and dirs if not os.path.isdir(config.log_dir): os.mkdir(config.log_dir) + try: + os.chmod(config.log_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + except Exception, err: + print "Can't change permission of %s: %s" % (config.log_dir, err) + if not os.path.isdir(config.data_dir): os.mkdir(config.data_dir) + try: + os.chmod(config.data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + except Exception, err: + print "Can't change permission of %s: %s" % (config.data_dir, err) + if not os.path.isfile("%s/sites.json" % config.data_dir): open("%s/sites.json" % config.data_dir, "w").write("{}") - os.chmod("%s/sites.json" % config.data_dir, stat.S_IRUSR | stat.S_IWUSR) if not os.path.isfile("%s/users.json" % config.data_dir): open("%s/users.json" % config.data_dir, "w").write("{}") - os.chmod("%s/users.json" % config.data_dir, stat.S_IRUSR | stat.S_IWUSR) # Setup logging if config.action == "main": @@ -180,7 +188,7 @@ class Actions(object): logging.info("Site created!") - def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False): + def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False, remove_missing_optional=False): from Site import Site from Site import SiteManager SiteManager.site_manager.load() @@ -200,7 +208,7 @@ class Actions(object): import getpass privatekey = getpass.getpass("Private key (input hidden):") diffs = site.content_manager.getDiffs(inner_path) - succ = site.content_manager.sign(inner_path=inner_path, privatekey=privatekey, update_changed_files=True) + succ = site.content_manager.sign(inner_path=inner_path, privatekey=privatekey, update_changed_files=True, remove_missing_optional=remove_missing_optional) if succ and publish: self.sitePublish(address, inner_path=inner_path, diffs=diffs) diff --git a/src/util/Platform.py b/src/util/Platform.py index f7b07e51..19477649 100644 --- a/src/util/Platform.py +++ b/src/util/Platform.py @@ -15,8 +15,8 @@ def setMaxfilesopened(limit): import resource soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) if soft < limit: - logging.debug("Current RLIMIT_NOFILE: %s, changing to %s..." % (soft, limit)) - resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard)) + logging.debug("Current RLIMIT_NOFILE: %s (max: %s), changing to %s..." % (soft, hard, limit)) + resource.setrlimit(resource.RLIMIT_NOFILE, (limit, hard)) return True except Exception, err: diff --git a/src/util/UpnpPunch.py b/src/util/UpnpPunch.py index 1e47d6a3..b2a163ae 100644 --- a/src/util/UpnpPunch.py +++ b/src/util/UpnpPunch.py @@ -4,6 +4,7 @@ import httplib import logging from urlparse import urlparse from xml.dom.minidom import parseString +from xml.parsers.expat import ExpatError from gevent import socket @@ -82,7 +83,7 @@ def _retrieve_igd_profile(url): Retrieve the device's UPnP profile. """ try: - return urllib2.urlopen(url.geturl(), timeout=5).read() + return urllib2.urlopen(url.geturl(), timeout=5).read().decode('utf-8') except socket.error: raise IGDError('IGD profile query timed out') @@ -100,7 +101,11 @@ def _parse_igd_profile(profile_xml): WANIPConnection or WANPPPConnection and return the 'controlURL' and the service xml schema. """ - dom = parseString(profile_xml) + try: + dom = parseString(profile_xml) + except ExpatError as e: + raise IGDError( + 'Unable to parse IGD reply: {0} \n\n\n {1}'.format(profile_xml, e)) service_types = dom.getElementsByTagName('serviceType') for service in service_types: @@ -268,7 +273,7 @@ def _send_requests(messages, location, upnp_schema, control_path): raise UpnpError('Sending requests using UPnP failed.') -def _orchestrate_soap_request(ip, port, msg_fn, desc=None, protos=['TCP', 'UDP']): +def _orchestrate_soap_request(ip, port, msg_fn, desc=None, protos=("TCP", "UDP")): logging.debug("Trying using local ip: %s" % ip) idg_data = _collect_idg_data(ip) @@ -284,7 +289,7 @@ def _communicate_with_igd(port=15441, desc="UpnpPunch", retries=3, fn=_create_open_message, - protos=["TCP", "UDP"]): + protos=("TCP", "UDP")): """ Manage sending a message generated by 'fn'. """ @@ -310,7 +315,7 @@ def _communicate_with_igd(port=15441, port, retries)) -def ask_to_open_port(port=15441, desc="UpnpPunch", retries=3, protos=["TCP", "UDP"]): +def ask_to_open_port(port=15441, desc="UpnpPunch", retries=3, protos=("TCP", "UDP")): logging.debug("Trying to open port %d." % port) _communicate_with_igd(port=port, desc=desc, @@ -319,7 +324,7 @@ def ask_to_open_port(port=15441, desc="UpnpPunch", retries=3, protos=["TCP", "UD protos=protos) -def ask_to_close_port(port=15441, desc="UpnpPunch", retries=3, protos=["TCP", "UDP"]): +def ask_to_close_port(port=15441, desc="UpnpPunch", retries=3, protos=("TCP", "UDP")): logging.debug("Trying to close port %d." % port) # retries=1 because multiple successes cause 500 response and failure _communicate_with_igd(port=port, diff --git a/src/util/helper.py b/src/util/helper.py index 13439cde..597a8d75 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -14,17 +14,15 @@ from Config import config def atomicWrite(dest, content, mode="w"): try: - permissions = stat.S_IMODE(os.lstat(dest).st_mode) - with open(dest + "-new", mode) as f: + with open(dest + "-tmpnew", mode) as f: f.write(content) f.flush() os.fsync(f.fileno()) - if os.path.isfile(dest + "-old"): # Previous incomplete write - os.rename(dest + "-old", dest + "-old-%s" % time.time()) - os.rename(dest, dest + "-old") - os.rename(dest + "-new", dest) - os.chmod(dest, permissions) - os.unlink(dest + "-old") + if os.path.isfile(dest + "-tmpold"): # Previous incomplete write + os.rename(dest + "-tmpold", dest + "-tmpold-%s" % time.time()) + os.rename(dest, dest + "-tmpold") + os.rename(dest + "-tmpnew", dest) + os.unlink(dest + "-tmpold") return True except Exception, err: from Debug import Debug @@ -32,8 +30,8 @@ def atomicWrite(dest, content, mode="w"): "File %s write failed: %s, reverting..." % (dest, Debug.formatException(err)) ) - if os.path.isfile(dest + "-old") and not os.path.isfile(dest): - os.rename(dest + "-old", dest) + if os.path.isfile(dest + "-tmpold") and not os.path.isfile(dest): + os.rename(dest + "-tmpold", dest) return False @@ -54,7 +52,7 @@ def openLocked(path, mode="w"): def getFreeSpace(): free_space = -1 if "statvfs" in dir(os): # Unix - statvfs = os.statvfs(config.data_dir) + statvfs = os.statvfs(config.data_dir.encode("utf8")) free_space = statvfs.f_frsize * statvfs.f_bavail else: # Windows try: diff --git a/update.py b/update.py index 1ca15a1a..bfdeb50c 100644 --- a/update.py +++ b/update.py @@ -1,6 +1,7 @@ import urllib import zipfile import os +import sys import ssl import httplib import socket @@ -52,6 +53,11 @@ def download(): def update(): from Config import config + if getattr(sys, 'source_update_dir', False): + if not os.path.isdir(sys.source_update_dir): + os.makedirs(sys.source_update_dir) + os.chdir(sys.source_update_dir) # New source code will be stored in different directory + updatesite_path = config.data_dir + "/" + config.updatesite sites_json = json.load(open(config.data_dir + "/sites.json")) updatesite_bad_files = sites_json.get(config.updatesite, {}).get("cache", {}).get("bad_files", {}) @@ -79,7 +85,7 @@ def update(): plugins_enabled.append(dir) print "Plugins enabled:", plugins_enabled, "disabled:", plugins_disabled - print "Extracting...", + print "Extracting to %s..." % os.getcwd(), for inner_path in inner_paths: if ".." in inner_path: continue diff --git a/zeronet.py b/zeronet.py index f56161ee..8fd07968 100755 --- a/zeronet.py +++ b/zeronet.py @@ -45,11 +45,16 @@ def main(): handler.close() logger.removeHandler(handler) - - except (Exception, ): # Prevent closing + except Exception, err: # Prevent closing import traceback - traceback.print_exc() - traceback.print_exc(file=open("log/error.log", "a")) + try: + import logging + logging.exception("Unhandled exception: %s" % err) + except Exception, log_err: + print "Failed to log error:", log_err + traceback.print_exc() + from Config import config + traceback.print_exc(file=open(config.log_dir + "/error.log", "a")) if main and main.update_after_shutdown: # Updater # Restart @@ -58,11 +63,22 @@ def main(): import time time.sleep(1) # Wait files to close args = sys.argv[:] - args.insert(0, sys.executable) + + sys.executable = sys.executable.replace(".pkg", "") # Frozen mac fix + + if not getattr(sys, 'frozen', False): + args.insert(0, sys.executable) + if sys.platform == 'win32': args = ['"%s"' % arg for arg in args] - os.execv(sys.executable, args) + + try: + print "Executing %s %s" % (sys.executable, args) + os.execv(sys.executable, args) + except Exception, err: + print "Execv error: %s" % err print "Bye." + if __name__ == '__main__': - main() \ No newline at end of file + main()