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 _request_matching_identities_lcwot(truster, context="vcs", prefix=None, fcpopts={}): """ Return a response for a partial nickname request. """ nickname_prefix, key_prefix = _parse_name(prefix) # TODO: Support different FCP IP / port. node = fcp.FCPNode(**fcpopts) atexit.register(node.shutdown) # Test for GetIdentitiesByPartialNickname support. currently LCWoT-only. # src/main/java/plugins/WebOfTrust/fcp/GetIdentitiesByPartialNickname # TODO: LCWoT allows limiting by context; should we make sure otherwise? # Feature request for WoT: https://bugs.freenetproject.org/view.php?id=6184 # GetIdentitiesByPartialNickname does not support empty nicknames. if not nickname_prefix: raise util.Abort( "Partial matching in LCWoT does not support empty nicknames. Got {}" .format(prefix)) params = { 'Message': 'GetIdentitiesByPartialNickname', 'Truster': truster.identity_id, 'PartialNickname': nickname_prefix + '*', 'PartialID': key_prefix, 'MaxIdentities': 2, 'Context': 'vcs' } response = \ node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust", plugin_params=params)[0] if response['header'] != 'FCPPluginReply' or \ 'Replies.Message' not in response: raise util.Abort('Unexpected reply. Got {0}\n'.format(response)) return response
def _request_matching_identities(truster, context="vcs", prefix=None, fcpopts={}): """ Return a list of responses for all matching identities. """ node = fcp.FCPNode(**fcpopts) atexit.register(node.shutdown) params = { 'Message': 'GetIdentities', # GetIdentitiesByScore is much slower 'Truster': truster.identity_id } if context: params['Context'] = context response = node.fcpPluginMessage( plugin_name="plugins.WebOfTrust.WebOfTrust", plugin_params=params)[0] if response['header'] != 'FCPPluginReply' or \ 'Replies.Message' not in response: raise util.Abort('Unexpected reply. Got {0}\n'.format(response)) nIDs = int(response["Replies.Identities.Amount"]) def get_attribute(attribute, i, message): return message['Replies.Identities.{0}.{1}'.format(i, attribute)] responses = [] for i in range(nIDs): identifier = "@".join( (get_attribute("Nickname", i, response), get_attribute("Identity", i, response))) if not prefix or identifier.startswith(prefix): responses.append( _get_identity(identifier, truster, exact=True, fcpopts=fcpopts)) return responses
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)
import fcp # ------------------------------------------ # state where our FCP port is fcpHost = "127.0.0.1" # ------------------------------------------ # create a node connection object # # we're setting a relatively high verbosity so you # can see the traffic node = fcp.FCPNode(host=fcpHost, verbosity=fcp.DETAIL) # ----------------------------------------------- # now, perform a simple direct insert of a string # val = raw_input("Please enter a string to insert: ") # ksk = raw_input("Please enter a short KSK key name: ") val = "testinsert" ksk = "testinsertkey" + uuid.uuid4().hex uri = "KSK@" + ksk print "Inserting %s, containing '%s'" % (uri, val) # do the put - note that 'data=' inserts a string directly # note too that mimetype is optional, defaulting to text/plain
put/get ksk ssk usk dir freesite → data file async wrong node ip different IP Port """ import sys, os, tempfile, random, uuid import fcp fcpHost = "127.0.0.1" workdir = tempfile.mkdtemp() os.chdir(workdir) myid = str(uuid.uuid4().hex) with open("index.html", "w") as f: f.write("<html><head><title>Test</title></head><body>Test</body></html>\n") node = fcp.FCPNode(host=fcpHost, verbosity=fcp.FATAL) def genkey(*args, **kwds): ''' >>> public, private = genkey() >>> fcp.node.uriIsPrivate(public) False >>> fcp.node.uriIsPrivate(private) True ''' return node.genkey(*args, **kwds) def getUniqueId(*args, **kwds):
def _get_local_identity(wot_identifier, fcpopts={}): """ Internal. Return (id_number, FCP reply) from WoT for a local identity matching the identifier. Abort if anything but exactly one match is found. :type wot_identifier: str """ nickname_prefix, key_prefix = _parse_name(wot_identifier) node = fcp.FCPNode(**fcpopts) atexit.register(node.shutdown) response = \ node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust", plugin_params={'Message': 'GetOwnIdentities'})[0] if response['header'] != 'FCPPluginReply' or \ 'Replies.Message' not in response or \ response['Replies.Message'] != 'OwnIdentities': raise util.Abort("Unexpected reply. Got {0}\n.".format(response)) # Find nicknames starting with the supplied nickname prefix. prefix = 'Replies.Nickname' # Key: nickname, value (id_num, public key hash). matches = {} for key in response.iterkeys(): if key.startswith(prefix) and \ response[key].startswith(nickname_prefix): # Key is Replies.Nickname<number>, where number is used in # the other attributes returned for that identity. id_num = key[len(prefix):] nickname = response[key] pubkey_hash = response['Replies.Identity{0}'.format(id_num)] matches[nickname] = (id_num, pubkey_hash) # Remove matching nicknames not also matching the (possibly partial) # public key hash. for key in matches.keys(): # public key hash is second member of value tuple. if not matches[key][1].startswith(key_prefix): del matches[key] if len(matches) > 1: raise util.Abort("'{0}' matches more than one local identity.".format( wot_identifier)) if len(matches) == 0: raise util.Abort( "No local identities match '{0}'.".format(wot_identifier)) assert len(matches) == 1 # id_num is first member of value tuple. only_key = matches.keys()[0] id_num = matches[only_key][0] return id_num, response
def _get_identity(wot_identifier, truster, exact=False, fcpopts={}): """ Internal. Return an FCP reply from WoT for an identity on the truster's trust list matching the identifier. Abort if anything but exactly one match is found. :type wot_identifier: str :type truster: Local_WoT_ID :param exact: Whether to match the wot_identifier exactly or use it as prefix. """ nickname_prefix, key_prefix = _parse_name(wot_identifier) # TODO: Support different FCP IP / port. node = fcp.FCPNode(**fcpopts) atexit.register(node.shutdown) if not exact: # Test for GetIdentitiesByPartialNickname support. currently LCWoT-only. # src/main/java/plugins/WebOfTrust/fcp/GetIdentitiesByPartialNickname # TODO: LCWoT allows limiting by context; should we make sure otherwise? # Feature request for WoT: https://bugs.freenetproject.org/view.php?id=6184 # GetIdentitiesByPartialNickname does not support empty nicknames. try: response = _request_matching_identities_lcwot( truster, context="vcs", prefix=wot_identifier, fcpopts=fcpopts) if response['Replies.Message'] == 'Identities': matches = response['Replies.IdentitiesMatched'] else: raise util.Abort("WoT does not support partial matching.") except util.Abort: all_responses = _request_matching_identities(truster, prefix=wot_identifier, fcpopts=fcpopts) matches = len(all_responses) if matches: response = all_responses[0] if matches == 0: raise util.Abort( "No identities match '{0}'.".format(wot_identifier)) elif matches == 1: return response else: # TODO: Ask the user to choose interactively (select 1, 2, 3, ...) raise util.Abort( "'{0}' matches more than one identity.".format(wot_identifier)) # exact matching requested. The key_prefix must be the complete key. # key_prefix must be a complete key for the lookup to succeed. params = { 'Message': 'GetIdentity', 'Truster': truster.identity_id, 'Identity': key_prefix } response = \ node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust", plugin_params=params)[0] if response['Replies.Message'] == 'Error': # Searching by exact public key hash, not matching. raise util.Abort("No identity has the complete public key hash '{0}'. " "({1}). Error: {2}".format( key_prefix, wot_identifier, response.get('Replies.Message', ""))) # There should be only one result. # Depends on https://bugs.freenetproject.org/view.php?id=5729 return response
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 connect(ui, repo): """ Connect to the WebUI plugin to provide local support. TODO: Add command option handling (fcphost and fcpport). """ node = fcp.FCPNode() atexit.register(node.shutdown) ui.status("Connecting.\n") # TODO: Would it be worthwhile to have a wrapper that includes PLUGIN_NAME? # TODO: Where to document the spec? devnotes.txt? How to format? hi_there = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, plugin_params={ 'Message': 'Hello', 'VoidQuery': 'true' })[0] if hi_there['header'] == 'Error': raise util.Abort("The DVCS web UI plugin is not loaded.") if hi_there['Replies.Message'] == 'Error': # TODO: Debugging print hi_there raise util.Abort("Another VCS instance is already connected.") session_token = hi_there['Replies.SessionToken'] ui.status("Connected.\n") def disconnect(signum, frame): ui.status("Disconnecting.\n") node.fcpPluginMessage(plugin_name=PLUGIN_NAME, plugin_params={ 'Message': 'Disconnect', 'SessionToken': session_token }) sys.exit() # Send Disconnect on interrupt instead of waiting on timeout. signal(SIGINT, disconnect) def ping(): # Loop with delay. while True: pong = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, plugin_params={ 'Message': 'Ping', 'SessionToken': session_token })[0] if pong['Replies.Message'] == 'Error': raise util.Abort(pong['Replies.Description']) elif pong['Replies.Message'] != 'Pong': ui.warn("Got unrecognized Ping reply '{0}'.\n".format( pong['Replies.Message'])) # Wait for less than timeout threshold. In testing responses take # a little over a second. sleep(3.5) # Start self-perpetuating pinging in the background. t = threading.Timer(0.0, ping) # Daemon threads do not hold up the process exiting. Allows prompt # response to - for instance - SIGTERM. t.daemon = True t.start() while True: # Load the config each time - it could change. # TODO: Monitor config file for change events instead. cfg = Config.from_ui(ui) query_identifier = node._getUniqueId() # The event-querying is single-threaded, which makes things slow as # everything waits on the completion of the current operation. # Asynchronous code would require changes on the plugin side but # potentially have much lower latency. # TODO: Can wrap away PLUGIN_NAME, SessionToken, and QueryIdentifier? command = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, plugin_params={ 'Message': 'Ready', 'SessionToken': session_token, 'QueryIdentifier': query_identifier })[0] response = command['Replies.Message'] if response == 'Error': raise util.Abort(command['Replies.Description']) if response not in handlers: raise util.Abort("Unsupported query '{0}'\n".format(response)) ui.status("Got query: {0}\n".format(response)) # Handlers are indexed by the query message name, take the query # message, and return (result_name, plugin_params). result_name, plugin_params = handlers[response](command, cfg=cfg, ui=ui) plugin_params['Message'] = result_name plugin_params['QueryIdentifier'] = query_identifier plugin_params['SessionToken'] = session_token ack = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, plugin_params=plugin_params)[0] if ack['Replies.Message'] != "Ack": raise util.Abort("Received unexpected message instead of result " "acknowledgement:\n{0}\n".format(ack)) ui.status("Query complete.\n")
continue stat_fields.append(argfield) continue i = arg.find(".") if (-1 != i): host = arg continue stat_fields.append(arg) if (2 != len(stat_fields) and not list_fields_flag): print "Must specify two stat_fields when not using --list-fields" print usage() sys.exit(1) f = fcp.FCPNode(host=host, port=port) entry = f.refstats(WithVolatile=True) f.shutdown() if (list_fields_flag): keys = entry.keys() keys.sort() print "Volatile fields:" for key in keys: if (not key.startswith("volatile.")): continue print key[9:] print print "non-volatile fields:" for key in keys: if (key.startswith("volatile.")): continue
def __init__(self): self.fcpNode = fcp.FCPNode(host="localhost")