diff --git a/src/Config.py b/src/Config.py index e7eeb85a..77fe54db 100644 --- a/src/Config.py +++ b/src/Config.py @@ -3,8 +3,8 @@ import ConfigParser class Config(object): def __init__(self): - self.version = "0.2.9" - self.rev = 134 + self.version = "0.3.0" + self.rev = 187 self.parser = self.createArguments() argv = sys.argv[:] # Copy command line arguments argv = self.parseConfig(argv) # Add arguments from config file @@ -28,10 +28,13 @@ class Config(object): coffeescript = "type %s | tools\\coffee\\coffee.cmd" else: coffeescript = None - if sys.platform.startswith("Darwin"): # For some reasons openssl doesnt works on mac yet (https://github.com/HelloZeroNet/ZeroNet/issues/94) + """ Probably fixed + if sys.platform.lower().startswith("darwin"): # For some reasons openssl doesnt works on mac yet (https://github.com/HelloZeroNet/ZeroNet/issues/94) use_openssl = False else: use_openssl = True + """ + use_openssl = True # Create parser parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -49,6 +52,7 @@ class Config(object): action.add_argument('address', help='Site to sign') 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('--publish', help='Publish site after the signing', action='store_true') # SitePublish action = subparsers.add_parser("sitePublish", help='Publish site to other peers: address') @@ -89,6 +93,10 @@ class Config(object): action.add_argument('cmd', help='Command to execute') action.add_argument('parameters', help='Parameters to command', nargs='?') + # CryptSign + action = subparsers.add_parser("cryptSign", help='Sign message using Bitcoin private key') + action.add_argument('message', help='Message to sign') + action.add_argument('privatekey', help='Private key') # Config parameters diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index bb28193e..508ef0d8 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -106,7 +106,7 @@ class Connection(object): self.incomplete_buff_recv = 0 self.handleMessage(message) message = None - buf = None + buff = None except Exception, err: if not self.closed: self.log("Socket error: %s" % Debug.formatException(err)) self.close() # MessageLoop ended, close connection diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 99d112fa..fa9edd46 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -1,4 +1,4 @@ -import json, time, re, os, gevent +import json, time, re, os, gevent, copy from Debug import Debug from Crypt import CryptHash from Config import config @@ -18,13 +18,14 @@ class ContentManager: content_inner_path = content_inner_path.strip("/") # Remove / from begning old_content = self.contents.get(content_inner_path) content_path = self.site.storage.getPath(content_inner_path) + content_path_dir = self.toDir(self.site.storage.getPath(content_inner_path)) content_dir = self.toDir(content_inner_path) if os.path.isfile(content_path): try: new_content = json.load(open(content_path)) except Exception, err: - self.log.error("Content.json load error: %s" % Debug.formatException(err)) + self.log.error("%s load error: %s" % (content_path, Debug.formatException(err))) return False else: self.log.error("Content.json not exits: %s" % content_path) @@ -58,6 +59,14 @@ class ContentManager: self.log.debug("Missing include: %s" % include_inner_path) changed += [include_inner_path] + # Load blind user includes (all subdir) + if load_includes and "user_contents" in new_content: + for relative_dir in os.listdir(content_path_dir): + include_inner_path = content_dir+relative_dir+"/content.json" + if not self.site.storage.isFile(include_inner_path): continue # Content.json not exits + success = self.loadContent(include_inner_path, add_bad_files=add_bad_files, load_includes=False) + if success: changed += success # Add changed files + # Update the content self.contents[content_inner_path] = new_content except Exception, err: @@ -97,19 +106,27 @@ class ContentManager: content = self.contents.get(content_inner_path.strip("/")) if content and "files" in content: # Check if content.json exists back = content["files"].get("/".join(inner_path_parts)) - if not back: return False - back["content_inner_path"] = content_inner_path + if back: + back["content_inner_path"] = content_inner_path + return back + + if content and "user_contents" in content: # User dir + back = content["user_contents"] + back["content_inner_path"] = re.sub("(.*)/.*?$", "\\1/content.json", inner_path) # Content.json is in the users dir return back - else: # No inner path in this dir, lets try the parent dir - if dirs: - inner_path_parts.insert(0, dirs.pop()) - else: # No more parent dirs - break + + # No inner path in this dir, lets try the parent dir + if dirs: + inner_path_parts.insert(0, dirs.pop()) + else: # No more parent dirs + break return False # Not found - def getIncludeInfo(self, inner_path): + # Get rules for the file + # Return: The rules for the file or False if not allowed + def getRules(self, inner_path, content=None): if not inner_path.endswith("content.json"): # Find the files content.json first file_info = self.getFileInfo(inner_path) if not file_info: return False # File not found @@ -119,9 +136,11 @@ class ContentManager: inner_path_parts.insert(0, dirs.pop()) # Dont check in self dir while True: content_inner_path = "%s/content.json" % "/".join(dirs) - content = self.contents.get(content_inner_path.strip("/")) - if content and "includes" in content: - return content["includes"].get("/".join(inner_path_parts)) + parent_content = self.contents.get(content_inner_path.strip("/")) + if parent_content and "includes" in parent_content: + return parent_content["includes"].get("/".join(inner_path_parts)) + elif parent_content and "user_contents" in parent_content: + return self.getUserContentRules(parent_content, inner_path, content) else: # No inner path in this dir, lets try the parent dir if dirs: inner_path_parts.insert(0, dirs.pop()) @@ -131,10 +150,55 @@ class ContentManager: return False + # Get rules for a user file + # Return: The rules of the file or False if not allowed + def getUserContentRules(self, parent_content, inner_path, content): + user_contents = parent_content["user_contents"] + user_address = re.match(".*/([A-Za-z0-9]*?)/.*?$", inner_path).group(1) # Delivered for directory + + try: + if not content: content = self.site.storage.loadJson(inner_path) # Read the file if no content specificed + except: # Content.json not exits + return { "signers": [user_address], "user_address": user_address } # Return information that we know for sure + + """if not "cert_user_name" in content: # New file, unknown user + content["cert_auth_type"] = "unknown" + content["cert_user_name"] = "unknown@unknown" + """ + user_urn = "%s/%s" % (content["cert_auth_type"], content["cert_user_id"]) # web/nofish@zeroid.bit + + rules = copy.copy(user_contents["permissions"].get(content["cert_user_id"], {})) # Default rules by username + if rules == False: return False # User banned + if "signers" in rules: rules["signers"] = rules["signers"][:] # Make copy of the signers + for permission_pattern, permission_rules in user_contents["permission_rules"].items(): # Regexp rules + if not re.match(permission_pattern, user_urn): continue # Rule is not valid for user + # Update rules if its better than current recorded ones + for key, val in permission_rules.iteritems(): + if key not in rules: + if type(val) is list: + rules[key] = val[:] # Make copy + else: + rules[key] = val + elif type(val) is int: # Int, update if larger + if val > rules[key]: rules[key] = val + elif hasattr(val, "startswith"): # String, update if longer + if len(val) > len(rules[key]): rules[key] = val + elif type(val) is list: # List, append + rules[key] += val + + rules["cert_signers"] = user_contents["cert_signers"] # Add valid cert signers + if "signers" not in rules: rules["signers"] = [] + rules["signers"].append(user_address) # Add user as valid signer + rules["user_address"] = user_address + + + return rules + + # 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): + def sign(self, inner_path = "content.json", privatekey=None, filewrite=True, update_changed_files=False, extend=None): content = self.contents.get(inner_path) if not content: # Content not exits yet, load default one self.log.info("File %s not exits yet, loading default values..." % inner_path) @@ -144,6 +208,7 @@ class ContentManager: content["description"] = "" content["signs_required"] = 1 content["ignore"] = "" + if extend: content.update(extend) # Add custom fields directory = self.toDir(self.site.storage.getPath(inner_path)) self.log.info("Opening site data directory: %s..." % directory) @@ -154,8 +219,13 @@ class ContentManager: for file_name in files: file_path = self.site.storage.getPath("%s/%s" % (root.strip("/"), file_name)) file_inner_path = re.sub(re.escape(directory), "", file_path) - - if file_name == "content.json" or (content.get("ignore") and re.match(content["ignore"], file_inner_path)) or file_name.startswith("."): # Ignore content.json, definied regexp and files starting with . + + if file_name == "content.json": ignored = True + elif content.get("ignore") and re.match(content["ignore"], file_inner_path): ignored = True + elif file_name.startswith("."): ignored = True + else: ignored = False + + if ignored: # Ignore content.json, definied regexp and files starting with . self.log.info("- [SKIPPED] %s" % file_inner_path) else: sha512sum = CryptHash.sha512sum(file_path) # Calculate sha512 sum of file @@ -184,7 +254,7 @@ class ContentManager: from Crypt import CryptBitcoin self.log.info("Verifying private key...") privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey) - valid_signers = self.getValidSigners(inner_path) + valid_signers = self.getValidSigners(inner_path, new_content) if privatekey_address not in valid_signers: return self.log.error("Private key invalid! Valid signers: %s, Private key address: %s" % (valid_signers, privatekey_address)) self.log.info("Correct %s in valid signers: %s" % (privatekey_address, valid_signers)) @@ -215,7 +285,7 @@ class ContentManager: if filewrite: self.log.info("Saving to %s..." % inner_path) - json.dump(new_content, open(self.site.storage.getPath(inner_path), "w"), indent=2, sort_keys=True) + self.site.storage.writeJson(inner_path, new_content) self.log.info("File %s signed!" % inner_path) @@ -227,25 +297,39 @@ class ContentManager: # The valid signers of content.json file # Return: ["1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6", "13ReyhCsjhpuCVahn1DHdf6eMqqEVev162"] - def getValidSigners(self, inner_path): + def getValidSigners(self, inner_path, content=None): valid_signers = [] if inner_path == "content.json": # Root content.json if "content.json" in self.contents and "signers" in self.contents["content.json"]: valid_signers += self.contents["content.json"]["signers"].keys() else: - include_info = self.getIncludeInfo(inner_path) - if include_info and "signers" in include_info: - valid_signers += include_info["signers"] + rules = self.getRules(inner_path, content) + if rules and "signers" in rules: + valid_signers += rules["signers"] if self.site.address not in valid_signers: valid_signers.append(self.site.address) # Site address always valid return valid_signers # Return: The required number of valid signs for the content.json - def getSignsRequired(self, inner_path): + def getSignsRequired(self, inner_path, content=None): return 1 # Todo: Multisig + def verifyCert(self, inner_path, content): + from Crypt import CryptBitcoin + + rules = self.getRules(inner_path, content) + if not rules.get("cert_signers"): return True # Does not need cert + + name, domain = content["cert_user_id"].split("@") + cert_address = rules["cert_signers"].get(domain) + if not cert_address: # Cert signer not allowed + self.log.error("Invalid cert signer: %s" % domain) + return False + return CryptBitcoin.verify("%s#%s/%s" % (rules["user_address"], content["cert_auth_type"], name), cert_address, content["cert_sign"]) + + # Checks if the content.json content is valid # Return: True or False def validContent(self, inner_path, content): @@ -266,26 +350,26 @@ class ContentManager: if inner_path == "content.json": return True # Root content.json is passed # Load include details - include_info = self.getIncludeInfo(inner_path) - if not include_info: - self.log.error("%s: No include info" % inner_path) + rules = self.getRules(inner_path, content) + if not rules: + self.log.error("%s: No rules" % inner_path) return False # Check include size limit - if include_info.get("max_size"): # Include size limit - if content_size > include_info["max_size"]: - self.log.error("%s: Include too large %s > %s" % (inner_path, content_size, include_info["max_size"])) + if rules.get("max_size"): # Include size limit + if content_size > rules["max_size"]: + self.log.error("%s: Include too large %s > %s" % (inner_path, content_size, rules["max_size"])) return False # Check if content includes allowed - if include_info.get("includes_allowed") == False and content.get("includes"): + if rules.get("includes_allowed") == False and content.get("includes"): self.log.error("%s: Includes not allowed" % inner_path) return False # Includes not allowed # Filename limit - if include_info.get("files_allowed"): + if rules.get("files_allowed"): for file_inner_path in content["files"].keys(): - if not re.match("^%s$" % include_info["files_allowed"], file_inner_path): + if not re.match("^%s$" % rules["files_allowed"], file_inner_path): self.log.error("%s: File not allowed" % file_inner_path) return False @@ -322,19 +406,25 @@ class ContentManager: if not self.validContent(inner_path, new_content): return False # Content not valid (files too large, invalid files) if signs: # New style signing - valid_signers = self.getValidSigners(inner_path) - signs_required = self.getSignsRequired(inner_path) + valid_signers = self.getValidSigners(inner_path, new_content) + signs_required = self.getSignsRequired(inner_path, new_content) if inner_path == "content.json" and len(valid_signers) > 1: # Check signers_sign on root content.json if not CryptBitcoin.verify("%s:%s" % (signs_required, ",".join(valid_signers)), self.site.address, new_content["signers_sign"]): self.log.error("%s invalid signers_sign!" % inner_path) return False + if inner_path != "content.json" and not self.verifyCert(inner_path, new_content): # Check if cert valid + self.log.error("%s invalid cert!" % inner_path) + return False + valid_signs = 0 for address in valid_signers: if address in signs: valid_signs += CryptBitcoin.verify(sign_content, address, signs[address]) if valid_signs >= signs_required: break # Break if we has enough signs + + return valid_signs >= signs_required else: # Old style signing return CryptBitcoin.verify(sign_content, self.site.address, sign) @@ -348,8 +438,10 @@ class ContentManager: if file_info: if "sha512" in file_info: hash_valid = CryptHash.sha512sum(file) == file_info["sha512"] - else: # Backward compatibility + elif "sha1" in file_info: # Backward compatibility hash_valid = CryptHash.sha1sum(file) == file_info["sha1"] + else: + hash_valid = False if file_info["size"] != file.tell(): self.log.error("%s file size does not match %s <> %s, Hash: %s" % (inner_path, file.tell(), file_info["size"], hash_valid)) return False diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index 295c1e8f..f5b80cf5 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -62,7 +62,11 @@ def verify(data, address, sign): # Verify data using address and sign else: # Use pure-python pub = btctools.ecdsa_recover(data, sign) sign_address = btctools.pubtoaddr(pub) - return sign_address == address + + if type(address) is list: # Any address in the list + return sign_address in address + else: # One possible address + return sign_address == address else: # Backward compatible old style bitcoin = BitcoinECC.Bitcoin() return bitcoin.VerifyMessageFromBitcoinAddress(address, data, sign) diff --git a/src/Db/Db.py b/src/Db/Db.py index 9a357a1e..0db36319 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -6,6 +6,7 @@ class Db: self.db_path = db_path self.db_dir = os.path.dirname(db_path)+"/" self.schema = schema + self.schema["version"] = self.schema.get("version", 1) self.conn = None self.cur = None self.log = logging.getLogger("Db:%s" % schema["db_name"]) @@ -85,6 +86,7 @@ class Db: cur.execute("BEGIN") # Check internal tables + # Check keyvalue table changed = cur.needTable("keyvalue", [ ["keyvalue_id", "INTEGER PRIMARY KEY AUTOINCREMENT"], ["key", "TEXT"], @@ -92,15 +94,25 @@ class Db: ["json_id", "INTEGER REFERENCES json (json_id)"], ],[ "CREATE UNIQUE INDEX key_id ON keyvalue(json_id, key)" - ], version=1) + ], version=self.schema["version"]) if changed: changed_tables.append("keyvalue") - changed = cur.needTable("json", [ - ["json_id", "INTEGER PRIMARY KEY AUTOINCREMENT"], - ["path", "VARCHAR(255)"] - ], [ - "CREATE UNIQUE INDEX path ON json(path)" - ], version=1) + # Check json table + if self.schema["version"] == 1: + changed = cur.needTable("json", [ + ["json_id", "INTEGER PRIMARY KEY AUTOINCREMENT"], + ["path", "VARCHAR(255)"] + ], [ + "CREATE UNIQUE INDEX path ON json(path)" + ], version=self.schema["version"]) + else: + changed = cur.needTable("json", [ + ["json_id", "INTEGER PRIMARY KEY AUTOINCREMENT"], + ["directory", "VARCHAR(255)"], + ["file_name", "VARCHAR(255)"] + ], [ + "CREATE UNIQUE INDEX path ON json(directory, file_name)" + ], version=self.schema["version"]) if changed: changed_tables.append("json") # Check schema tables diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index e3e32754..9167bfe1 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -1,4 +1,4 @@ -import time +import time, re # Special sqlite cursor class DbCursor: @@ -12,7 +12,7 @@ class DbCursor: def execute(self, query, params=None): if isinstance(params, dict): # Make easier select and insert by allowing dict params if query.startswith("SELECT") or query.startswith("DELETE"): # Convert param dict to SELECT * FROM table WHERE key = ?, key2 = ? format - wheres = ", ".join([key+" = ?" for key in params]) + wheres = "AND ".join([key+" = ?" for key in params]) query = query.replace("?", wheres) params = params.values() else: # Convert param dict to INSERT INTO table (key, key2) VALUES (?, ?) format @@ -94,12 +94,21 @@ class DbCursor: # Get or create a row for json file # Return: The database row def getJsonRow(self, file_path): - res = self.execute("SELECT * FROM json WHERE ? LIMIT 1", {"path": file_path}) - row = res.fetchone() - if not row: # No row yet, create it - self.execute("INSERT INTO json ?", {"path": file_path}) + directory, file_name = re.match("^(.*?)/*([^/]*)$", file_path).groups() + if self.db.schema["version"] == 1: res = self.execute("SELECT * FROM json WHERE ? LIMIT 1", {"path": file_path}) row = res.fetchone() + if not row: # No row yet, create it + self.execute("INSERT INTO json ?", {"path": file_path}) + res = self.execute("SELECT * FROM json WHERE ? LIMIT 1", {"path": file_path}) + row = res.fetchone() + else: + res = self.execute("SELECT * FROM json WHERE ? LIMIT 1", {"directory": directory, "file_name": file_name}) + row = res.fetchone() + if not row: # No row yet, create it + self.execute("INSERT INTO json ?", {"directory": directory, "file_name": file_name}) + res = self.execute("SELECT * FROM json WHERE ? LIMIT 1", {"directory": directory, "file_name": file_name}) + row = res.fetchone() return row def close(self): diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 57942bcd..dfb2e8b0 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -170,6 +170,12 @@ class FileRequest(object): self.response({"error": "Unknown site"}) return False modified_files = {inner_path: content["modified"] for inner_path, content in site.content_manager.contents.iteritems() if content["modified"] > params["since"]} + + # Add peer to site if not added before + connected_peer = site.addPeer(self.connection.ip, self.connection.port) + if connected_peer: # Just added + connected_peer.connect(self.connection) # Assign current connection to peer + self.response({"modified_files": modified_files}) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 2a84423d..92e49b9f 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -131,6 +131,7 @@ class Peer(object): return False buff.write(back["body"]) + back["body"] = None # Save memory if back["location"] == back["size"]: # End of file break else: diff --git a/src/Site/Site.py b/src/Site/Site.py index b0f5aad9..3dc81128 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -151,17 +151,18 @@ class Site: self.log.debug("Start downloading...%s" % self.bad_files) gevent.spawn(self.announce) if check_size: # Check the size first - valid = downloadContent(download_files=False) + valid = downloadContent(download_files=False) # Just download content.json files if not valid: return False # Cant download content.jsons or size is not fits + # Download everything found = self.downloadContent("content.json") + self.checkModifications(0) # Download multiuser blind includes return found # Update worker, try to find client that supports listModifications command - def updater(self, peers_try, queried): - since = self.settings.get("modified", 60*60*24)-60*60*24 # Get modified since last update - 1day + def updater(self, peers_try, queried, since): while 1: if not peers_try or len(queried) >= 3: # Stop after 3 successful query break @@ -179,16 +180,9 @@ class Site: gevent.spawn(self.downloadContent, inner_path) # Download the content.json + the changed files - - # Update content.json from peers and download changed files - # Return: None - @util.Noparallel() - def update(self, announce=False): - self.content_manager.loadContent("content.json") # Reload content.json - self.content_updated = None # Reset content updated time - self.updateWebsocket(updating=True) - if announce: self.announce() - + # Check modified content.json files from peers and add modified files to bad_files + # Return: Successfully queried peers [Peer, Peer...] + def checkModifications(self, since=None): peers_try = [] # Try these peers queried = [] # Successfully queried from these peers @@ -200,15 +194,30 @@ class Site: elif len(peers_try) < 5: # Backup peers, add to end of the try list peers_try.append(peer) - self.log.debug("Try to get listModifications from peers: %s" % peers_try) + if since == None: # No since definied, download from last modification time-1day + since = self.settings.get("modified", 60*60*24)-60*60*24 + self.log.debug("Try to get listModifications from peers: %s since: %s" % (peers_try, since)) updaters = [] for i in range(3): - updaters.append(gevent.spawn(self.updater, peers_try, queried)) + updaters.append(gevent.spawn(self.updater, peers_try, queried, since)) gevent.joinall(updaters, timeout=5) # Wait 5 sec to workers time.sleep(0.1) self.log.debug("Queried listModifications from: %s" % queried) + return queried + + + # Update content.json from peers and download changed files + # Return: None + @util.Noparallel() + def update(self, announce=False): + self.content_manager.loadContent("content.json") # Reload content.json + self.content_updated = None # Reset content updated time + self.updateWebsocket(updating=True) + if announce: self.announce() + + queried = self.checkModifications() if not queried: # Not found any client that supports listModifications self.log.debug("Fallback to old-style update") @@ -279,10 +288,11 @@ class Site: # Update content.json on peers @util.Noparallel() def publish(self, limit=5, inner_path="content.json"): - self.log.info( "Publishing to %s/%s peers..." % (limit, len(self.peers)) ) + self.log.info( "Publishing to %s/%s peers..." % (min(len(self.peers), limit), len(self.peers)) ) published = [] # Successfully published (Peer) publishers = [] # Publisher threads peers = self.peers.values() + if not peers: return 0 # No peers found random.shuffle(peers) event_done = gevent.event.AsyncResult() @@ -381,12 +391,79 @@ class Site: self.log.debug("Queried pex from %s peers got %s new peers." % (done, added)) + # Gather peers from tracker + # Return: Complete time or False on error + def announceTracker(self, protocol, ip, port, fileserver_port, address_hash, my_peer_id): + s = time.time() + if protocol == "udp": # Udp tracker + if config.disable_udp: return False # No udp supported + tracker = UdpTrackerClient(ip, port) + tracker.peer_port = fileserver_port + try: + tracker.connect() + tracker.poll_once() + tracker.announce(info_hash=address_hash, num_want=50) + back = tracker.poll_once() + peers = back["response"]["peers"] + except Exception, err: + return False + + else: # Http tracker + params = { + 'info_hash': binascii.a2b_hex(address_hash), + 'peer_id': my_peer_id, 'port': fileserver_port, + 'uploaded': 0, 'downloaded': 0, 'left': 0, 'compact': 1, 'numwant': 30, + 'event': 'started' + } + req = None + try: + url = "http://"+ip+"?"+urllib.urlencode(params) + # Load url + with gevent.Timeout(10, False): # Make sure of timeout + req = urllib2.urlopen(url, timeout=8) + response = req.read() + req.fp._sock.recv=None # Hacky avoidance of memory leak for older python versions + req.close() + req = None + if not response: + self.log.debug("Http tracker %s response error" % url) + return False + # Decode peers + peer_data = bencode.decode(response)["peers"] + response = None + peer_count = len(peer_data) / 6 + peers = [] + for peer_offset in xrange(peer_count): + off = 6 * peer_offset + peer = peer_data[off:off + 6] + addr, port = struct.unpack('!LH', peer) + peers.append({"addr": socket.inet_ntoa(struct.pack('!L', addr)), "port": port}) + except Exception, err: + self.log.debug("Http tracker %s error: %s" % (url, err)) + if req: + req.close() + req = None + return False + + # Adding peers + added = 0 + for peer in peers: + if not peer["port"]: continue # Dont add peers with port 0 + if self.addPeer(peer["addr"], peer["port"]): added += 1 + if added: + self.worker_manager.onPeers() + self.updateWebsocket(peers_added=added) + self.log.debug("Found %s peers, new: %s" % (len(peers), added)) + return time.time()-s + + # Add myself and get other peers from tracker def announce(self, force=False): if time.time() < self.last_announce+30 and not force: return # No reannouncing within 30 secs self.last_announce = time.time() errors = [] - address_hash = hashlib.sha1(self.address).hexdigest() + slow = [] + address_hash = hashlib.sha1(self.address).hexdigest() # Site address hash my_peer_id = sys.modules["main"].file_server.peer_id if sys.modules["main"].file_server.port_opened: @@ -396,73 +473,30 @@ class Site: s = time.time() announced = 0 + threads = [] - for protocol, ip, port in SiteManager.TRACKERS: - if protocol == "udp": # Udp tracker - if config.disable_udp: continue # No udp supported - tracker = UdpTrackerClient(ip, port) - tracker.peer_port = fileserver_port - try: - tracker.connect() - tracker.poll_once() - tracker.announce(info_hash=address_hash, num_want=50) - back = tracker.poll_once() - peers = back["response"]["peers"] - except Exception, err: - errors.append("%s://%s:%s" % (protocol, ip, port)) - continue - - else: # Http tracker - params = { - 'info_hash': binascii.a2b_hex(address_hash), - 'peer_id': my_peer_id, 'port': fileserver_port, - 'uploaded': 0, 'downloaded': 0, 'left': 0, 'compact': 1, 'numwant': 30, - 'event': 'started' - } - req = None - try: - url = "http://"+ip+"?"+urllib.urlencode(params) - # Load url - req = urllib2.urlopen(url, timeout=10) - response = req.read() - req.fp._sock.recv=None # Hacky avoidance of memory leak for older python versions - req.close() - req = None - # Decode peers - peer_data = bencode.decode(response)["peers"] - response = None - peer_count = len(peer_data) / 6 - peers = [] - for peer_offset in xrange(peer_count): - off = 6 * peer_offset - peer = peer_data[off:off + 6] - addr, port = struct.unpack('!LH', peer) - peers.append({"addr": socket.inet_ntoa(struct.pack('!L', addr)), "port": port}) - except Exception, err: - self.log.debug("Http tracker %s error: %s" % (url, err)) - errors.append("%s://%s" % (protocol, ip)) - if req: - req.close() - req = None - continue - - # Adding peers - added = 0 - for peer in peers: - if not peer["port"]: continue # Dont add peers with port 0 - if self.addPeer(peer["addr"], peer["port"]): added += 1 - if added: - self.worker_manager.onPeers() - self.updateWebsocket(peers_added=added) - self.log.debug("Found %s peers, new: %s" % (len(peers), added)) - announced += 1 + for protocol, ip, port in SiteManager.TRACKERS: # Start announce threads + thread = gevent.spawn(self.announceTracker, protocol, ip, port, fileserver_port, address_hash, my_peer_id) + threads.append(thread) + thread.ip = ip + thread.protocol = protocol + gevent.joinall(threads) # Wait for announce finish + + for thread in threads: + if thread.value: + if thread.value > 1: + slow.append("%.2fs %s://%s" % (thread.value, thread.protocol, thread.ip)) + announced += 1 + else: + errors.append("%s://%s" % (thread.protocol, thread.ip)) + # Save peers num self.settings["peers"] = len(self.peers) self.saveSettings() if len(errors) < len(SiteManager.TRACKERS): # Less errors than total tracker nums - self.log.debug("Announced port %s to %s trackers in %.3fs, errors: %s" % (fileserver_port, announced, time.time()-s, errors)) + self.log.debug("Announced port %s to %s trackers in %.3fs, errors: %s, slow: %s" % (fileserver_port, announced, time.time()-s, errors, slow)) else: self.log.error("Announced to %s trackers in %.3fs, failed" % (announced, time.time()-s)) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 1f7d0481..7a7e4d14 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -12,9 +12,12 @@ TRACKERS = [ #("udp", "trackr.sytes.net", 80), #("udp", "tracker4.piratux.com", 6969) ("http", "exodus.desync.com:80/announce", None), - ("http", "announce.torrentsmd.com:6969/announce", None), - #("http", "i.bandito.org/announce", None), - #("http", "tracker.tfile.me/announce", None), + ("http", "tracker.aletorrenty.pl:2710/announce", None), + #("http", "torrent.gresille.org/announce", None), # Slow + #("http", "announce.torrentsmd.com:6969/announce", None), # Off + #("http", "i.bandito.org/announce", None), # Off + ("http", "retracker.telecom.kz/announce", None) + ] diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index c3344a34..fd08ae85 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -1,6 +1,8 @@ import os, re, shutil, json, time, sqlite3 import gevent.event from Db import Db +from Debug import Debug + class SiteStorage: def __init__(self, site, allow_create=True): @@ -36,13 +38,17 @@ class SiteStorage: def closeDb(self): if self.db: self.db.close() + self.event_db_busy = None self.db = None # Return db class def getDb(self): - if not self.db and self.has_db: - self.openDb() + if not self.db: + self.log.debug("No database, waiting for dbschema.json...") + self.site.needFile("dbschema.json", priority=1) + self.has_db = self.isFile("dbschema.json") # Recheck if dbschema exits + if self.has_db: self.openDb() return self.db @@ -143,10 +149,13 @@ class SiteStorage: if inner_path == "dbschema.json": self.has_db = self.isFile("dbschema.json") self.getDb().checkTables() # Check if any if table schema changed - elif inner_path != "content.json" and inner_path.endswith(".json") and self.has_db: # Load json file to db + elif inner_path.endswith(".json") and self.has_db: # Load json file to db self.log.debug("Loading json file to db: %s" % inner_path) - self.getDb().loadJson(file_path) - + try: + self.getDb().loadJson(file_path) + except Exception, err: + self.log.error("Json %s load error: %s" % (inner_path, Debug.formatException(err))) + self.closeDb() # Load and parse json file @@ -154,6 +163,21 @@ class SiteStorage: with self.open(inner_path) as file: return json.load(file) + # Write formatted json file + def writeJson(self, inner_path, data): + content = json.dumps(data, indent=2, sort_keys=True) + # Make it a little more compact by removing unnecessary white space + def compact_list(match): + return "[ "+match.group(1).strip()+" ]" + + def compact_dict(match): + return "{ "+match.group(1).strip()+" }" + + content = re.sub("\[([^,\{\[]{10,100}?)\]", compact_list, content, flags=re.DOTALL) + content = re.sub("\{([^,\[\{]{10,100}?)\}", compact_dict, content, flags=re.DOTALL) + # Write to disk + self.write(inner_path, content) + # Get file size def getSize(self, inner_path): diff --git a/src/Test/test.py b/src/Test/test.py index 8067ebd3..250d015d 100644 --- a/src/Test/test.py +++ b/src/Test/test.py @@ -152,7 +152,6 @@ class TestCase(unittest.TestCase): # Cleanup os.unlink("data/test/zeronet.db") os.rmdir("data/test/") - print "ok" def testContentManagerIncludes(self): @@ -162,12 +161,12 @@ class TestCase(unittest.TestCase): site = Site("1TaLk3zM7ZRskJvrh3ZNCDVGXvkJusPKQ") # Include info - include_info = site.content_manager.getIncludeInfo("data/users/1BhcaqWViN1YBnNgXb5aq5NtEhKtKdKZMB/content.json") - self.assertEqual(include_info["signers"], ['1BhcaqWViN1YBnNgXb5aq5NtEhKtKdKZMB']) - self.assertEqual(include_info["user_name"], 'testuser4') - self.assertEqual(include_info["max_size"], 10000) - self.assertEqual(include_info["includes_allowed"], False) - self.assertEqual(include_info["files_allowed"], 'data.json') + rules = site.content_manager.getRules("data/users/1BhcaqWViN1YBnNgXb5aq5NtEhKtKdKZMB/content.json") + self.assertEqual(rules["signers"], ['1BhcaqWViN1YBnNgXb5aq5NtEhKtKdKZMB']) + self.assertEqual(rules["user_name"], 'testuser4') + self.assertEqual(rules["max_size"], 10000) + self.assertEqual(rules["includes_allowed"], False) + self.assertEqual(rules["files_allowed"], 'data.json') # Valid signers self.assertEqual( site.content_manager.getValidSigners("data/users/1BhcaqWViN1YBnNgXb5aq5NtEhKtKdKZMB/content.json"), @@ -207,9 +206,106 @@ class TestCase(unittest.TestCase): self.assertEqual(site.content_manager.verifyFile("data/users/1BhcaqWViN1YBnNgXb5aq5NtEhKtKdKZMB/content.json", data, ignore_same=False), True) + def testUserContentRules(self): + from Site import Site + from cStringIO import StringIO + import json + + site = Site("1Hb9rY98TNnA6TYeozJv4w36bqEiBn6x8Y") + user_content = site.storage.loadJson("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json") + + # File info for not exits file + self.assertEqual(site.content_manager.getFileInfo("data/users/notexits/data.json")["content_inner_path"], "data/users/notexits/content.json") + self.assertEqual(site.content_manager.getValidSigners("data/users/notexits/data.json"), ["notexits", "1Hb9rY98TNnA6TYeozJv4w36bqEiBn6x8Y"]) + + # File info for exsitsing file + file_info = site.content_manager.getFileInfo("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/data.json") + valid_signers = site.content_manager.getValidSigners(file_info["content_inner_path"], user_content) + self.assertEqual(valid_signers, ['14wgQ4VDDZNoRMFF4yCDuTrBSHmYhL3bet', '1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C', '1Hb9rY98TNnA6TYeozJv4w36bqEiBn6x8Y']) + + # Known user + user_content["cert_auth_type"] = "web" + user_content["cert_user_id"] = "nofish@zeroid.bit" + rules = site.content_manager.getRules("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_content) + self.assertEqual(rules["max_size"], 100000) + + # Unknown user + user_content["cert_auth_type"] = "web" + user_content["cert_user_id"] = "noone@zeroid.bit" + rules = site.content_manager.getRules("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_content) + self.assertEqual(rules["max_size"], 10000) + + # User with more size limit by auth type + user_content["cert_auth_type"] = "bitmsg" + user_content["cert_user_id"] = "noone@zeroid.bit" + rules = site.content_manager.getRules("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_content) + self.assertEqual(rules["max_size"], 20000) + + # Banned user + user_content["cert_auth_type"] = "web" + user_content["cert_user_id"] = "bad@zeroid.bit" + rules = site.content_manager.getRules("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_content) + self.assertFalse(rules) + + + def testUserContentCert(self): + from Site import Site + from cStringIO import StringIO + import json + user_addr = "1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C" + user_priv = "5Kk7FSA63FC2ViKmKLuBxk9gQkaQ5713hKq8LmFAf4cVeXh6K6A" + cert_addr = "14wgQ4VDDZNoRMFF4yCDuTrBSHmYhL3bet" + cert_priv = "5JusJDSjHaMHwUjDT3o6eQ54pA6poo8La5fAgn1wNc3iK59jxjA" + + site = Site("1Hb9rY98TNnA6TYeozJv4w36bqEiBn6x8Y") + #user_content = site.storage.loadJson("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json") + # site.content_manager.contents["data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json"] = user_content # Add to content manager + # Check if the user file is loaded + self.assertTrue("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json" in site.content_manager.contents) + user_content = site.content_manager.contents["data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json"] + cert_content = site.content_manager.contents["data/users/content.json"] + # Override cert signer + cert_content["user_contents"]["cert_signers"]["zeroid.bit"] = ["14wgQ4VDDZNoRMFF4yCDuTrBSHmYhL3bet", "1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz"] + + + # Valid cert providers + rules = site.content_manager.getRules("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_content) + self.assertEqual(rules["cert_signers"], {"zeroid.bit": ["14wgQ4VDDZNoRMFF4yCDuTrBSHmYhL3bet", "1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz"]} ) + + # Add cert + user_content["cert_sign"] = CryptBitcoin.sign( + "1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C#%s/%s" % (user_content["cert_auth_type"], user_content["cert_user_id"].split("@")[0]), cert_priv + ) + + # Verify cert + self.assertTrue(site.content_manager.verifyCert("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_content)) + self.assertFalse(site.content_manager.verifyCert("data/users/badaddress/content.json", user_content)) + + + # Sign user content + #signed_content = site.content_manager.sign("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_priv, filewrite=False) + signed_content = site.storage.loadJson("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json") + + # Test user cert + self.assertTrue(site.content_manager.verifyFile("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", StringIO(json.dumps(signed_content)), ignore_same=False)) + + # Test banned user + site.content_manager.contents["data/users/content.json"]["user_contents"]["permissions"][user_content["cert_user_id"]] = False + self.assertFalse(site.content_manager.verifyFile("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", StringIO(json.dumps(signed_content)), ignore_same=False)) + + # Test invalid cert + user_content["cert_sign"] = CryptBitcoin.sign( + "badaddress#%s/%s" % (user_content["cert_auth_type"], user_content["cert_user_id"]), cert_priv + ) + signed_content = site.content_manager.sign("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_priv, filewrite=False) + self.assertFalse(site.content_manager.verifyFile("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", StringIO(json.dumps(signed_content)), ignore_same=False)) + + + if __name__ == "__main__": import logging - logging.getLogger().setLevel(level=logging.CRITICAL) - unittest.main(verbosity=2, defaultTest="TestCase.testContentManagerIncludes") + logging.getLogger().setLevel(level=logging.FATAL) + unittest.main(verbosity=2) + #unittest.main(verbosity=2, defaultTest="TestCase.testUserContentCert") diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index cc8b5e42..fbbe3437 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -170,7 +170,7 @@ class UiRequest(object): if not site: return False - extra_headers.append(("X-Frame-Options", "DENY")) + #extra_headers.append(("X-Frame-Options", "DENY")) self.sendHeader(extra_headers=extra_headers[:]) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 83a4b0ac..f10e2664 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -96,44 +96,13 @@ class UiWebsocket(object): permissions = permissions[:] permissions.append("ADMIN") + admin_commands = ("sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "channelJoinAllsite", "serverUpdate", "certSet") + if cmd == "response": # It's a response to a command return self.actionResponse(req["to"], req["result"]) - elif cmd == "ping": - func = self.actionPing - elif cmd == "channelJoin": - func = self.actionChannelJoin - elif cmd == "siteInfo": - func = self.actionSiteInfo - elif cmd == "serverInfo": - func = self.actionServerInfo - elif cmd == "siteUpdate": - func = self.actionSiteUpdate - elif cmd == "sitePublish": - func = self.actionSitePublish - elif cmd == "fileWrite": - func = self.actionFileWrite - elif cmd == "fileGet": - func = self.actionFileGet - elif cmd == "fileQuery": - func = self.actionFileQuery - elif cmd == "dbQuery": - func = self.actionDbQuery - # Admin commands - elif cmd == "sitePause" and "ADMIN" in permissions: - func = self.actionSitePause - elif cmd == "siteResume" and "ADMIN" in permissions: - func = self.actionSiteResume - elif cmd == "siteDelete" and "ADMIN" in permissions: - func = self.actionSiteDelete - elif cmd == "siteList" and "ADMIN" in permissions: - func = self.actionSiteList - elif cmd == "siteSetLimit" and "ADMIN" in permissions: - func = self.actionSiteSetLimit - elif cmd == "channelJoinAllsite" and "ADMIN" in permissions: - func = self.actionChannelJoinAllsite - elif cmd == "serverUpdate" and "ADMIN" in permissions: - func = self.actionServerUpdate - else: + elif cmd in admin_commands and "ADMIN" not in permissions: # Admin commands + return self.response(req["id"], "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) if not func: # Unknown command @@ -158,6 +127,7 @@ class UiWebsocket(object): content["includes"] = len(content.get("includes", {})) if "sign" in content: del(content["sign"]) if "signs" in content: del(content["signs"]) + if "signers_sign" in content: del(content["signers_sign"]) settings = site.settings.copy() del settings["wrapper_key"] # Dont expose wrapper key @@ -167,6 +137,7 @@ class UiWebsocket(object): "auth_key": self.site.settings["auth_key"], # Obsolete, will be removed "auth_key_sha512": hashlib.sha512(self.site.settings["auth_key"]).hexdigest()[0:64], # Obsolete, will be removed "auth_address": self.user.getAuthAddress(site.address, create=create_user), + "cert_user_id": self.user.getCertUserId(site.address), "address": site.address, "settings": settings, "content_updated": site.content_updated, @@ -236,8 +207,16 @@ class UiWebsocket(object): def actionSitePublish(self, to, privatekey=None, inner_path="content.json"): site = self.site + extend = {} # Extended info for signing if not inner_path.endswith("content.json"): # Find the content.json first - inner_path = site.content_manager.getFileInfo(inner_path)["content_inner_path"] + file_info = site.content_manager.getFileInfo(inner_path) + inner_path = file_info["content_inner_path"] + if "cert_signers" in file_info: # Its an user dir file + cert = self.user.getCert(self.site.address) + extend["cert_auth_type"] = cert["auth_type"] + extend["cert_user_id"] = self.user.getCertUserId(site.address) + extend["cert_sign"] = cert["cert_sign"] + if not site.settings["own"] and self.user.getAuthAddress(self.site.address) not in self.site.content_manager.getValidSigners(inner_path): return self.response(to, "Forbidden, you can only modify your own sites") @@ -246,7 +225,7 @@ class UiWebsocket(object): # Signing site.content_manager.loadContent(add_bad_files=False) # Reload content.json, ignore errors to make it up-to-date - signed = site.content_manager.sign(inner_path, privatekey) # Sign using private key sent by user + signed = site.content_manager.sign(inner_path, privatekey, extend=extend) # Sign using private key sent by user if signed: if inner_path == "content_json": self.cmd("notification", ["done", "Private key correct, content signed!", 5000]) # Display message for 5 sec else: @@ -301,7 +280,13 @@ class UiWebsocket(object): if inner_path.endswith("content.json"): self.site.content_manager.loadContent(inner_path, add_bad_files=False) - return self.response(to, "ok") + self.response(to, "ok") + + # Send sitechanged to other local users + for ws in self.site.websockets: + if ws != self: + ws.event("siteChanged", self.site, {"event": ["file_done", inner_path]}) + # Find data in json files @@ -314,7 +299,7 @@ class UiWebsocket(object): # Sql query - def actionDbQuery(self, to, query, params=None): + def actionDbQuery(self, to, query, params=None, wait_for=None): rows = [] try: res = self.site.storage.query(query, params) @@ -327,15 +312,95 @@ class UiWebsocket(object): # Return file content - def actionFileGet(self, to, inner_path): + def actionFileGet(self, to, inner_path, required=True): try: - self.site.needFile(inner_path, priority=1) + if required: self.site.needFile(inner_path, priority=1) body = self.site.storage.read(inner_path) except: body = None return self.response(to, body) + def actionFileRules(self, to, inner_path): + rules = self.site.content_manager.getRules(inner_path) + if inner_path.endswith("content.json"): + content = self.site.content_manager.contents.get(inner_path) + if content: + rules["current_size"] = len(json.dumps(content)) + sum([file["size"] for file in content["files"].values()]) + else: + rules["current_size"] = 0 + return self.response(to, rules) + + + # Add certificate to user + def actionCertAdd(self, to, domain, auth_type, auth_user_name, cert): + try: + res = self.user.addCert(self.user.getAuthAddress(self.site.address), domain, auth_type, auth_user_name, cert) + if res == True: + self.cmd("notification", ["done", "New certificate added: %s/%s@%s." % (auth_type, auth_user_name, domain)]) + self.response(to, "ok") + else: + self.response(to, "Not changed") + except Exception, err: + self.response(to, {"error": err.message}) + + + # Select certificate for site + def actionCertSelect(self, to, accepted_domains=[]): + accounts = [] + accounts.append(["", "Unique to site", ""]) # Default option + active = "" # Make it active if no other option found + + # Add my certs + auth_address = self.user.getAuthAddress(self.site.address) # Current auth address + for domain, cert in self.user.certs.items(): + if auth_address == cert["auth_address"]: + active = domain + title = cert["auth_user_name"]+"@"+domain + if domain in accepted_domains: + accounts.append([domain, title, ""]) + else: + accounts.append([domain, title, "disabled"]) + + + # Render the html + body = "Select account you want to use in this site:" + # Accounts + for domain, account, css_class in accounts: + if domain == active: + css_class += " active" # Currently selected option + title = "%s (currently selected)" % account + else: + title = "%s" % account + body += "%s" % (css_class, domain, title) + # More avalible providers + more_domains = [domain for domain in accepted_domains if domain not in self.user.certs] # Domainains we not displayed yet + if more_domains: + # body+= "Accepted authorization providers by the site:" + body+= "
" + for domain in more_domains: + body += "Register »%s" % (domain, domain) + body+= "
" + + body += """ + + """ + + # Send the notification + self.cmd("notification", ["ask", body]) + + + # Set certificate that used for authenticate user for site + def actionCertSet(self, to, domain): + self.user.setCert(self.site.address, domain) + self.site.updateWebsocket(cert_changed=domain) + # - Admin actions - diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index cb277612..5175317d 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -13,7 +13,6 @@ class Notifications add: (id, type, body, timeout=0) -> - @log id, type, body, timeout # Close notifications with same id for elem in $(".notification-#{id}") @close $(elem) @@ -55,15 +54,14 @@ class Notifications elem.animate({"width": width}, 700, "easeInOutCubic") $(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000) - # Close button - $(".close", elem).on "click", => + # Close button or Confirm button + $(".close, .button", elem).on "click", => @close elem return false - # Close on button click within body (confirm dialog) - $(".button", elem).on "click", => + # Select list + $(".select", elem).on "click", => @close elem - return false close: (elem) -> diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index cfdfdd86..8afa2cb1 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -30,6 +30,12 @@ class Wrapper if window.location.hash src = $("#inner-iframe").attr("src").replace(/#.*/, "")+window.location.hash $("#inner-iframe").attr("src", src) + + ###setInterval (-> + console.log document.hasFocus() + ), 1000### + $("#inner-iframe").focus() + @ @@ -67,6 +73,10 @@ class Wrapper if @ws.ws.readyState == 1 and not @wrapperWsInited # If ws already opened @sendInner {"cmd": "wrapperOpenedWebsocket"} @wrapperWsInited = true + else if cmd == "innerLoaded" + if window.location.hash + $("#inner-iframe")[0].src += window.location.hash # Hash tag + @log "Added hash to location", $("#inner-iframe")[0].src else if cmd == "wrapperNotification" # Display notification @actionNotification(message) else if cmd == "wrapperConfirm" # Display confirm message @@ -208,7 +218,6 @@ class Wrapper @inner_loaded = true if not @inner_ready then @sendInner {"cmd": "wrapperReady"} # Inner frame loaded before wrapper #if not @site_error then @loading.hideScreen() # Hide loading screen - if window.location.hash then $("#inner-iframe")[0].src += window.location.hash # Hash tag if @ws.ws.readyState == 1 and not @site_info # Ws opened @reloadSiteInfo() else if @site_info and @site_info.content?.title? @@ -313,13 +322,27 @@ class Wrapper return false + isProxyRequest: -> + return window.location.pathname == "/" + + + gotoSite: (elem) => + href = $(elem).attr("href") + if @isProxyRequest() # Fix for proxy request + $(elem).attr("href", "http://zero#{href}") + + log: (args...) -> console.log "[Wrapper]", args... +origin = window.server_url or window.location.origin -if window.server_url - ws_url = "ws://#{window.server_url.replace('http://', '')}/Websocket?wrapper_key=#{window.wrapper_key}" +if origin.indexOf("https:") == 0 + proto = { ws: 'wss', http: 'https' } else - ws_url = "ws://#{window.location.hostname}:#{window.location.port}/Websocket?wrapper_key=#{window.wrapper_key}" + proto = { ws: 'ws', http: 'http' } + +ws_url = proto.ws + ":" + origin.replace(proto.http+":", "") + "/Websocket?wrapper_key=" + window.wrapper_key + window.wrapper = new Wrapper(ws_url) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index c10ccf34..4b2cbc42 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -58,6 +58,16 @@ a { color: black } .notification small { color: #AAA } .body-white .notification { box-shadow: 0px 1px 9px rgba(0,0,0,0.1) } +/* Notification select */ +.notification .select { + display: block; padding: 10px; margin-right: -32px; text-decoration: none; border-left: 3px solid #EEE; + margin-top: 1px; transition: all 0.3s; color: #666 +} +.notification .select:hover, .notification .select.active { background-color: #007AFF; border-left: 3px solid #5D68FF; color: white; transition: none } +.notification .select:active, .notification .select:focus { background-color: #3396FF; color: white; transition: none; border-left-color: #3396FF } +.notification .select.disabled { opacity: 0.5; pointer-events: none } +.notification .select small { color: inherit; } + /* Notification types */ .notification-ask .notification-icon { background-color: #f39c12; } .notification-info .notification-icon { font-size: 22px; font-weight: bold; background-color: #2980b9; line-height: 48px } @@ -115,6 +125,10 @@ a { color: black } box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; opacity: 1.0; transform: rotate(3deg) translate(0px, -4px); } +/* Icons */ +.icon-profile { font-size: 6px; top: 0em; border-radius: 0.7em 0.7em 0 0; background: #FFFFFF; width: 1.5em; height: 0.7em; position: relative; display: inline-block; margin-right: 4px } +.icon-profile::before { position: absolute; content: ""; top: -1em; left: 0.38em; width: 0.8em; height: 0.85em; border-radius: 50%; background: #FFFFFF } + /* Animations */ @keyframes flip { @@ -130,3 +144,9 @@ a { color: black } 70% { opacity: 0 } 100% { opacity: 0 } } + +/* Print styles */ +@media print { + #inner-iframe { position: fixed; } + .progressbar, .fixbutton, .notifications, .loadingscreen { visibility: hidden; } +} \ No newline at end of file diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index 83fabae1..be2564b5 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -63,6 +63,16 @@ a { color: black } .notification small { color: #AAA } .body-white .notification { -webkit-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -moz-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -o-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; -ms-box-shadow: 0px 1px 9px rgba(0,0,0,0.1) ; box-shadow: 0px 1px 9px rgba(0,0,0,0.1) } +/* Notification select */ +.notification .select { + display: block; padding: 10px; margin-right: -32px; text-decoration: none; border-left: 3px solid #EEE; + margin-top: 1px; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; color: #666 +} +.notification .select:hover, .notification .select.active { background-color: #007AFF; border-left: 3px solid #5D68FF; color: white; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } +.notification .select:active, .notification .select:focus { background-color: #3396FF; color: white; -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; border-left-color: #3396FF } +.notification .select.disabled { opacity: 0.5; pointer-events: none } +.notification .select small { color: inherit; } + /* Notification types */ .notification-ask .notification-icon { background-color: #f39c12; } .notification-info .notification-icon { font-size: 22px; font-weight: bold; background-color: #2980b9; line-height: 48px } @@ -120,6 +130,10 @@ a { color: black } -webkit-box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; -moz-box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; -o-box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; -ms-box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d ; opacity: 1.0; -webkit-transform: rotate(3deg) translate(0px, -4px); -moz-transform: rotate(3deg) translate(0px, -4px); -o-transform: rotate(3deg) translate(0px, -4px); -ms-transform: rotate(3deg) translate(0px, -4px); transform: rotate(3deg) translate(0px, -4px) ; } +/* Icons */ +.icon-profile { font-size: 6px; top: 0em; -webkit-border-radius: 0.7em 0.7em 0 0; -moz-border-radius: 0.7em 0.7em 0 0; -o-border-radius: 0.7em 0.7em 0 0; -ms-border-radius: 0.7em 0.7em 0 0; border-radius: 0.7em 0.7em 0 0 ; background: #FFFFFF; width: 1.5em; height: 0.7em; position: relative; display: inline-block; margin-right: 4px } +.icon-profile::before { position: absolute; content: ""; top: -1em; left: 0.38em; width: 0.8em; height: 0.85em; -webkit-border-radius: 50%; -moz-border-radius: 50%; -o-border-radius: 50%; -ms-border-radius: 50%; border-radius: 50% ; background: #FFFFFF } + /* Animations */ @keyframes flip { @@ -161,8 +175,9 @@ a { color: black } 100% { opacity: 0 } } + /* Print styles */ @media print { #inner-iframe { position: fixed; } .progressbar, .fixbutton, .notifications, .loadingscreen { visibility: hidden; } -} +} \ No newline at end of file diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index f0ad9b2b..5b5559ac 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -595,7 +595,6 @@ jQuery.extend( jQuery.easing, if (timeout == null) { timeout = 0; } - this.log(id, type, body, timeout); _ref = $(".notification-" + id); for (_i = 0, _len = _ref.length; _i < _len; _i++) { elem = _ref[_i]; @@ -644,16 +643,15 @@ jQuery.extend( jQuery.easing, "width": width }, 700, "easeInOutCubic"); $(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000); - $(".close", elem).on("click", (function(_this) { + $(".close, .button", elem).on("click", (function(_this) { return function() { _this.close(elem); return false; }; })(this)); - return $(".button", elem).on("click", (function(_this) { + return $(".select", elem).on("click", (function(_this) { return function() { - _this.close(elem); - return false; + return _this.close(elem); }; })(this)); }; @@ -683,6 +681,7 @@ jQuery.extend( jQuery.easing, }).call(this); + /* ---- src/Ui/media/Sidebar.coffee ---- */ @@ -742,12 +741,13 @@ jQuery.extend( jQuery.easing, (function() { - var Wrapper, ws_url, + var Wrapper, origin, proto, ws_url, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __slice = [].slice; Wrapper = (function() { function Wrapper(ws_url) { + this.gotoSite = __bind(this.gotoSite, this); this.setSizeLimit = __bind(this.setSizeLimit, this); this.onLoad = __bind(this.onLoad, this); this.onCloseWebsocket = __bind(this.onCloseWebsocket, this); @@ -785,6 +785,12 @@ jQuery.extend( jQuery.easing, } }; })(this)); + + /*setInterval (-> + console.log document.hasFocus() + ), 1000 + */ + $("#inner-iframe").focus(); this; } @@ -831,6 +837,11 @@ jQuery.extend( jQuery.easing, }); return this.wrapperWsInited = true; } + } else if (cmd === "innerLoaded") { + if (window.location.hash) { + $("#inner-iframe")[0].src += window.location.hash; + return this.log("Added hash to location", $("#inner-iframe")[0].src); + } } else if (cmd === "wrapperNotification") { return this.actionNotification(message); } else if (cmd === "wrapperConfirm") { @@ -1032,9 +1043,6 @@ jQuery.extend( jQuery.easing, "cmd": "wrapperReady" }); } - if (window.location.hash) { - $("#inner-iframe")[0].src += window.location.hash; - } if (this.ws.ws.readyState === 1 && !this.site_info) { return this.reloadSiteInfo(); } else if (this.site_info && (((_ref = this.site_info.content) != null ? _ref.title : void 0) != null)) { @@ -1163,6 +1171,18 @@ jQuery.extend( jQuery.easing, return false; }; + Wrapper.prototype.isProxyRequest = function() { + return window.location.pathname === "/"; + }; + + Wrapper.prototype.gotoSite = function(elem) { + var href; + href = $(elem).attr("href"); + if (this.isProxyRequest()) { + return $(elem).attr("href", "http://zero" + href); + } + }; + Wrapper.prototype.log = function() { var args; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; @@ -1173,15 +1193,22 @@ jQuery.extend( jQuery.easing, })(); - var origin = window.server_url || window.location.origin; - var proto; - if (origin.indexOf('https:') === 0) { - proto = { ws: 'wss', ht: 'https' }; + origin = window.server_url || window.location.origin; + + if (origin.indexOf("https:") === 0) { + proto = { + ws: 'wss', + http: 'https' + }; } else { - proto = { ws: 'ws', ht: 'http' }; + proto = { + ws: 'ws', + http: 'http' + }; } - ws_url = proto.ws + ":" + (origin.replace(proto.ht + ':', '')) + "/Websocket?wrapper_key=" + window.wrapper_key; + + ws_url = proto.ws + ":" + origin.replace(proto.http + ":", "") + "/Websocket?wrapper_key=" + window.wrapper_key; window.wrapper = new Wrapper(ws_url); -}).call(this); +}).call(this); \ No newline at end of file diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 3a888067..9a1a3dc0 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -11,6 +11,12 @@ + +
@@ -40,15 +46,17 @@ - + - - - - - - + diff --git a/src/User/User.py b/src/User/User.py index 65f64c13..1d9f3a0a 100644 --- a/src/User/User.py +++ b/src/User/User.py @@ -4,17 +4,19 @@ from Plugin import PluginManager @PluginManager.acceptPlugins class User(object): - def __init__(self, master_address=None, master_seed=None): + def __init__(self, master_address=None, master_seed=None, data={}): if master_seed: self.master_seed = master_seed self.master_address = CryptBitcoin.privatekeyToAddress(self.master_seed) elif master_address: self.master_address = master_address - self.master_seed = None + self.master_seed = data.get("master_seed") else: self.master_seed = CryptBitcoin.newSeed() self.master_address = CryptBitcoin.privatekeyToAddress(self.master_seed) - self.sites = {} + self.sites = data.get("sites", {}) + self.certs = data.get("certs", {}) + self.log = logging.getLogger("User:%s" % self.master_address) @@ -22,10 +24,10 @@ class User(object): def save(self): users = json.load(open("data/users.json")) if not self.master_address in users: users[self.master_address] = {} # Create if not exits - user_data = users[self.master_address] if self.master_seed: user_data["master_seed"] = self.master_seed user_data["sites"] = self.sites + user_data["certs"] = self.certs open("data/users.json", "w").write(json.dumps(users, indent=2, sort_keys=True)) self.log.debug("Saved") @@ -50,14 +52,66 @@ class User(object): # Get BIP32 address from site address # Return: BIP32 auth address def getAuthAddress(self, address, create=True): - return self.getSiteData(address, create)["auth_address"] + cert = self.getCert(address) + if cert: + return cert["auth_address"] + else: + return self.getSiteData(address, create)["auth_address"] def getAuthPrivatekey(self, address, create=True): - return self.getSiteData(address, create)["auth_privatekey"] + cert = self.getCert(address) + if cert: + return cert["auth_privatekey"] + else: + return self.getSiteData(address, create)["auth_privatekey"] - # Set user attributes from dict - def setData(self, data): - for key, val in data.items(): - setattr(self, key, val) + # Add cert for the user + def addCert(self, auth_address, domain, auth_type, auth_user_name, cert_sign): + domain = domain.lower() + auth_privatekey = [site["auth_privatekey"] for site in self.sites.values() if site["auth_address"] == auth_address][0] # Find privatekey by auth address + cert_node = { + "auth_address": auth_address, + "auth_privatekey": auth_privatekey, + "auth_type": auth_type, + "auth_user_name": auth_user_name, + "cert_sign": cert_sign + } + # Check if we have already cert for that domain and its not the same + if self.certs.get(domain) and self.certs[domain] != cert_node: + raise Exception("You already have certificate for this domain: %s/%s@%s" % (self.certs[domain]["auth_type"], self.certs[domain]["auth_user_name"], domain)) + elif self.certs.get(domain) == cert_node: # Same, not updated + return None + else: # Not exits yet, add + self.certs[domain] = cert_node + self.save() + return True + + + def setCert(self, address, domain): + site_data = self.getSiteData(address) + if domain: + site_data["cert"] = domain + else: + del site_data["cert"] + self.save() + return site_data + + + # Get cert for the site address + # Return: { "auth_address": ..., "auth_privatekey":..., "auth_type": "web", "auth_user_name": "nofish", "cert_sign": ... } or None + def getCert(self, address): + site_data = self.getSiteData(address, create=False) + if not site_data or not "cert" in site_data: return None # Site dont have cert + return self.certs.get(site_data["cert"]) + + + # Get cert user name for the site address + # Return: user@certprovider.bit or None + def getCertUserId(self, address): + site_data = self.getSiteData(address, create=False) + if not site_data or not "cert" in site_data: return None # Site dont have cert + cert = self.certs.get(site_data["cert"]) + if cert: + return cert["auth_user_name"]+"@"+site_data["cert"] \ No newline at end of file diff --git a/src/User/UserManager.py b/src/User/UserManager.py index 2f07c375..4b16b121 100644 --- a/src/User/UserManager.py +++ b/src/User/UserManager.py @@ -18,8 +18,7 @@ class UserManager(object): # Load new users for master_address, data in json.load(open("data/users.json")).items(): if master_address not in self.users: - user = User(master_address) - user.setData(data) + user = User(master_address, data=data) self.users[master_address] = user added += 1 user_found.append(master_address) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 18cecaa4..2f5d4c2b 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -62,7 +62,7 @@ class Worker: task["failed"].append(self.peer) self.task = None self.peer.hash_failed += 1 - if self.peer.hash_failed >= 3: # Broken peer + if self.peer.hash_failed >= max(len(self.manager.tasks), 3): # More fails than tasks number but atleast 3: Broken peer break task["workers_num"] -= 1 time.sleep(1) @@ -77,9 +77,17 @@ class Worker: self.thread = gevent.spawn(self.downloader) + # Skip current task + def skip(self): + self.manager.log.debug("%s: Force skipping" % self.key) + if self.thread: + self.thread.kill(exception=Debug.Notify("Worker stopped")) + self.start() + + # Force stop the worker def stop(self): - self.manager.log.debug("%s: Force stopping, thread" % self.key) + self.manager.log.debug("%s: Force stopping" % self.key) self.running = False if self.thread: self.thread.kill(exception=Debug.Notify("Worker stopped")) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 783d30f7..8f2dde35 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -32,18 +32,23 @@ class WorkerManager: # Clean up workers for worker in self.workers.values(): - if worker.task and worker.task["done"]: worker.stop() # Stop workers with task done + if worker.task and worker.task["done"]: worker.skip() # Stop workers with task done if not self.tasks: continue tasks = self.tasks[:] # Copy it so removing elements wont cause any problem for task in tasks: - if (task["time_started"] and time.time() >= task["time_started"]+60) or (time.time() >= task["time_added"]+60 and not self.workers): # Task taking too long time, or no peer after 60sec kill it - self.log.debug("Timeout, Cleaning up task: %s" % task) - # Clean up workers + if task["time_started"] and time.time() >= task["time_started"]+60: # Task taking too long time, skip it + self.log.debug("Timeout, Skipping: %s" % task) + # Skip to next file workers workers = self.findWorkers(task) - for worker in workers: - worker.stop() + if workers: + for worker in workers: + worker.skip() + else: + self.failTask(task) + elif time.time() >= task["time_added"]+60 and not self.workers: # No workers left + self.log.debug("Timeout, Cleanup task: %s" % task) # Remove task self.failTask(task) @@ -178,12 +183,13 @@ class WorkerManager: # Mark a task failed def failTask(self, task): - task["done"] = True - self.tasks.remove(task) # Remove from queue - self.site.onFileFail(task["inner_path"]) - task["evt"].set(False) - if not self.tasks: - self.started_task_num = 0 + if task in self.tasks: + task["done"] = True + self.tasks.remove(task) # Remove from queue + self.site.onFileFail(task["inner_path"]) + task["evt"].set(False) + if not self.tasks: + self.started_task_num = 0 # Mark a task done diff --git a/src/lib/opensslVerify/opensslVerify-alter.py b/src/lib/opensslVerify/opensslVerify-alter.py new file mode 100644 index 00000000..fadbb5ee --- /dev/null +++ b/src/lib/opensslVerify/opensslVerify-alter.py @@ -0,0 +1,393 @@ +# Code is borrowed from https://github.com/blocktrail/python-bitcoinlib +# Thanks! + +import base64, hashlib + +import ctypes +import ctypes.util +_bchr = chr +_bord = ord +try: + _ssl = ctypes.CDLL("src/lib/opensslVerify/libeay32.dll") +except: + _ssl = ctypes.cdll.LoadLibrary(ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or 'libeay32') + +import sys + +openssl_version = "%.9X" % _ssl.SSLeay() + + +# this specifies the curve used with ECDSA. +_NID_secp256k1 = 714 # from openssl/obj_mac.h + +# Thx to Sam Devlin for the ctypes magic 64-bit fix. +def _check_result (val, func, args): + if val == 0: + raise ValueError + else: + return ctypes.c_void_p(val) + +_ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p +_ssl.EC_KEY_new_by_curve_name.errcheck = _check_result + +# From openssl/ecdsa.h +class ECDSA_SIG_st(ctypes.Structure): + _fields_ = [("r", ctypes.c_void_p), + ("s", ctypes.c_void_p)] + +class CECKey: + """Wrapper around OpenSSL's EC_KEY""" + + POINT_CONVERSION_COMPRESSED = 2 + POINT_CONVERSION_UNCOMPRESSED = 4 + + def __init__(self): + self.k = _ssl.EC_KEY_new_by_curve_name(_NID_secp256k1) + + def __del__(self): + if _ssl: + _ssl.EC_KEY_free(self.k) + self.k = None + + def set_secretbytes(self, secret): + priv_key = _ssl.BN_bin2bn(secret, 32, _ssl.BN_new()) + group = _ssl.EC_KEY_get0_group(self.k) + pub_key = _ssl.EC_POINT_new(group) + ctx = _ssl.BN_CTX_new() + if not _ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx): + raise ValueError("Could not derive public key from the supplied secret.") + _ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx) + _ssl.EC_KEY_set_private_key(self.k, priv_key) + _ssl.EC_KEY_set_public_key(self.k, pub_key) + _ssl.EC_POINT_free(pub_key) + _ssl.BN_CTX_free(ctx) + return self.k + + def set_privkey(self, key): + self.mb = ctypes.create_string_buffer(key) + return _ssl.d2i_ECPrivateKey(ctypes.byref(self.k), ctypes.byref(ctypes.pointer(self.mb)), len(key)) + + def set_pubkey(self, key): + self.mb = ctypes.create_string_buffer(key) + return _ssl.o2i_ECPublicKey(ctypes.byref(self.k), ctypes.byref(ctypes.pointer(self.mb)), len(key)) + + def get_privkey(self): + size = _ssl.i2d_ECPrivateKey(self.k, 0) + mb_pri = ctypes.create_string_buffer(size) + _ssl.i2d_ECPrivateKey(self.k, ctypes.byref(ctypes.pointer(mb_pri))) + return mb_pri.raw + + def get_pubkey(self): + size = _ssl.i2o_ECPublicKey(self.k, 0) + mb = ctypes.create_string_buffer(size) + _ssl.i2o_ECPublicKey(self.k, ctypes.byref(ctypes.pointer(mb))) + return mb.raw + + def get_raw_ecdh_key(self, other_pubkey): + ecdh_keybuffer = ctypes.create_string_buffer(32) + r = _ssl.ECDH_compute_key(ctypes.pointer(ecdh_keybuffer), 32, + _ssl.EC_KEY_get0_public_key(other_pubkey.k), + self.k, 0) + if r != 32: + raise Exception('CKey.get_ecdh_key(): ECDH_compute_key() failed') + return ecdh_keybuffer.raw + + def get_ecdh_key(self, other_pubkey, kdf=lambda k: hashlib.sha256(k).digest()): + # FIXME: be warned it's not clear what the kdf should be as a default + r = self.get_raw_ecdh_key(other_pubkey) + return kdf(r) + + def sign(self, hash): + if not isinstance(hash, bytes): + raise TypeError('Hash must be bytes instance; got %r' % hash.__class__) + if len(hash) != 32: + raise ValueError('Hash must be exactly 32 bytes long') + + sig_size0 = ctypes.c_uint32() + sig_size0.value = _ssl.ECDSA_size(self.k) + mb_sig = ctypes.create_string_buffer(sig_size0.value) + result = _ssl.ECDSA_sign(0, hash, len(hash), mb_sig, ctypes.byref(sig_size0), self.k) + assert 1 == result + if bitcoin.core.script.IsLowDERSignature(mb_sig.raw[:sig_size0.value]): + return mb_sig.raw[:sig_size0.value] + else: + return self.signature_to_low_s(mb_sig.raw[:sig_size0.value]) + + def sign_compact(self, hash): + if not isinstance(hash, bytes): + raise TypeError('Hash must be bytes instance; got %r' % hash.__class__) + if len(hash) != 32: + raise ValueError('Hash must be exactly 32 bytes long') + + sig_size0 = ctypes.c_uint32() + sig_size0.value = _ssl.ECDSA_size(self.k) + mb_sig = ctypes.create_string_buffer(sig_size0.value) + result = _ssl.ECDSA_sign(0, hash, len(hash), mb_sig, ctypes.byref(sig_size0), self.k) + assert 1 == result + + if bitcoin.core.script.IsLowDERSignature(mb_sig.raw[:sig_size0.value]): + sig = mb_sig.raw[:sig_size0.value] + else: + sig = self.signature_to_low_s(mb_sig.raw[:sig_size0.value]) + + sig = bitcoin.core.DERSignature.deserialize(sig) + + r_val = sig.r + s_val = sig.s + + # assert that the r and s are less than 32 long, excluding leading 0s + assert len(r_val) <= 32 or r_val[0:-32] == b'\x00' + assert len(s_val) <= 32 or s_val[0:-32] == b'\x00' + + # ensure r and s are always 32 chars long by 0padding + r_val = ((b'\x00' * 32) + r_val)[-32:] + s_val = ((b'\x00' * 32) + s_val)[-32:] + + # tmp pubkey of self, but always compressed + pubkey = CECKey() + pubkey.set_pubkey(self.get_pubkey()) + pubkey.set_compressed(True) + + # bitcoin core does <4, but I've seen other places do <2 and I've never seen a i > 1 so far + for i in range(0, 4): + cec_key = CECKey() + cec_key.set_compressed(True) + + result = cec_key.recover(r_val, s_val, hash, len(hash), i, 1) + if result == 1: + if cec_key.get_pubkey() == pubkey.get_pubkey(): + return r_val + s_val, i + + raise ValueError + + def signature_to_low_s(self, sig): + der_sig = ECDSA_SIG_st() + _ssl.d2i_ECDSA_SIG(ctypes.byref(ctypes.pointer(der_sig)), ctypes.byref(ctypes.c_char_p(sig)), len(sig)) + group = _ssl.EC_KEY_get0_group(self.k) + order = _ssl.BN_new() + halforder = _ssl.BN_new() + ctx = _ssl.BN_CTX_new() + _ssl.EC_GROUP_get_order(group, order, ctx) + _ssl.BN_rshift1(halforder, order) + + # Verify that s is over half the order of the curve before we actually subtract anything from it + if _ssl.BN_cmp(der_sig.s, halforder) > 0: + _ssl.BN_sub(der_sig.s, order, der_sig.s) + + _ssl.BN_free(halforder) + _ssl.BN_free(order) + _ssl.BN_CTX_free(ctx) + + derlen = _ssl.i2d_ECDSA_SIG(ctypes.pointer(der_sig), 0) + if derlen == 0: + _ssl.ECDSA_SIG_free(der_sig) + return None + new_sig = ctypes.create_string_buffer(derlen) + _ssl.i2d_ECDSA_SIG(ctypes.pointer(der_sig), ctypes.byref(ctypes.pointer(new_sig))) + _ssl.BN_free(der_sig.r) + _ssl.BN_free(der_sig.s) + + return new_sig.raw + + def verify(self, hash, sig): + """Verify a DER signature""" + if not sig: + return false + + # New versions of OpenSSL will reject non-canonical DER signatures. de/re-serialize first. + norm_sig = ctypes.c_void_p(0) + _ssl.d2i_ECDSA_SIG(ctypes.byref(norm_sig), ctypes.byref(ctypes.c_char_p(sig)), len(sig)) + + derlen = _ssl.i2d_ECDSA_SIG(norm_sig, 0) + if derlen == 0: + _ssl.ECDSA_SIG_free(norm_sig) + return false + + norm_der = ctypes.create_string_buffer(derlen) + _ssl.i2d_ECDSA_SIG(norm_sig, ctypes.byref(ctypes.pointer(norm_der))) + _ssl.ECDSA_SIG_free(norm_sig) + + # -1 = error, 0 = bad sig, 1 = good + return _ssl.ECDSA_verify(0, hash, len(hash), norm_der, derlen, self.k) == 1 + + def set_compressed(self, compressed): + if compressed: + form = self.POINT_CONVERSION_COMPRESSED + else: + form = self.POINT_CONVERSION_UNCOMPRESSED + _ssl.EC_KEY_set_conv_form(self.k, form) + + def recover(self, sigR, sigS, msg, msglen, recid, check): + """ + Perform ECDSA key recovery (see SEC1 4.1.6) for curves over (mod p)-fields + recid selects which key is recovered + if check is non-zero, additional checks are performed + """ + i = int(recid / 2) + + r = None + s = None + ctx = None + R = None + O = None + Q = None + + assert len(sigR) == 32, len(sigR) + assert len(sigS) == 32, len(sigS) + + try: + r = _ssl.BN_bin2bn(bytes(sigR), len(sigR), _ssl.BN_new()) + s = _ssl.BN_bin2bn(bytes(sigS), len(sigS), _ssl.BN_new()) + + group = _ssl.EC_KEY_get0_group(self.k) + ctx = _ssl.BN_CTX_new() + order = _ssl.BN_CTX_get(ctx) + ctx = _ssl.BN_CTX_new() + + if not _ssl.EC_GROUP_get_order(group, order, ctx): + return -2 + + x = _ssl.BN_CTX_get(ctx) + if not _ssl.BN_copy(x, order): + return -1 + if not _ssl.BN_mul_word(x, i): + return -1 + if not _ssl.BN_add(x, x, r): + return -1 + + field = _ssl.BN_CTX_get(ctx) + if not _ssl.EC_GROUP_get_curve_GFp(group, field, None, None, ctx): + return -2 + + if _ssl.BN_cmp(x, field) >= 0: + return 0 + + R = _ssl.EC_POINT_new(group) + if R is None: + return -2 + if not _ssl.EC_POINT_set_compressed_coordinates_GFp(group, R, x, recid % 2, ctx): + return 0 + + if check: + O = _ssl.EC_POINT_new(group) + if O is None: + return -2 + if not _ssl.EC_POINT_mul(group, O, None, R, order, ctx): + return -2 + if not _ssl.EC_POINT_is_at_infinity(group, O): + return 0 + + Q = _ssl.EC_POINT_new(group) + if Q is None: + return -2 + + n = _ssl.EC_GROUP_get_degree(group) + e = _ssl.BN_CTX_get(ctx) + if not _ssl.BN_bin2bn(msg, msglen, e): + return -1 + + if 8 * msglen > n: + _ssl.BN_rshift(e, e, 8 - (n & 7)) + + zero = _ssl.BN_CTX_get(ctx) + # if not _ssl.BN_zero(zero): + # return -1 + if not _ssl.BN_mod_sub(e, zero, e, order, ctx): + return -1 + rr = _ssl.BN_CTX_get(ctx) + if not _ssl.BN_mod_inverse(rr, r, order, ctx): + return -1 + sor = _ssl.BN_CTX_get(ctx) + if not _ssl.BN_mod_mul(sor, s, rr, order, ctx): + return -1 + eor = _ssl.BN_CTX_get(ctx) + if not _ssl.BN_mod_mul(eor, e, rr, order, ctx): + return -1 + if not _ssl.EC_POINT_mul(group, Q, eor, R, sor, ctx): + return -2 + + if not _ssl.EC_KEY_set_public_key(self.k, Q): + return -2 + + return 1 + finally: + if r: _ssl.BN_free(r) + if s: _ssl.BN_free(s) + if ctx: _ssl.BN_CTX_free(ctx) + if R: _ssl.EC_POINT_free(R) + if O: _ssl.EC_POINT_free(O) + if Q: _ssl.EC_POINT_free(Q) + + +def recover_compact(hash, sig): + """Recover a public key from a compact signature.""" + if len(sig) != 65: + raise ValueError("Signature should be 65 characters, not [%d]" % (len(sig), )) + + recid = (_bord(sig[0]) - 27) & 3 + compressed = (_bord(sig[0]) - 27) & 4 != 0 + + cec_key = CECKey() + cec_key.set_compressed(compressed) + + sigR = sig[1:33] + sigS = sig[33:65] + + result = cec_key.recover(sigR, sigS, hash, len(hash), recid, 0) + + if result < 1: + return False + + pubkey = cec_key.get_pubkey() + + return pubkey + +def encode(val, base, minlen=0): + base, minlen = int(base), int(minlen) + code_string = ''.join([chr(x) for x in range(256)]) + result = "" + while val > 0: + result = code_string[val % base] + result + val //= base + return code_string[0] * max(minlen - len(result), 0) + result + +def num_to_var_int(x): + x = int(x) + if x < 253: return chr(x) + elif x < 65536: return chr(253)+encode(x, 256, 2)[::-1] + elif x < 4294967296: return chr(254) + encode(x, 256, 4)[::-1] + else: return chr(255) + encode(x, 256, 8)[::-1] + + +def msg_magic(message): + return "\x18Bitcoin Signed Message:\n" + num_to_var_int( len(message) ) + message + + +def getMessagePubkey(message, sig): + message = msg_magic(message) + hash = hashlib.sha256(hashlib.sha256(message).digest()).digest() + sig = base64.b64decode(sig) + + pubkey = recover_compact(hash, sig) + return pubkey + +def test(): + sign = "HGbib2kv9gm9IJjDt1FXbXFczZi35u0rZR3iPUIt5GglDDCeIQ7v8eYXVNIaLoJRI4URGZrhwmsYQ9aVtRTnTfQ=" + pubkey = "044827c756561b8ef6b28b5e53a000805adbf4938ab82e1c2b7f7ea16a0d6face9a509a0a13e794d742210b00581f3e249ebcc705240af2540ea19591091ac1d41" + assert getMessagePubkey("hello", sign).encode("hex") == pubkey + +test() # Make sure it working right + +if __name__ == "__main__": + import time, sys + sys.path.append("..") + from pybitcointools import bitcoin as btctools + priv = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk" + address = "1N2XWu5soeppX2qUjvrf81rpdbShKJrjTr" + sign = btctools.ecdsa_sign("hello", priv) # HGbib2kv9gm9IJjDt1FXbXFczZi35u0rZR3iPUIt5GglDDCeIQ7v8eYXVNIaLoJRI4URGZrhwmsYQ9aVtRTnTfQ= + + s = time.time() + for i in range(100): + pubkey = getMessagePubkey("hello", sign) + verified = btctools.pubkey_to_address(pubkey) == address + print "100x Verified", verified, time.time()-s diff --git a/src/lib/opensslVerify/opensslVerify-alter2.py b/src/lib/opensslVerify/opensslVerify-alter2.py new file mode 100644 index 00000000..69b607e2 --- /dev/null +++ b/src/lib/opensslVerify/opensslVerify-alter2.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python +## +## @file contrib/verifymessage/python/terracoin_verifymessage.py +## @brief terracoin signed message verification sample script. +## @author unknown author ; found on pastebin +## + +import ctypes +import ctypes.util +import hashlib +import base64 +addrtype = 0 + +try: + ssl = ctypes.CDLL("src/lib/opensslVerify/libeay32.dll") +except: + ssl = ctypes.cdll.LoadLibrary(ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or 'libeay32') + +openssl_version = "%.9X" % ssl.SSLeay() + +NID_secp256k1 = 714 + +def check_result (val, func, args): + if val == 0: + raise ValueError + else: + return ctypes.c_void_p (val) + +ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p +ssl.EC_KEY_new_by_curve_name.errcheck = check_result + +POINT_CONVERSION_COMPRESSED = 2 +POINT_CONVERSION_UNCOMPRESSED = 4 + +__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +__b58base = len(__b58chars) + +def b58encode(v): + """ encode v, which is a string of bytes, to base58. + """ + + long_value = 0L + for (i, c) in enumerate(v[::-1]): + long_value += (256**i) * ord(c) + + result = '' + while long_value >= __b58base: + div, mod = divmod(long_value, __b58base) + result = __b58chars[mod] + result + long_value = div + result = __b58chars[long_value] + result + + # Bitcoin does a little leading-zero-compression: + # leading 0-bytes in the input become leading-1s + nPad = 0 + for c in v: + if c == '\0': nPad += 1 + else: break + + return (__b58chars[0]*nPad) + result + +def hash_160(public_key): + md = hashlib.new('ripemd160') + md.update(hashlib.sha256(public_key).digest()) + return md.digest() + +def hash_160_to_bc_address(h160): + vh160 = chr(addrtype) + h160 + h = Hash(vh160) + addr = vh160 + h[0:4] + return b58encode(addr) + +def public_key_to_bc_address(public_key): + h160 = hash_160(public_key) + return hash_160_to_bc_address(h160) + +def msg_magic(message): + #return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message + return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message + +def get_address(eckey): + size = ssl.i2o_ECPublicKey (eckey, 0) + mb = ctypes.create_string_buffer (size) + ssl.i2o_ECPublicKey (eckey, ctypes.byref (ctypes.pointer (mb))) + return public_key_to_bc_address(mb.raw) + +def Hash(data): + return hashlib.sha256(hashlib.sha256(data).digest()).digest() + +def bx(bn, size=32): + b = ctypes.create_string_buffer(size) + ssl.BN_bn2bin(bn, b); + return b.raw.encode('hex') + +def verify_message(address, signature, message): + pkey = ssl.EC_KEY_new_by_curve_name (NID_secp256k1) + eckey = SetCompactSignature(pkey, Hash(msg_magic(message)), signature) + addr = get_address(eckey) + return (address == addr) + +def SetCompactSignature(pkey, hash, signature): + sig = base64.b64decode(signature) + if len(sig) != 65: + raise BaseException("Wrong encoding") + nV = ord(sig[0]) + if nV < 27 or nV >= 35: + return False + if nV >= 31: + ssl.EC_KEY_set_conv_form(pkey, POINT_CONVERSION_COMPRESSED) + nV -= 4 + r = ssl.BN_bin2bn (sig[1:33], 32, ssl.BN_new()) + s = ssl.BN_bin2bn (sig[33:], 32, ssl.BN_new()) + eckey = ECDSA_SIG_recover_key_GFp(pkey, r, s, hash, len(hash), nV - 27, False); + return eckey + +def ECDSA_SIG_recover_key_GFp(eckey, r, s, msg, msglen, recid, check): + n = 0 + i = recid / 2 + + group = ssl.EC_KEY_get0_group(eckey) + ctx = ssl.BN_CTX_new() + ssl.BN_CTX_start(ctx) + order = ssl.BN_CTX_get(ctx) + ssl.EC_GROUP_get_order(group, order, ctx) + x = ssl.BN_CTX_get(ctx) + ssl.BN_copy(x, order); + ssl.BN_mul_word(x, i); + ssl.BN_add(x, x, r) + field = ssl.BN_CTX_get(ctx) + ssl.EC_GROUP_get_curve_GFp(group, field, None, None, ctx) + + if (ssl.BN_cmp(x, field) >= 0): + return False + + R = ssl.EC_POINT_new(group) + ssl.EC_POINT_set_compressed_coordinates_GFp(group, R, x, recid % 2, ctx) + + if check: + O = ssl.EC_POINT_new(group) + ssl.EC_POINT_mul(group, O, None, R, order, ctx) + if ssl.EC_POINT_is_at_infinity(group, O): + return False + + Q = ssl.EC_POINT_new(group) + n = ssl.EC_GROUP_get_degree(group) + e = ssl.BN_CTX_get(ctx) + ssl.BN_bin2bn(msg, msglen, e) + if 8 * msglen > n: ssl.BN_rshift(e, e, 8 - (n & 7)) + + + zero = ssl.BN_CTX_get(ctx) + ssl.BN_set_word(zero, 0) + ssl.BN_mod_sub(e, zero, e, order, ctx) + rr = ssl.BN_CTX_get(ctx); + ssl.BN_mod_inverse(rr, r, order, ctx) + sor = ssl.BN_CTX_get(ctx) + ssl.BN_mod_mul(sor, s, rr, order, ctx) + eor = ssl.BN_CTX_get(ctx) + ssl.BN_mod_mul(eor, e, rr, order, ctx) + ssl.EC_POINT_mul(group, Q, eor, R, sor, ctx) + ssl.EC_KEY_set_public_key(eckey, Q) + return eckey + + +def getMessagePubkey(message, sig): + pkey = ssl.EC_KEY_new_by_curve_name(NID_secp256k1) + eckey = SetCompactSignature(pkey, Hash(msg_magic(message)), sig) + size = ssl.i2o_ECPublicKey (eckey, 0) + mb = ctypes.create_string_buffer (size) + ssl.i2o_ECPublicKey (eckey, ctypes.byref (ctypes.pointer (mb))) + return mb.raw + +def test(): + sign = "HGbib2kv9gm9IJjDt1FXbXFczZi35u0rZR3iPUIt5GglDDCeIQ7v8eYXVNIaLoJRI4URGZrhwmsYQ9aVtRTnTfQ=" + pubkey = "044827c756561b8ef6b28b5e53a000805adbf4938ab82e1c2b7f7ea16a0d6face9a509a0a13e794d742210b00581f3e249ebcc705240af2540ea19591091ac1d41" + assert getMessagePubkey("hello", sign).encode("hex") == pubkey + +test() # Make sure it working right + +if __name__ == "__main__": + import time, os, sys + sys.path.append("..") + from pybitcointools import bitcoin as btctools + priv = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk" + address = "1N2XWu5soeppX2qUjvrf81rpdbShKJrjTr" + sign = btctools.ecdsa_sign("hello", priv) # HGbib2kv9gm9IJjDt1FXbXFczZi35u0rZR3iPUIt5GglDDCeIQ7v8eYXVNIaLoJRI4URGZrhwmsYQ9aVtRTnTfQ= + + s = time.time() + for i in range(100): + pubkey = getMessagePubkey("hello", sign) + verified = btctools.pubkey_to_address(pubkey) == address + print "100x Verified", verified, time.time()-s diff --git a/src/lib/opensslVerify/opensslVerify.py b/src/lib/opensslVerify/opensslVerify.py index 84e3a8b7..c7736a2e 100644 --- a/src/lib/opensslVerify/opensslVerify.py +++ b/src/lib/opensslVerify/opensslVerify.py @@ -1,346 +1,242 @@ -# Code is borrowed from https://github.com/blocktrail/python-bitcoinlib -# Thanks! +# via http://pastebin.com/H1XikJFd +# -*- Mode: Python -*- -import base64, hashlib +# This is a combination of http://pastebin.com/bQtdDzHx and +# https://github.com/Bitmessage/PyBitmessage/blob/master/src/pyelliptic/openssl.py +# that doesn't crash on OSX. +# Long message bug fixed by ZeroNet import ctypes import ctypes.util -_bchr = chr -_bord = ord +import hashlib +import base64 +addrtype = 0 + +class _OpenSSL: + """ + Wrapper for OpenSSL using ctypes + """ + def __init__(self, library): + """ + Build the wrapper + """ + try: + self._lib = ctypes.CDLL(library) + except: + self._lib = ctypes.cdll.LoadLibrary(library) + + self.pointer = ctypes.pointer + self.c_int = ctypes.c_int + self.byref = ctypes.byref + self.create_string_buffer = ctypes.create_string_buffer + + self.BN_new = self._lib.BN_new + self.BN_new.restype = ctypes.c_void_p + self.BN_new.argtypes = [] + + self.BN_copy = self._lib.BN_copy + self.BN_copy.restype = ctypes.c_void_p + self.BN_copy.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.BN_mul_word = self._lib.BN_mul_word + self.BN_mul_word.restype = ctypes.c_int + self.BN_mul_word.argtypes = [ctypes.c_void_p, ctypes.c_int] + + self.BN_set_word = self._lib.BN_set_word + self.BN_set_word.restype = ctypes.c_int + self.BN_set_word.argtypes = [ctypes.c_void_p, ctypes.c_int] + + self.BN_add = self._lib.BN_add + self.BN_add.restype = ctypes.c_void_p + self.BN_add.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_void_p] + + self.BN_mod_sub = self._lib.BN_mod_sub + self.BN_mod_sub.restype = ctypes.c_int + self.BN_mod_sub.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p] + + self.BN_mod_mul = self._lib.BN_mod_mul + self.BN_mod_mul.restype = ctypes.c_int + self.BN_mod_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p] + + self.BN_mod_inverse = self._lib.BN_mod_inverse + self.BN_mod_inverse.restype = ctypes.c_void_p + self.BN_mod_inverse.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p] + + self.BN_cmp = self._lib.BN_cmp + self.BN_cmp.restype = ctypes.c_int + self.BN_cmp.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.BN_bn2bin = self._lib.BN_bn2bin + self.BN_bn2bin.restype = ctypes.c_int + self.BN_bn2bin.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.BN_bin2bn = self._lib.BN_bin2bn + self.BN_bin2bn.restype = ctypes.c_void_p + self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p] + + self.EC_KEY_new_by_curve_name = self._lib.EC_KEY_new_by_curve_name + self.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p + self.EC_KEY_new_by_curve_name.argtypes = [ctypes.c_int] + + self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group + self.EC_KEY_get0_group.restype = ctypes.c_void_p + self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] + + self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key + self.EC_KEY_set_private_key.restype = ctypes.c_int + self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + self.EC_KEY_set_public_key = self._lib.EC_KEY_set_public_key + self.EC_KEY_set_public_key.restype = ctypes.c_int + self.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + self.EC_POINT_set_compressed_coordinates_GFp = self._lib.EC_POINT_set_compressed_coordinates_GFp + self.EC_POINT_set_compressed_coordinates_GFp.restype = ctypes.c_int + self.EC_POINT_set_compressed_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] + + self.EC_POINT_new = self._lib.EC_POINT_new + self.EC_POINT_new.restype = ctypes.c_void_p + self.EC_POINT_new.argtypes = [ctypes.c_void_p] + + self.EC_POINT_free = self._lib.EC_POINT_free + self.EC_POINT_free.restype = None + self.EC_POINT_free.argtypes = [ctypes.c_void_p] + + self.EC_GROUP_get_order = self._lib.EC_GROUP_get_order + self.EC_GROUP_get_order.restype = ctypes.c_void_p + self.EC_GROUP_get_order.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.EC_GROUP_get_degree = self._lib.EC_GROUP_get_degree + self.EC_GROUP_get_degree.restype = ctypes.c_void_p + self.EC_GROUP_get_degree.argtypes = [ctypes.c_void_p] + + self.EC_GROUP_get_curve_GFp = self._lib.EC_GROUP_get_curve_GFp + self.EC_GROUP_get_curve_GFp.restype = ctypes.c_void_p + self.EC_GROUP_get_curve_GFp.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p] + + self.EC_POINT_mul = self._lib.EC_POINT_mul + self.EC_POINT_mul.restype = ctypes.c_int + self.EC_POINT_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key + self.EC_KEY_set_private_key.restype = ctypes.c_int + self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + self.EC_KEY_set_conv_form = self._lib.EC_KEY_set_conv_form + self.EC_KEY_set_conv_form.restype = None + self.EC_KEY_set_conv_form.argtypes = [ctypes.c_void_p, + ctypes.c_int] + + self.BN_CTX_new = self._lib.BN_CTX_new + self._lib.BN_CTX_new.restype = ctypes.c_void_p + self._lib.BN_CTX_new.argtypes = [] + + self.BN_CTX_start = self._lib.BN_CTX_start + self._lib.BN_CTX_start.restype = ctypes.c_void_p + self._lib.BN_CTX_start.argtypes = [ctypes.c_void_p] + + self.BN_CTX_get = self._lib.BN_CTX_get + self._lib.BN_CTX_get.restype = ctypes.c_void_p + self._lib.BN_CTX_get.argtypes = [ctypes.c_void_p] + + self.ECDSA_sign = self._lib.ECDSA_sign + self.ECDSA_sign.restype = ctypes.c_int + self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.ECDSA_verify = self._lib.ECDSA_verify + self.ECDSA_verify.restype = ctypes.c_int + self.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] + + self.i2o_ECPublicKey = self._lib.i2o_ECPublicKey + self.i2o_ECPublicKey.restype = ctypes.c_void_p + self.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + + + try: - _ssl = ctypes.CDLL("src/lib/opensslVerify/libeay32.dll") + ssl = _OpenSSL("src/lib/opensslVerify/libeay32.dll") except: - _ssl = ctypes.cdll.LoadLibrary(ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or 'libeay32') + ssl = _OpenSSL(ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or 'libeay32') -import sys +openssl_version = "%.9X" % ssl._lib.SSLeay() -openssl_version = "%.9X" % _ssl.SSLeay() +NID_secp256k1 = 714 - -# this specifies the curve used with ECDSA. -_NID_secp256k1 = 714 # from openssl/obj_mac.h - -# Thx to Sam Devlin for the ctypes magic 64-bit fix. -def _check_result (val, func, args): +def check_result (val, func, args): if val == 0: raise ValueError else: - return ctypes.c_void_p(val) + return ctypes.c_void_p (val) -_ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p -_ssl.EC_KEY_new_by_curve_name.errcheck = _check_result +ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p +ssl.EC_KEY_new_by_curve_name.errcheck = check_result -# From openssl/ecdsa.h -class ECDSA_SIG_st(ctypes.Structure): - _fields_ = [("r", ctypes.c_void_p), - ("s", ctypes.c_void_p)] +POINT_CONVERSION_COMPRESSED = 2 +POINT_CONVERSION_UNCOMPRESSED = 4 -class CECKey: - """Wrapper around OpenSSL's EC_KEY""" +__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +__b58base = len(__b58chars) - POINT_CONVERSION_COMPRESSED = 2 - POINT_CONVERSION_UNCOMPRESSED = 4 +def b58encode(v): + """ encode v, which is a string of bytes, to base58. + """ - def __init__(self): - self.k = _ssl.EC_KEY_new_by_curve_name(_NID_secp256k1) + long_value = 0L + for (i, c) in enumerate(v[::-1]): + long_value += (256**i) * ord(c) - def __del__(self): - if _ssl: - _ssl.EC_KEY_free(self.k) - self.k = None + result = '' + while long_value >= __b58base: + div, mod = divmod(long_value, __b58base) + result = __b58chars[mod] + result + long_value = div + result = __b58chars[long_value] + result - def set_secretbytes(self, secret): - priv_key = _ssl.BN_bin2bn(secret, 32, _ssl.BN_new()) - group = _ssl.EC_KEY_get0_group(self.k) - pub_key = _ssl.EC_POINT_new(group) - ctx = _ssl.BN_CTX_new() - if not _ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx): - raise ValueError("Could not derive public key from the supplied secret.") - _ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx) - _ssl.EC_KEY_set_private_key(self.k, priv_key) - _ssl.EC_KEY_set_public_key(self.k, pub_key) - _ssl.EC_POINT_free(pub_key) - _ssl.BN_CTX_free(ctx) - return self.k + # Bitcoin does a little leading-zero-compression: + # leading 0-bytes in the input become leading-1s + nPad = 0 + for c in v: + if c == '\0': nPad += 1 + else: break - def set_privkey(self, key): - self.mb = ctypes.create_string_buffer(key) - return _ssl.d2i_ECPrivateKey(ctypes.byref(self.k), ctypes.byref(ctypes.pointer(self.mb)), len(key)) + return (__b58chars[0]*nPad) + result - def set_pubkey(self, key): - self.mb = ctypes.create_string_buffer(key) - return _ssl.o2i_ECPublicKey(ctypes.byref(self.k), ctypes.byref(ctypes.pointer(self.mb)), len(key)) +def hash_160(public_key): + md = hashlib.new('ripemd160') + md.update(hashlib.sha256(public_key).digest()) + return md.digest() - def get_privkey(self): - size = _ssl.i2d_ECPrivateKey(self.k, 0) - mb_pri = ctypes.create_string_buffer(size) - _ssl.i2d_ECPrivateKey(self.k, ctypes.byref(ctypes.pointer(mb_pri))) - return mb_pri.raw +def hash_160_to_bc_address(h160): + vh160 = chr(addrtype) + h160 + h = Hash(vh160) + addr = vh160 + h[0:4] + return b58encode(addr) - def get_pubkey(self): - size = _ssl.i2o_ECPublicKey(self.k, 0) - mb = ctypes.create_string_buffer(size) - _ssl.i2o_ECPublicKey(self.k, ctypes.byref(ctypes.pointer(mb))) - return mb.raw - - def get_raw_ecdh_key(self, other_pubkey): - ecdh_keybuffer = ctypes.create_string_buffer(32) - r = _ssl.ECDH_compute_key(ctypes.pointer(ecdh_keybuffer), 32, - _ssl.EC_KEY_get0_public_key(other_pubkey.k), - self.k, 0) - if r != 32: - raise Exception('CKey.get_ecdh_key(): ECDH_compute_key() failed') - return ecdh_keybuffer.raw - - def get_ecdh_key(self, other_pubkey, kdf=lambda k: hashlib.sha256(k).digest()): - # FIXME: be warned it's not clear what the kdf should be as a default - r = self.get_raw_ecdh_key(other_pubkey) - return kdf(r) - - def sign(self, hash): - if not isinstance(hash, bytes): - raise TypeError('Hash must be bytes instance; got %r' % hash.__class__) - if len(hash) != 32: - raise ValueError('Hash must be exactly 32 bytes long') - - sig_size0 = ctypes.c_uint32() - sig_size0.value = _ssl.ECDSA_size(self.k) - mb_sig = ctypes.create_string_buffer(sig_size0.value) - result = _ssl.ECDSA_sign(0, hash, len(hash), mb_sig, ctypes.byref(sig_size0), self.k) - assert 1 == result - if bitcoin.core.script.IsLowDERSignature(mb_sig.raw[:sig_size0.value]): - return mb_sig.raw[:sig_size0.value] - else: - return self.signature_to_low_s(mb_sig.raw[:sig_size0.value]) - - def sign_compact(self, hash): - if not isinstance(hash, bytes): - raise TypeError('Hash must be bytes instance; got %r' % hash.__class__) - if len(hash) != 32: - raise ValueError('Hash must be exactly 32 bytes long') - - sig_size0 = ctypes.c_uint32() - sig_size0.value = _ssl.ECDSA_size(self.k) - mb_sig = ctypes.create_string_buffer(sig_size0.value) - result = _ssl.ECDSA_sign(0, hash, len(hash), mb_sig, ctypes.byref(sig_size0), self.k) - assert 1 == result - - if bitcoin.core.script.IsLowDERSignature(mb_sig.raw[:sig_size0.value]): - sig = mb_sig.raw[:sig_size0.value] - else: - sig = self.signature_to_low_s(mb_sig.raw[:sig_size0.value]) - - sig = bitcoin.core.DERSignature.deserialize(sig) - - r_val = sig.r - s_val = sig.s - - # assert that the r and s are less than 32 long, excluding leading 0s - assert len(r_val) <= 32 or r_val[0:-32] == b'\x00' - assert len(s_val) <= 32 or s_val[0:-32] == b'\x00' - - # ensure r and s are always 32 chars long by 0padding - r_val = ((b'\x00' * 32) + r_val)[-32:] - s_val = ((b'\x00' * 32) + s_val)[-32:] - - # tmp pubkey of self, but always compressed - pubkey = CECKey() - pubkey.set_pubkey(self.get_pubkey()) - pubkey.set_compressed(True) - - # bitcoin core does <4, but I've seen other places do <2 and I've never seen a i > 1 so far - for i in range(0, 4): - cec_key = CECKey() - cec_key.set_compressed(True) - - result = cec_key.recover(r_val, s_val, hash, len(hash), i, 1) - if result == 1: - if cec_key.get_pubkey() == pubkey.get_pubkey(): - return r_val + s_val, i - - raise ValueError - - def signature_to_low_s(self, sig): - der_sig = ECDSA_SIG_st() - _ssl.d2i_ECDSA_SIG(ctypes.byref(ctypes.pointer(der_sig)), ctypes.byref(ctypes.c_char_p(sig)), len(sig)) - group = _ssl.EC_KEY_get0_group(self.k) - order = _ssl.BN_new() - halforder = _ssl.BN_new() - ctx = _ssl.BN_CTX_new() - _ssl.EC_GROUP_get_order(group, order, ctx) - _ssl.BN_rshift1(halforder, order) - - # Verify that s is over half the order of the curve before we actually subtract anything from it - if _ssl.BN_cmp(der_sig.s, halforder) > 0: - _ssl.BN_sub(der_sig.s, order, der_sig.s) - - _ssl.BN_free(halforder) - _ssl.BN_free(order) - _ssl.BN_CTX_free(ctx) - - derlen = _ssl.i2d_ECDSA_SIG(ctypes.pointer(der_sig), 0) - if derlen == 0: - _ssl.ECDSA_SIG_free(der_sig) - return None - new_sig = ctypes.create_string_buffer(derlen) - _ssl.i2d_ECDSA_SIG(ctypes.pointer(der_sig), ctypes.byref(ctypes.pointer(new_sig))) - _ssl.BN_free(der_sig.r) - _ssl.BN_free(der_sig.s) - - return new_sig.raw - - def verify(self, hash, sig): - """Verify a DER signature""" - if not sig: - return false - - # New versions of OpenSSL will reject non-canonical DER signatures. de/re-serialize first. - norm_sig = ctypes.c_void_p(0) - _ssl.d2i_ECDSA_SIG(ctypes.byref(norm_sig), ctypes.byref(ctypes.c_char_p(sig)), len(sig)) - - derlen = _ssl.i2d_ECDSA_SIG(norm_sig, 0) - if derlen == 0: - _ssl.ECDSA_SIG_free(norm_sig) - return false - - norm_der = ctypes.create_string_buffer(derlen) - _ssl.i2d_ECDSA_SIG(norm_sig, ctypes.byref(ctypes.pointer(norm_der))) - _ssl.ECDSA_SIG_free(norm_sig) - - # -1 = error, 0 = bad sig, 1 = good - return _ssl.ECDSA_verify(0, hash, len(hash), norm_der, derlen, self.k) == 1 - - def set_compressed(self, compressed): - if compressed: - form = self.POINT_CONVERSION_COMPRESSED - else: - form = self.POINT_CONVERSION_UNCOMPRESSED - _ssl.EC_KEY_set_conv_form(self.k, form) - - def recover(self, sigR, sigS, msg, msglen, recid, check): - """ - Perform ECDSA key recovery (see SEC1 4.1.6) for curves over (mod p)-fields - recid selects which key is recovered - if check is non-zero, additional checks are performed - """ - i = int(recid / 2) - - r = None - s = None - ctx = None - R = None - O = None - Q = None - - assert len(sigR) == 32, len(sigR) - assert len(sigS) == 32, len(sigS) - - try: - r = _ssl.BN_bin2bn(bytes(sigR), len(sigR), _ssl.BN_new()) - s = _ssl.BN_bin2bn(bytes(sigS), len(sigS), _ssl.BN_new()) - - group = _ssl.EC_KEY_get0_group(self.k) - ctx = _ssl.BN_CTX_new() - order = _ssl.BN_CTX_get(ctx) - ctx = _ssl.BN_CTX_new() - - if not _ssl.EC_GROUP_get_order(group, order, ctx): - return -2 - - x = _ssl.BN_CTX_get(ctx) - if not _ssl.BN_copy(x, order): - return -1 - if not _ssl.BN_mul_word(x, i): - return -1 - if not _ssl.BN_add(x, x, r): - return -1 - - field = _ssl.BN_CTX_get(ctx) - if not _ssl.EC_GROUP_get_curve_GFp(group, field, None, None, ctx): - return -2 - - if _ssl.BN_cmp(x, field) >= 0: - return 0 - - R = _ssl.EC_POINT_new(group) - if R is None: - return -2 - if not _ssl.EC_POINT_set_compressed_coordinates_GFp(group, R, x, recid % 2, ctx): - return 0 - - if check: - O = _ssl.EC_POINT_new(group) - if O is None: - return -2 - if not _ssl.EC_POINT_mul(group, O, None, R, order, ctx): - return -2 - if not _ssl.EC_POINT_is_at_infinity(group, O): - return 0 - - Q = _ssl.EC_POINT_new(group) - if Q is None: - return -2 - - n = _ssl.EC_GROUP_get_degree(group) - e = _ssl.BN_CTX_get(ctx) - if not _ssl.BN_bin2bn(msg, msglen, e): - return -1 - - if 8 * msglen > n: - _ssl.BN_rshift(e, e, 8 - (n & 7)) - - zero = _ssl.BN_CTX_get(ctx) - # if not _ssl.BN_zero(zero): - # return -1 - if not _ssl.BN_mod_sub(e, zero, e, order, ctx): - return -1 - rr = _ssl.BN_CTX_get(ctx) - if not _ssl.BN_mod_inverse(rr, r, order, ctx): - return -1 - sor = _ssl.BN_CTX_get(ctx) - if not _ssl.BN_mod_mul(sor, s, rr, order, ctx): - return -1 - eor = _ssl.BN_CTX_get(ctx) - if not _ssl.BN_mod_mul(eor, e, rr, order, ctx): - return -1 - if not _ssl.EC_POINT_mul(group, Q, eor, R, sor, ctx): - return -2 - - if not _ssl.EC_KEY_set_public_key(self.k, Q): - return -2 - - return 1 - finally: - if r: _ssl.BN_free(r) - if s: _ssl.BN_free(s) - if ctx: _ssl.BN_CTX_free(ctx) - if R: _ssl.EC_POINT_free(R) - if O: _ssl.EC_POINT_free(O) - if Q: _ssl.EC_POINT_free(Q) - - -def recover_compact(hash, sig): - """Recover a public key from a compact signature.""" - if len(sig) != 65: - raise ValueError("Signature should be 65 characters, not [%d]" % (len(sig), )) - - recid = (_bord(sig[0]) - 27) & 3 - compressed = (_bord(sig[0]) - 27) & 4 != 0 - - cec_key = CECKey() - cec_key.set_compressed(compressed) - - sigR = sig[1:33] - sigS = sig[33:65] - - result = cec_key.recover(sigR, sigS, hash, len(hash), recid, 0) - - if result < 1: - return False - - pubkey = cec_key.get_pubkey() - - return pubkey +def public_key_to_bc_address(public_key): + h160 = hash_160(public_key) + return hash_160_to_bc_address(h160) def encode(val, base, minlen=0): base, minlen = int(base), int(minlen) @@ -358,18 +254,107 @@ def num_to_var_int(x): elif x < 4294967296: return chr(254) + encode(x, 256, 4)[::-1] else: return chr(255) + encode(x, 256, 8)[::-1] - def msg_magic(message): - return "\x18Bitcoin Signed Message:\n" + num_to_var_int( len(message) ) + message + return "\x18Bitcoin Signed Message:\n" + num_to_var_int( len(message) ) + message + +def get_address(eckey): + size = ssl.i2o_ECPublicKey (eckey, 0) + mb = ctypes.create_string_buffer (size) + ssl.i2o_ECPublicKey (eckey, ctypes.byref (ctypes.pointer (mb))) + return public_key_to_bc_address(mb.raw) + +def Hash(data): + return hashlib.sha256(hashlib.sha256(data).digest()).digest() + +def bx(bn, size=32): + b = ctypes.create_string_buffer(size) + ssl.BN_bn2bin(bn, b); + return b.raw.encode('hex') + +def verify_message(address, signature, message): + pkey = ssl.EC_KEY_new_by_curve_name(NID_secp256k1) + eckey = SetCompactSignature(pkey, Hash(msg_magic(message)), signature) + addr = get_address(eckey) + return (address == addr) + +def SetCompactSignature(pkey, hash, signature): + sig = base64.b64decode(signature) + if len(sig) != 65: + raise BaseException("Wrong encoding") + nV = ord(sig[0]) + if nV < 27 or nV >= 35: + return False + if nV >= 31: + ssl.EC_KEY_set_conv_form(pkey, POINT_CONVERSION_COMPRESSED) + nV -= 4 + r = ssl.BN_bin2bn (sig[1:33], 32, None) + s = ssl.BN_bin2bn (sig[33:], 32, None) + eckey = ECDSA_SIG_recover_key_GFp(pkey, r, s, hash, len(hash), nV - 27, + False); + return eckey + +def ECDSA_SIG_recover_key_GFp(eckey, r, s, msg, msglen, recid, check): + n = 0 + i = recid / 2 + + group = ssl.EC_KEY_get0_group(eckey) + ctx = ssl.BN_CTX_new() + ssl.BN_CTX_start(ctx) + order = ssl.BN_CTX_get(ctx) + ssl.EC_GROUP_get_order(group, order, ctx) + x = ssl.BN_CTX_get(ctx) + ssl.BN_copy(x, order); + ssl.BN_mul_word(x, i); + ssl.BN_add(x, x, r) + field = ssl.BN_CTX_get(ctx) + ssl.EC_GROUP_get_curve_GFp(group, field, None, None, ctx) + + if (ssl.BN_cmp(x, field) >= 0): + return False + + R = ssl.EC_POINT_new(group) + ssl.EC_POINT_set_compressed_coordinates_GFp(group, R, x, recid % 2, ctx) + + if check: + O = ssl.EC_POINT_new(group) + ssl.EC_POINT_mul(group, O, None, R, order, ctx) + if ssl.EC_POINT_is_at_infinity(group, O): + return False + + Q = ssl.EC_POINT_new(group) + n = ssl.EC_GROUP_get_degree(group) + e = ssl.BN_CTX_get(ctx) + ssl.BN_bin2bn(msg, msglen, e) + if 8 * msglen > n: ssl.BN_rshift(e, e, 8 - (n & 7)) + + zero = ssl.BN_CTX_get(ctx) + ssl.BN_set_word(zero, 0) + ssl.BN_mod_sub(e, zero, e, order, ctx) + rr = ssl.BN_CTX_get(ctx); + ssl.BN_mod_inverse(rr, r, order, ctx) + sor = ssl.BN_CTX_get(ctx) + ssl.BN_mod_mul(sor, s, rr, order, ctx) + eor = ssl.BN_CTX_get(ctx) + ssl.BN_mod_mul(eor, e, rr, order, ctx) + ssl.EC_POINT_mul(group, Q, eor, R, sor, ctx) + ssl.EC_KEY_set_public_key(eckey, Q) + return eckey + +def close(): + import _ctypes + if "FreeLibrary" in dir(_ctypes): + _ctypes.FreeLibrary(ssl._lib._handle) + else: + _ctypes.dlclose(ssl._lib._handle) def getMessagePubkey(message, sig): - message = msg_magic(message) - hash = hashlib.sha256(hashlib.sha256(message).digest()).digest() - sig = base64.b64decode(sig) - - pubkey = recover_compact(hash, sig) - return pubkey + pkey = ssl.EC_KEY_new_by_curve_name(NID_secp256k1) + eckey = SetCompactSignature(pkey, Hash(msg_magic(message)), sig) + size = ssl.i2o_ECPublicKey (eckey, 0) + mb = ctypes.create_string_buffer (size) + ssl.i2o_ECPublicKey (eckey, ctypes.byref (ctypes.pointer (mb))) + return mb.raw def test(): sign = "HGbib2kv9gm9IJjDt1FXbXFczZi35u0rZR3iPUIt5GglDDCeIQ7v8eYXVNIaLoJRI4URGZrhwmsYQ9aVtRTnTfQ=" @@ -379,8 +364,10 @@ def test(): test() # Make sure it working right if __name__ == "__main__": - import time + import time, os, sys + sys.path.append("..") from pybitcointools import bitcoin as btctools + print "OpenSSL version %s" % openssl_version priv = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk" address = "1N2XWu5soeppX2qUjvrf81rpdbShKJrjTr" sign = btctools.ecdsa_sign("hello", priv) # HGbib2kv9gm9IJjDt1FXbXFczZi35u0rZR3iPUIt5GglDDCeIQ7v8eYXVNIaLoJRI4URGZrhwmsYQ9aVtRTnTfQ= diff --git a/src/lib/opensslVerify/stablityTest.py b/src/lib/opensslVerify/stablityTest.py new file mode 100644 index 00000000..d6b498c4 --- /dev/null +++ b/src/lib/opensslVerify/stablityTest.py @@ -0,0 +1,16 @@ +import opensslVerify, gevent, time +from gevent import monkey; monkey.patch_all(thread=False, ssl=False) + +def test(): + data = "A"*1024 + sign = "G2Jo8dDa+jqvJipft9E3kfrAxjESWLBpVtuGIiEBCD/UUyHmRMYNqnlWeOiaHHpja5LOP+U5CanRALfOjCSYIa8=" + for i in range(5*1000): + if i%1000 == 0: + print i, len(data) + data += data+"A" + time.sleep(0) + pub = opensslVerify.getMessagePubkey(data, sign) + + print repr(pub), len(data) + +gevent.joinall([gevent.spawn(test), gevent.spawn(test)]) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 5822c0d1..81fe96f0 100644 --- a/src/main.py +++ b/src/main.py @@ -111,7 +111,7 @@ class Actions: logging.info("Site created!") - def siteSign(self, address, privatekey=None, inner_path="content.json"): + def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False): from Site import Site logging.info("Signing site: %s..." % address) site = Site(address, allow_create = False) @@ -119,7 +119,9 @@ class Actions: if not privatekey: # If no privatekey in args then ask it now import getpass privatekey = getpass.getpass("Private key (input hidden):") - 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) + if succ and publish: + self.sitePublish(address, inner_path=inner_path) def siteVerify(self, address): @@ -128,16 +130,18 @@ class Actions: s = time.time() logging.info("Verifing site: %s..." % address) site = Site(address) + bad_files = [] for content_inner_path in site.content_manager.contents: logging.info("Verifing %s signature..." % content_inner_path) if site.content_manager.verifyFile(content_inner_path, site.storage.open(content_inner_path, "rb"), ignore_same=False) == True: logging.info("[OK] %s signed by address %s!" % (content_inner_path, address)) else: - logging.error("[ERROR] %s not signed by address %s!" % (content_inner_path, address)) + logging.error("[ERROR] %s: invalid file!" % content_inner_path) + bad_files += content_inner_path logging.info("Verifying site files...") - bad_files = site.storage.verifyFiles() + bad_files += site.storage.verifyFiles() if not bad_files: logging.info("[OK] All file sha512sum matches! (%.3fs)" % (time.time()-s)) else: @@ -197,17 +201,20 @@ class Actions: else: # Just ask the tracker logging.info("Gathering peers from tracker") site.announce() # Gather peers - site.publish(20, inner_path) # Push to 20 peers - time.sleep(3) - logging.info("Serving files (max 60s)...") - gevent.joinall([file_server_thread], timeout=60) - logging.info("Done.") + published = site.publish(20, inner_path) # Push to 20 peers + if published > 0: + time.sleep(3) + logging.info("Serving files (max 60s)...") + gevent.joinall([file_server_thread], timeout=60) + logging.info("Done.") + else: + logging.info("No peers found for this site, sitePublish command only works if you already have peers serving your site") # Crypto commands - def cryptoPrivatekeyToAddress(self, privatekey=None): + def cryptPrivatekeyToAddress(self, privatekey=None): from Crypt import CryptBitcoin if not privatekey: # If no privatekey in args then ask it now import getpass @@ -216,6 +223,11 @@ class Actions: print CryptBitcoin.privatekeyToAddress(privatekey) + def cryptSign(self, message, privatekey): + from Crypt import CryptBitcoin + print CryptBitcoin.sign(message, privatekey) + + # Peer def peerPing(self, peer_ip, peer_port): diff --git a/src/util/Noparallel.py b/src/util/Noparallel.py index 68521944..45946789 100644 --- a/src/util/Noparallel.py +++ b/src/util/Noparallel.py @@ -9,7 +9,7 @@ class Noparallel(object): # Only allow function running once in same time def __call__(self, func): def wrapper(*args, **kwargs): - key = (func, tuple(args), tuple(kwargs)) # Unique key for function including parameters + key = (func, tuple(args), tuple(kwargs.items())) # Unique key for function including parameters if key in self.threads: # Thread already running (if using blocking mode) thread = self.threads[key] if self.blocking: @@ -24,14 +24,13 @@ class Noparallel(object): # Only allow function running once in same time return thread else: # Thread not running thread = gevent.spawn(func, *args, **kwargs) # Spawning new thread + thread.link(lambda thread: self.cleanup(key, thread)) self.threads[key] = thread if self.blocking: # Wait for finish thread.join() ret = thread.value - if key in self.threads: del(self.threads[key]) # Allowing it to run again return ret else: # No blocking just return the thread - thread.link(lambda thread: self.cleanup(key, thread)) return thread wrapper.func_name = func.func_name diff --git a/src/util/RateLimit.py b/src/util/RateLimit.py index fc038e86..330fde6d 100644 --- a/src/util/RateLimit.py +++ b/src/util/RateLimit.py @@ -38,7 +38,7 @@ def callQueue(event): # Rate limit and delay function call if needed, If the function called again within the rate limit interval then previous queued call will be dropped # Return: Immedietly gevent thread def callAsync(event, allowed_again=10, func=None, *args, **kwargs): - if isAllowed(event): # Not called recently, call it now + if isAllowed(event, allowed_again): # Not called recently, call it now called(event) # print "Calling now" return gevent.spawn(func, *args, **kwargs) diff --git a/update.py b/update.py index 2400ab98..9b5f9793 100644 --- a/update.py +++ b/update.py @@ -56,7 +56,10 @@ def update(): if dest_dir != dest_path.strip("/"): data = zip.read(inner_path) - open(dest_path, 'wb').write(data) + try: + open(dest_path, 'wb').write(data) + except Exception, err: + print dest_path, err print "Done." diff --git a/zeronet.py b/zeronet.py index 4632274d..71ce0764 100644 --- a/zeronet.py +++ b/zeronet.py @@ -10,6 +10,13 @@ def main(): main.start() if main.update_after_shutdown: # Updater import update, sys, os, gc + # Try cleanup openssl + try: + if "lib.opensslVerify" in sys.modules: + sys.modules["lib.opensslVerify"].opensslVerify.close() + except Exception, err: + print "Error closing openssl", err + # Update update.update() @@ -24,7 +31,7 @@ def main(): except Exception, err: # Prevent closing import traceback traceback.print_exc() - raw_input("-- Error happened, press enter to close --") + traceback.print_exc(file=open("log/error.log", "a")) if main and main.update_after_shutdown: # Updater # Restart