def resolve_push_uri(ui, path, resolve_edition=True, fcphost=None, fcpport=None): """ Return a push URI for the given path. Raise util.Abort if unable to resolve identity or repository. :param resolve_edition: Defaults to True. If False, skips resolving the repository, uses the edition number 0. and does not modify the repository name. This is useful for finding a push URI for a repository that does not already exist. :param ui: For feedback. :param path: path describing a repo - nick@key/repo_name, where the identity is a local one. (Such that the insert URI is known.) """ # Expecting <id stuff>/repo_name wot_id, repo_name = path.split('/', 1) local_id = Local_WoT_ID(wot_id, fcpopts=get_fcpopts(fcphost=fcphost, fcpport=fcpport)) if resolve_edition: # TODO: find_repo should make it clearer that it returns a request URI, # and return a USK. repo = find_repo(ui, local_id, repo_name) # Request URI repo_uri = USK(repo) # Maintains name, edition. repo_uri.key = local_id.insert_uri.key return str(repo_uri) else: repo_uri = local_id.insert_uri.clone() repo_uri.name = repo_name repo_uri.edition = 0 return str(repo_uri)
def __init__(self, wot_identifier, fcpopts={}): """ Create a WoT_ID for a local identity matching the identifier. :type wot_identifier: str """ id_num, message = _get_local_identity(wot_identifier, fcpopts=fcpopts) self.insert_uri = USK(message['Replies.InsertURI{0}'.format(id_num)]) WoT_ID.__init__(self, None, None, id_num=id_num, message=message, fcpopts=fcpopts)
def update_repo_listing(ui, for_identity, fcphost=None, fcpport=None): """ Insert list of repositories published by the given identity. :type for_identity: Local_WoT_ID """ # TODO: Somehow store the edition, perhaps in ~/.infocalypse. WoT # properties are apparently not appropriate. cfg = Config.from_ui(ui) # TODO: WoT property containing repo list edition. Used when requesting. # Version number to support possible format changes. root = ET.Element('vcs', {'version': '0'}) ui.status("Updating repo listing for '%s'\n" % for_identity) for request_uri in build_repo_list(ui, for_identity): repo = ET.SubElement(root, 'repository', { 'vcs': VCS_NAME, }) repo.text = request_uri # TODO: Nonstandard IP and port from cfg node = fcp.FCPNode(**get_fcpopts(fcphost=fcphost, fcpport=fcpport)) atexit.register(node.shutdown) insert_uri = for_identity.insert_uri.clone() insert_uri.name = 'vcs' insert_uri.edition = cfg.get_repo_list_edition(for_identity) ui.status("Inserting with URI:\n{0}\n".format(insert_uri)) uri = node.put(uri=str(insert_uri), mimetype='application/xml', data=ET.tostring(root), priority=1) if uri is None: ui.warn("Failed to update repository listing.") else: ui.status("Updated repository listing:\n{0}\n".format(uri)) cfg.set_repo_list_edition(for_identity, USK(uri).edition) Config.to_file(cfg)
def fetch_edition(uri, fcphost=None, fcpport=None): """ Fetch a USK uri, following redirects. Change the uri edition to the one fetched. :type uri: USK """ node = fcp.FCPNode(**get_fcpopts(fcphost=fcphost, fcpport=fcpport)) atexit.register(node.shutdown) # Following a redirect automatically does not provide the edition used, # so manually following redirects is required. # TODO: Is there ever legitimately more than one redirect? try: return node.get(str(uri), priority=1) except fcp.FCPGetFailed, e: # Error code 27 is permanent redirect: there's a newer edition of # the USK. # https://wiki.freenetproject.org/FCPv2/GetFailed#Fetch_Error_Codes if not e.info['Code'] == 27: raise uri.edition = USK(e.info['RedirectURI']).edition return node.get(str(uri), priority=1)
def __init__(self, wot_identifier, truster, id_num=0, message=None, fcpopts={}): """ If using LCWoT, either the nickname prefix should be enough to be unambiguous, or failing that enough of the key. If using WoT, partial search is not supported, and the entire key must be specified. :type truster: Local_WoT_ID :type wot_identifier: str :param truster: Check trust list of this local identity. :param wot_identifier: Nickname and key, delimited by @. Either half can be omitted. """ # id_num and message are internal and used to allow constructing # a WoT_ID for a Local_WoT_ID. Their default values parse the first # (and only) identity described by an unspecified message, in which case # it queries WoT to produce one. is_local_identity = message is not None if not message: message = _get_identity(wot_identifier, truster, fcpopts=fcpopts) def get_attribute(attribute): return message['Replies.{0}{1}'.format(attribute, id_num)] self.nickname = get_attribute('Nickname') self.request_uri = USK(get_attribute('RequestURI')) self.identity_id = get_attribute('Identity') self.contexts = [] self.properties = {} context_prefix = "Replies.Contexts{0}.Context".format(id_num) property_prefix = "Replies.Properties{0}.Property".format(id_num) for key in message.iterkeys(): if key.startswith(context_prefix): self.contexts.append(message[key]) elif key.startswith(property_prefix) and key.endswith(".Name"): # ".Name" is 5 characters, before which is the number. num = key[len(property_prefix):-5] # Example: # Replies.Properties1.Property1.Name = IntroductionPuzzleCount # Replies.Properties1.Property1.Value = 10 name = message[key] value = message[property_prefix + num + '.Value'] # LCWoT returns many things with duplicates in properties, # so this conflict is something that can happen. Checking for # value conflict restricts the message to cases where it # actually has an effect. if name in self.properties and value != self.properties[name]: print( "WARNING: '{0}' has conflicting value as a property.". format(name)) self.properties[name] = value # Freemail addresses encode the public key hash with base32 instead of # base64 as WoT does. This is to be case insensitive because email # addresses are not case sensitive, so some clients may mangle case. # See: # https://github.com/zidel/Freemail/blob/v0.2.2.1/docs/spec/spec.tex#L32 if not 'Freemail' in self.contexts: self.freemail_address = None else: re_encode = b32encode(base64decode(self.identity_id)) # Remove trailing '=' padding. re_encode = re_encode.rstrip('=') # Freemail addresses are lower case. self.freemail_address = string.lower(self.nickname + '@' + re_encode + '.freemail') # TODO: Would it be preferable to use ui to obey quieting switches? if is_local_identity: print("Using local identity {0}".format(self)) else: print("Using identity {0}".format(self))
def infocalypse_create(ui_, repo, local_identity=None, **opts): """ Create a new Infocalypse repository in Freenet. :type local_identity: Local_WoT_ID :param local_identity: If specified the new repository is associated with that identity. """ params, stored_cfg = get_config_info(ui_, opts) if opts['uri'] and opts['wot']: ui_.warn("Please specify only one of --uri or --wot.\n") return elif opts['uri']: insert_uri = parse_repo_path(opts['uri']) elif opts['wot']: opts['wot'] = parse_repo_path(opts['wot']) nick_prefix, repo_name, repo_edition = opts['wot'].split('/', 2) if not repo_name.endswith('.R1') and not repo_name.endswith('.R0'): ui_.warn("Warning: Creating repository without redundancy. (R0 or" " R1)\n") from wot_id import Local_WoT_ID local_identity = Local_WoT_ID(nick_prefix) insert_uri = local_identity.insert_uri.clone() insert_uri.name = repo_name insert_uri.edition = repo_edition # Before passing along into execute_create(). insert_uri = str(insert_uri) else: ui_.warn("Please set the insert key with either --uri or --wot.\n") return # This is a WoT repository. if local_identity: # Prompt whether to replace in the case of conflicting names. from wot import build_repo_list request_usks = build_repo_list(ui_, local_identity) names = map(lambda x: USK(x).get_repo_name(), request_usks) new_name = USK(insert_uri).get_repo_name() if new_name in names: replace = ui_.prompt("A repository with the name '{0}' is already" " published by {1}. Replace it? [y/N]".format( new_name, local_identity), default='n') if replace.lower() != 'y': raise util.Abort("A repository with this name already exists.") # Remove the existing repository from each configuration section. existing_usk = request_usks[names.index(new_name)] existing_dir = None for directory, request_usk in stored_cfg.request_usks.iteritems(): if request_usk == existing_usk: if existing_dir: raise util.Abort("Configuration lists the same " "request USK multiple times.") existing_dir = directory assert existing_dir existing_hash = normalize(existing_usk) # Config file changes will not be written until a successful insert # below. del stored_cfg.version_table[existing_hash] del stored_cfg.request_usks[existing_dir] del stored_cfg.insert_usks[existing_hash] del stored_cfg.wot_identities[existing_hash] # Add "vcs" context. No-op if the identity already has it. msg_params = { 'Message': 'AddContext', 'Identity': local_identity.identity_id, 'Context': 'vcs' } import fcp import wot node = fcp.FCPNode(**wot.get_fcpopts(fcphost=opts["fcphost"], fcpport=opts["fcpport"])) atexit.register(node.shutdown) vcs_response =\ node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust", plugin_params=msg_params)[0] if vcs_response['header'] != 'FCPPluginReply' or\ 'Replies.Message' not in vcs_response or\ vcs_response['Replies.Message'] != 'ContextAdded': raise util.Abort( "Failed to add context. Got {0}\n.".format(vcs_response)) set_target_version(ui_, repo, opts, params, "Only inserting to version(s): %s\n") params['INSERT_URI'] = insert_uri inserted_to = execute_create(ui_, repo, params, stored_cfg) if inserted_to and local_identity: # creation returns a list of request URIs; use the first. stored_cfg.set_wot_identity(inserted_to[0], local_identity) Config.to_file(stored_cfg) import wot wot.update_repo_listing(ui_, local_identity, fcphost=opts["fcphost"], fcpport=opts["fcpport"])
def read_repo_listing(ui, identity, fcphost=None, fcpport=None): """ Read a repo listing for a given identity. Return a dictionary of repository request URIs keyed by name. :type identity: WoT_ID TODO: get host and port from config """ cfg = Config.from_ui(ui) uri = identity.request_uri.clone() uri.name = 'vcs' uri.edition = cfg.get_repo_list_edition(identity) # TODO: Set and read vcs edition property. ui.status("Fetching.\n") mime_type, repo_xml, msg = fetch_edition(uri, fcphost=fcphost, fcpport=fcpport) ui.status("Fetched {0}.\n".format(uri)) cfg.set_repo_list_edition(identity, uri.edition) Config.to_file(cfg) repositories = {} ambiguous = [] root = fromstring(repo_xml) for repository in root.iterfind('repository'): if repository.get('vcs') == VCS_NAME: uri = USK(repository.text) name = uri.get_repo_name() if name not in repositories: repositories[name] = uri else: existing = repositories[name] if uri.key == existing.key and uri.name == existing.name: # Different edition of same key and complete name. # Use the latest edition. if uri.edition > existing.edition: repositories[name] = uri else: # Different key or complete name. Later remove and give # warning. ambiguous.append(name) for name in ambiguous: # Same repo name but different key or exact name. ui.warn( "\"{0}\" refers ambiguously to multiple paths. Ignoring.\n".format( name)) del repositories[name] # TODO: Would it make sense to mention those for which multiple editions # are specified? It has no practical impact from this perspective, # and these problems should be pointed out (or prevented) for local repo # lists. for name in repositories.iterkeys(): ui.status("Found repository \"{0}\".\n".format(name)) # Convert values from USKs to strings - USKs are not expected elsewhere. for key in repositories.keys(): repositories[key] = str(repositories[key]) return repositories