First pass support for fn-pull'ing with a usk hash value and command line trust map editing. e.g. hg fn-pull --hash 2220b02cf7ee hg fn-fmsread --trust --fmsid djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks --hash 2220b02cf7ee hg fn-fmsread --untrust --fmsid djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks --hash 2220b02cf7ee Still needs pylint cleanup and doc updates.
diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py --- a/infocalypse/__init__.py +++ b/infocalypse/__init__.py @@ -295,10 +295,11 @@ from infcmds import get_config_info, exe execute_push, execute_setup, execute_copy, execute_reinsert, \ execute_info -from fmscmds import execute_fmsread, execute_fmsnotify +from fmscmds import execute_fmsread, execute_fmsnotify, get_uri_from_hash from sitecmds import execute_putsite, execute_genkey from config import read_freesite_cfg +from validate import is_hex_string, is_fms_id def set_target_version(ui_, repo, opts, params, msg_fmt): """ INTERNAL: Update TARGET_VERSION in params. """ @@ -390,7 +391,19 @@ def infocalypse_pull(ui_, repo, **opts): """ Pull from an Infocalypse repository in Freenet. """ params, stored_cfg = get_config_info(ui_, opts) - request_uri = opts['uri'] + + if opts['hash']: + # Use FMS to lookup the uri from the repo hash. + if opts['uri'] != '': + ui_.warn("Ignoring --uri because --hash is set!\n") + if len(opts['hash']) != 1: + raise util.Abort("Only one --hash value is allowed.") + params['FMSREAD_HASH'] = opts['hash'][0] + params['FMSREAD_ONLYTRUSTED'] = bool(opts['onlytrusted']) + request_uri = get_uri_from_hash(ui_, repo, params, stored_cfg) + else: + request_uri = opts['uri'] + if request_uri == '': request_uri = stored_cfg.get_request_uri(repo.root) if not request_uri: @@ -444,6 +457,44 @@ def infocalypse_info(ui_, repo, **opts): params['REQUEST_URI'] = request_uri execute_info(ui_, params, stored_cfg) +def parse_trust_args(params, opts): + """ INTERNAL: Helper function to parse --hash and --fmsid. """ + if opts.get('hash', []) == []: + raise util.Abort("Use --hash to set the USK hash.") + if len(opts['hash']) != 1: + raise util.Abort("Only one --hash value is allowed.") + if not is_hex_string(opts['hash'][0]): + raise util.Abort("[%s] doesn't look like a USK hash." % + opts['hash'][0]) + + if opts.get('fmsid', []) == []: + raise util.Abort("Use --fmsid to set the FMS id.") + if len(opts['fmsid']) != 1: + raise util.Abort("Only one --fmsid value is allowed.") + if not is_fms_id(opts['fmsid'][0]): + raise util.Abort("[%s] doesn't look like an FMS id." + % opts['fmsid'][0]) + + params['FMSREAD_HASH'] = opts['hash'][0] + params['FMSREAD_FMSID'] = opts['fmsid'][0] + +def parse_fmsread_subcmd(params, opts): + """ INTERNAL: Parse subcommand for fmsread.""" + if opts['listall']: + params['FMSREAD'] = 'listall' + elif opts['list']: + params['FMSREAD'] = 'list' + elif opts['showtrust']: + params['FMSREAD'] = 'showtrust' + elif opts['trust']: + params['FMSREAD'] = 'trust' + parse_trust_args(params, opts) + elif opts['untrust']: + params['FMSREAD'] = 'untrust' + parse_trust_args(params, opts) + else: + params['FMSREAD'] = 'update' + def infocalypse_fmsread(ui_, repo, **opts): """ Read repository update information from fms. """ @@ -457,13 +508,7 @@ def infocalypse_fmsread(ui_, repo, **opt if not request_uri: ui_.status("There is no stored request URI for this repo.\n") request_uri = None - - if opts['listall']: - params['FMSREAD'] = 'listall' - elif opts['list']: - params['FMSREAD'] = 'list' - else: - params['FMSREAD'] = 'update' + parse_fmsread_subcmd(params, opts) params['DRYRUN'] = opts['dryrun'] params['REQUEST_URI'] = request_uri execute_fmsread(ui_, params, stored_cfg) @@ -555,7 +600,10 @@ NOSEARCH_OPT = [('', 'nosearch', None, ' cmdtable = { "fn-pull": (infocalypse_pull, - [('', 'uri', '', 'request URI to pull from'),] + [('', 'uri', '', 'request URI to pull from'), + ('', 'hash', [], 'repo hash of repository to pull from'), + ('', 'onlytrusted', None, 'only use repo announcements from ' + + 'known users')] + FCP_OPTS + NOSEARCH_OPT + AGGRESSIVE_OPT, @@ -596,9 +644,14 @@ cmdtable = { "fn-fmsread": (infocalypse_fmsread, [('', 'uri', '', 'request URI'), + ('', 'hash', [], 'repo hash to modify trust for'), + ('', 'fmsid', [], 'FMS id to modify trust for'), ('', 'list', None, 'show repo USKs from trusted ' + 'fms identities'), ('', 'listall', None, 'show all repo USKs'), + ('', 'showtrust', None, 'show the trust map'), + ('', 'trust', None, 'add an entry to the trust map'), + ('', 'untrust', None, 'remove an entry from the trust map'), ('', 'dryrun', None, "don't update the index cache"),], "[options]"), diff --git a/infocalypse/config.py b/infocalypse/config.py --- a/infocalypse/config.py +++ b/infocalypse/config.py @@ -20,7 +20,6 @@ Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks """ - import os import sys @@ -28,6 +27,9 @@ from fcpclient import get_usk_hash, is_u get_usk_for_usk_version from knownrepos import DEFAULT_TRUST, DEFAULT_GROUPS, \ DEFAULT_NOTIFICATION_GROUP + +from validate import is_hex_string, is_fms_id + from mercurial import util # Similar hack is used in fms.py. @@ -202,6 +204,15 @@ class Config: #def get_key_alias(self, alias, is_public): # pass + def trusted_notifiers(self, repo_hash): + """ Return the list of FMS ids trusted to modify the repo with + repository hash repo_hash. """ + ret = [] + for fms_id in self.fmsread_trust_map: + if repo_hash in self.fmsread_trust_map[fms_id]: + ret.append(fms_id) + return ret + @classmethod def update_defaults(cls, parser, cfg): """ INTERNAL: Helper function to simplify from_file. """ @@ -264,12 +275,21 @@ class Config: fields = parser.get('fmsread_trust_map', ordinal).strip().split('|') # REDFLAG: better validation for fms_id, hashes? - if fields[0].find('@') == -1: + if not is_fms_id(fields[0]): raise ValueError("%s doesn't look like an fms id." % fields[0]) if len(fields) < 2: raise ValueError("No USK hashes for fms id: %s?" % fields[0]) + for value in fields[1:]: + if not is_hex_string(value): + raise ValueError("%s doesn't look like a repo hash." % + value) + + if fields[0] in cfg.fmsread_trust_map: + raise ValueError(("%s appears more than once in the " + + "[fmsread_trust_map] section.") % + fields[0]) cfg.fmsread_trust_map[fields[0]] = tuple(fields[1:]) @@ -401,3 +421,48 @@ def read_freesite_cfg(ui_, repo, params, if not params['SITE_KEY'].startswith('SSK@'): raise util.Abort("Stored site key not an SSK?") +def known_hashes(trust_map): + """ Return all repo hashes in the trust map. """ + ret = set([]) + for fms_id in trust_map: + ret |= set(trust_map[fms_id]) + return tuple(ret) + +# REMEMBER that hashes are stored in a tuple not a list! +def trust_id_for_repo(trust_map, fms_id, repo_hash): + """ Accept index updates from an fms id for a repo.""" + assert is_fms_id(fms_id) + assert is_hex_string(repo_hash) + + hashes = trust_map.get(fms_id, ()) + if repo_hash in hashes: + return False + hashes = list(hashes) + hashes.append(repo_hash) + trust_map[fms_id] = tuple(hashes) + + return True + +# See above. +def untrust_id_for_repo(trust_map, fms_id, repo_hash): + """ Stop accepting index updates from an fms id for a repo.""" + assert is_fms_id(fms_id) + assert is_hex_string(repo_hash) + + if not fms_id in trust_map: + return False + + hashes = list(trust_map[fms_id]) + # Paranoia. There shouldn't be duplicates. + count = 0 + while repo_hash in hashes: + hashes.remove(repo_hash) + count += 1 + + if len(hashes) == 0: + del trust_map[fms_id] + return True + + trust_map[fms_id] = tuple(hashes) + return count > 0 + diff --git a/infocalypse/fms.py b/infocalypse/fms.py --- a/infocalypse/fms.py +++ b/infocalypse/fms.py @@ -26,8 +26,7 @@ import StringIO from fcpclient import get_usk_hash, get_version, is_usk_file, \ get_usk_for_usk_version -# Hmmm... This dependency doesn't really belong here. -from knownrepos import KNOWN_REPOS +from validate import is_hex_string # Similar HACK is used in config.py import knownrepos # Just need a module to read __file__ from @@ -95,14 +94,18 @@ class IFmsMessageSink: items is an nntplib xover items tuple. """ - raise NotImplementedError() + # Avoid pylint R0922 + # raise NotImplementedError() + pass def recv_fms_msg(self, group, items, lines): """ Handle an fms message. items is an nntplib xover items tuple. """ - raise NotImplementedError() + # Avoid pylint R0922 + # raise NotImplementedError() + pass def recv_msgs(fms_host, fms_port, msg_sink, groups): """ Read messages from fms. """ @@ -139,6 +142,10 @@ def recv_msgs(fms_host, fms_port, msg_si ############################################################ # Infocalypse specific stuff. ############################################################ +# REDFLAG: LATER, move this into fmscmd.py? + +# REDFLAG: Research, when exactly? +# Can sometimes see fms ids w/o human readable part. def clean_nym(fms_id): """ Returns the line noise part of an fms id, after the '@'. """ pos = fms_id.index('@') @@ -233,7 +240,6 @@ def parse(text, is_lines=False): announcements.sort() return (tuple(updates), tuple(announcements)) - def strip_names(trust_map): """ Returns a trust map without human readable names in the keys. """ clean = {} @@ -246,142 +252,202 @@ def strip_names(trust_map): + clean.get(cleaned, []))) return clean -# REDFLAG: Trust map ids are w/o names -# 'isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks', not -# 'djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks' -class USKIndexUpdateParser(IFmsMessageSink): - """ Class which accumulates USK index update notifications - from fms messages. """ - def __init__(self, trust_map, keep_untrusted=False): +# REDFLAG: Trust map ids are w/o names. +# +# clean_fms_id -> (set(uri,..), hash-> index, set(full_fms_id, ...)) +class USKNotificationParser(IFmsMessageSink): + """ IFmsMessageSink reads and saves all updates and announcements """ + def __init__(self, trust_map=None): IFmsMessageSink.__init__(self) - self.trust_map = strip_names(trust_map) - self.updates = {} - self.untrusted = None - if keep_untrusted: - self.untrusted = {} + self.table = {} + self.trust_map = trust_map def wants_msg(self, dummy, items): - """ IFmsMessageSink implementation. """ + """ Return True if the message should be passed to recv_fms_msg, + False, otherwise. + + items is an nntplib xover items tuple. + """ + # Skip replies, accept everything else. if len(items[5]) != 0: - # Skip replies - return False - - if not self.untrusted is None: - return True - - if clean_nym(items[2]) not in self.trust_map: - #print "Not trusted: ", items[2] - # Sender not authoritative on any USK. - return False - - return True - - def recv_fms_msg(self, dummy, items, lines): - """ IFmsMessageSink implementation. """ - allowed_hashes = self.trust_map.get(clean_nym(items[2]), ()) - - #print "---\nSender: %s\nSubject: %s\n" % (items[2], items[1]) - for update in parse(lines, True)[0]: - if update[0] in allowed_hashes: - # Only update if the nym is trusted *for the specific USK*. - #print "UPDATING ---\nSender: %s\nSubject: - # %s\n" % (items[2], items[1]) - self.handle_trusted_update(update) - else: - self.handle_untrusted_update(items[2], update) - - def handle_trusted_update(self, update): - """ INTERNAL: Handle a single update. """ - index = update[1] - value = self.updates.get(update[0], index) - if index >= value: - self.updates[update[0]] = index - - def handle_untrusted_update(self, sender, update): - """ INTERNAL: Handle a single untrusted update. """ - entry = self.untrusted.get(update[0], []) - if not sender in entry: - entry.append(sender) - self.untrusted[update[0]] = entry - - def updated(self, previous=None): - """ Returns a USK hash -> index map for USKs which - have been updated. """ - if previous is None: - previous = {} - ret = {} - for usk_hash in self.updates: - if not usk_hash in previous: - ret[usk_hash] = self.updates[usk_hash] - continue - if self.updates[usk_hash] > previous[usk_hash]: - ret[usk_hash] = self.updates[usk_hash] - - return ret - -class USKAnnouncementParser(IFmsMessageSink): - """ Class which accumulates USK announcement notifications - from fms messages. """ - # None means accept all announcements. - def __init__(self, trust_map = None, include_defaults=False): - IFmsMessageSink.__init__(self) - if not trust_map is None: - trust_map = strip_names(trust_map) - self.trust_map = trust_map - self.usks = {} - if include_defaults: - for owner, usk in KNOWN_REPOS: - if ((not trust_map is None) and - (not clean_nym(owner) in trust_map)): - continue - self.handle_announcement(owner, usk) - - def wants_msg(self, dummy, items): - """ IFmsMessageSink implementation. """ - if len(items[5]) != 0: - # Skip replies return False if self.trust_map is None: return True - if clean_nym(items[2]) not in self.trust_map: - #print "Not trusted: ", items[2] - # Sender not authoritative on any USK. - return False - - return True + return items[2] in self.trust_map def recv_fms_msg(self, dummy, items, lines): """ IFmsMessageSink implementation. """ #print "---\nSender: %s\nSubject: %s\n" % (items[2], items[1]) - for usk in parse(lines, True)[1]: - self.handle_announcement(items[2], usk) + clean_id = clean_nym(items[2]) + new_updates, new_announcements = parse(lines, True) + #if len(new_updates) > 0 or len(new_announcements) > 0: + # print "---\nSender: %s\nSubject: %s\n" % (items[2], items[1]) - def handle_announcement(self, sender, usk): - """ INTERNAL: Handle a single announcement """ - usk = get_usk_for_usk_version(usk, 0) - entry = self.usks.get(usk, []) - if not sender in entry: - entry.append(sender) - self.usks[usk] = entry + for update in new_updates: + self.handle_update(clean_id, items[2], update[0], update[1]) -HEX_CHARS = frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f']) + for announcement in new_announcements: + self.handle_announcement(clean_id, items[2], announcement) -# Really no library function to do this? -# REQUIRES: Lowercase! -def is_hex_string(value, length=12): - """ Returns True if value is a lowercase hex digit string, - False otherwise. """ - if not length is None: - if len(value) != length: - raise ValueError("Expected hex string of length: %i" % length) - for char in value: - if not char in HEX_CHARS: - return False - return True + def add_default_repos(self, default_repos): + """ Add table entries from a [(fms_id, usk), ...] list. """ + for repo_entry in default_repos: + clean_id = clean_nym(repo_entry[0]) + usk_hash = get_usk_hash(repo_entry[1]) + self.handle_announcement(clean_id, repo_entry[0], repo_entry[1]) + # Implicit in announcement + self.handle_update(clean_id, repo_entry[0], usk_hash, + get_version(repo_entry[1])) + # parse() handles implicit updates in annoucements + def handle_announcement(self, clean_id, fms_id, usk): + """ INTERNAL: process a single announcement. """ + entry = self.table.get(clean_id, (set([]), {}, set([]))) + entry[0].add(get_usk_for_usk_version(usk, 0)) + entry[2].add(fms_id) + + self.table[clean_id] = entry + + def handle_update(self, clean_id, fms_id, usk_hash, index): + """ INTERNAL: process a single update. """ + if index < 0: + print "handle_update -- skipped negative index!" + return + + entry = self.table.get(clean_id, (set([]), {}, set([]))) + prev_index = entry[1].get(usk_hash, 0) + if index > prev_index: + prev_index = index + entry[1][usk_hash] = prev_index + entry[2].add(fms_id) + + self.table[clean_id] = entry + + # REDFLAG: Doesn't deep copy. Passing out refs to stuff in table. + def invert_table(self): + """ INTERNAL: Return (clean_id -> fms_id, usk->set(clean_id, ...), + repo_hash->usk->set(clean_id, ...)) """ + # clean_id -> fms_id + fms_id_map = {} + # usk -> clean_id + announce_map = {} + # repo_hash -> clean_id + update_map = {} + + for clean_id in self.table: + table_entry = self.table[clean_id] + + # Backmap to the human readable fms ids + fms_id_map[clean_id] = self.get_human_name(clean_id, table_entry) + + # Accumulate all announcements + for usk in table_entry[0]: + entry = announce_map.get(usk, set([])) + entry.add(clean_id) + announce_map[usk] = entry + + # Accumulate all updates + for usk_hash in table_entry[1]: + entry = update_map.get(usk_hash, set([])) + entry.add(clean_id) + update_map[usk_hash] = entry + + return (fms_id_map, announce_map, update_map) + + def get_human_name(self, clean_id, table_entry=None): + """ INTERNAL: Return a full FMS id from a clean_id. """ + ret = None + if table_entry is None: + table_entry = self.table[clean_id] + + for fms_id in table_entry[2]: + fields = fms_id.split('@') + if len(fields[0].strip()) > 0: + ret = fms_id + break # break inner loop. + if ret is None: + # REDFLAG: Nail down when this can happen. + print "??? saw an fms id with no human readable part ???" + print list(table_entry[2])[0] + ret = list(table_entry[2])[0] + return ret + + # changed, untrusted + def get_updated(self, trust_map, version_table): + """ Returns trusted and untrusted changes with respect to + the version table. """ + + clean_trust = strip_names(trust_map) + # usk_hash -> index + max_trusted = {} + # usk_hash -> (index, fms_id) + max_untrusted = {} + for clean_id in self.table: + table_entry = self.table[clean_id] + for usk_hash in table_entry[1]: + if not usk_hash in version_table: + continue # Not a repo we care about. + + index = table_entry[1][usk_hash] + if index <= version_table[usk_hash]: + continue # Not news. Already know about that index. + + if (clean_id in clean_trust and + usk_hash in clean_trust[clean_id]): + # Trusted update + if not usk_hash in max_trusted: + max_trusted[usk_hash] = index + elif index > max_trusted[usk_hash]: + max_trusted[usk_hash] = index + else: + # Untrusted update + fms_id = self.get_human_name(clean_id, table_entry) + if not usk_hash in max_untrusted: + max_untrusted[usk_hash] = (index, fms_id) + elif index > max_untrusted[usk_hash]: + max_untrusted[usk_hash] = (index, fms_id) + + changed = {} + untrusted = {} + for usk_hash in version_table: + if usk_hash in max_trusted: + changed[usk_hash] = max_trusted[usk_hash] + + if usk_hash in max_untrusted: + if usk_hash in changed: + if max_untrusted[usk_hash][0] > changed[usk_hash]: + # There was a trusted update, but the untrusted one + # was higher. + untrusted[usk_hash] = max_untrusted[usk_hash] + else: + # No trusted updated + untrusted[usk_hash] = max_untrusted[usk_hash] + + # changed is usk_hash->index + # untrusted is usk_hash->(index, fms_id) + return (changed, untrusted) + +def show_table(parser, out_func): + """ Dump the announcements and updates in a human readable format. """ + fms_id_map, announce_map, update_map = parser.invert_table() + + usks = announce_map.keys() + usks.sort() + + for usk in usks: + usk_hash = get_usk_hash(usk) + out_func("USK Hash: %s\n" % usk_hash) + out_func("USK: %s\n" % usk) + out_func("Announced by:\n") + for clean_id in announce_map[usk]: + out_func(" %s\n" % fms_id_map[clean_id]) + out_func("Updated by:\n") + for clean_id in update_map[usk_hash]: + out_func(" %i:%s\n" % (parser.table[clean_id][1][usk_hash], + fms_id_map[clean_id])) + out_func("\n") ############################################################ DEFAULT_SUBJECT = 'Ignore' @@ -409,31 +475,8 @@ Group : %s def smoke_test(): """ Smoke test the functions in this module. """ - # trust_map = {'djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks': - # ('be68e8feccdd', ),} - - trust_map = {'falafel@IxVqeqM0LyYdTmYAf5z49SJZUxr7NtQkOqVYG0hvITw': - ('1' * 12, ), - 'SDiZ@17fy9sQtAvZI~nwDt5xXJkTZ9KlXon1ucEakK0vOFTc': - ('2' * 12, ), - } - - parser = USKIndexUpdateParser(trust_map) - recv_msgs('127.0.0.1', 11119, parser, ('test',)) - print - print "fms updates:" - print parser.updated() - print - print - parser = USKAnnouncementParser(trust_map) - recv_msgs('127.0.0.1', 11119, parser, ('test',)) - print - print "fms announcements:" - print parser.usks - print - print - + # REDFLAG: tests for USKNotificationParser ??? values0 = ((('be68e8feccdd', 23), ('e246cc31bc42', 3)), ('USK@kRM~jJVREwnN2qnA8R0Vt8HmpfRzBZ0j4rHC2cQ-0hw,' + '2xcoQVdQLyqfTpF2DpkdUIbHFCeL4W~2X1phUYymnhM,AQACAAE/' diff --git a/infocalypse/fmscmds.py b/infocalypse/fmscmds.py --- a/infocalypse/fmscmds.py +++ b/infocalypse/fmscmds.py @@ -19,68 +19,86 @@ Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks """ +from mercurial import util from fcpclient import get_usk_hash -from fms import USKAnnouncementParser, USKIndexUpdateParser, recv_msgs, \ - to_msg_string, MSG_TEMPLATE, send_msgs +from knownrepos import KNOWN_REPOS -from config import Config +from fms import recv_msgs, to_msg_string, MSG_TEMPLATE, send_msgs, \ + USKNotificationParser, show_table + +from config import Config, trust_id_for_repo, untrust_id_for_repo, known_hashes from infcmds import do_key_setup, setup, cleanup def handled_list(ui_, params, stored_cfg): - """ INTERNAL: Helper function to simplify execute_fmsread. """ - if params['FMSREAD'] != 'list' and params['FMSREAD'] != 'listall': + """ INTERNAL: HACKED""" + if params['FMSREAD'] != 'listall' and params['FMSREAD'] != 'list': return False - if params['FMSREAD'] == 'listall': - parser = USKAnnouncementParser(None, True) - if params['VERBOSITY'] >= 2: - ui_.status('Listing all repo USKs.\n') - else: + trust_map = None + if params['FMSREAD'] == 'list': trust_map = stored_cfg.fmsread_trust_map.copy() # paranoid copy - if params['VERBOSITY'] >= 2: - fms_ids = trust_map.keys() - fms_ids.sort() - ui_.status(("Only listing repo USKs from trusted " - + "fms IDs:\n%s\n\n") % '\n'.join(fms_ids)) - parser = USKAnnouncementParser(trust_map, True) + fms_ids = trust_map.keys() + fms_ids.sort() + ui_.status(("Only listing repo USKs from trusted " + + "FMS IDs:\n %s\n\n") % '\n '.join(fms_ids)) + parser = USKNotificationParser(trust_map) + parser.add_default_repos(KNOWN_REPOS) recv_msgs(stored_cfg.defaults['FMS_HOST'], stored_cfg.defaults['FMS_PORT'], parser, stored_cfg.fmsread_groups) - - if len(parser.usks) == 0: - ui_.status("No USKs found.\n") - return True - - ui_.status("\n") - for usk in parser.usks: - usk_entry = parser.usks[usk] - ui_.status("USK Hash: %s\n%s\n%s\n\n" % - (get_usk_hash(usk), usk, - '\n'.join(usk_entry))) + show_table(parser, ui_.status) return True -def dump_trust_map(ui_, params, trust_map): +def dump_trust_map(ui_, params, trust_map, force=False): """ Show verbose trust map information. """ - if params['VERBOSITY'] < 2: + if not force and params['VERBOSITY'] < 2: return - if not params['REQUEST_URI'] is None: - ui_.status("USK Hash: %s\n" % get_usk_hash(params['REQUEST_URI'])) + if not force and not params['REQUEST_URI'] is None: + ui_.status("USK hash for local repository: %s\n" % + get_usk_hash(params['REQUEST_URI'])) fms_ids = trust_map.keys() fms_ids.sort() ui_.status("Update Trust Map:\n") for fms_id in fms_ids: - ui_.status(" %s: %s\n" % (fms_id, - ' '.join(trust_map[fms_id]))) + ui_.status(" %s\n %s\n" % (fms_id, + '\n '.join(trust_map[fms_id]))) ui_.status("\n") +def handled_trust_cmd(ui_, params, stored_cfg): + """ INTERNAL: Handle --trust, --untrust and --showtrust. """ + if params['FMSREAD'] == 'trust': + if trust_id_for_repo(stored_cfg.fmsread_trust_map, + params['FMSREAD_FMSID'], + params['FMSREAD_HASH']): + ui_.status("Updated the trust map.\n") + Config.to_file(stored_cfg) + return True + elif params['FMSREAD'] == 'untrust': + if untrust_id_for_repo(stored_cfg.fmsread_trust_map, + params['FMSREAD_FMSID'], + params['FMSREAD_HASH']): + ui_.status("Updated the trust map.\n") + Config.to_file(stored_cfg) + return True + + elif params['FMSREAD'] == 'showtrust': + dump_trust_map(ui_, params, stored_cfg.fmsread_trust_map, True) + return True + + return False + def execute_fmsread(ui_, params, stored_cfg): """ Run the fmsread command. """ + + if handled_trust_cmd(ui_, params, stored_cfg): + return + if params['VERBOSITY'] >= 2: ui_.status(('Connecting to fms on %s:%i\n' + 'Searching groups: %s\n') % @@ -93,30 +111,34 @@ def execute_fmsread(ui_, params, stored_ return # Updating Repo USK indices for repos which are - # listed int the fmsread_trust_map section of the + # listed in the fmsread_trust_map section of the # config file. trust_map = stored_cfg.fmsread_trust_map.copy() # paranoid copy dump_trust_map(ui_, params, trust_map) ui_.status("Raking through fms messages. This may take a while...\n") - parser = USKIndexUpdateParser(trust_map, True) + parser = USKNotificationParser() recv_msgs(stored_cfg.defaults['FMS_HOST'], stored_cfg.defaults['FMS_PORT'], parser, stored_cfg.fmsread_groups) - changed = parser.updated(stored_cfg.version_table) - if params['VERBOSITY'] >= 2: - if parser.untrusted and len(parser.untrusted) > 0: - text = 'Skipped Untrusted Updates:\n' - for usk_hash in parser.untrusted: - text += usk_hash + ':\n' - fms_ids = parser.untrusted[usk_hash] - for fms_id in fms_ids: - text += ' ' + fms_id + '\n' - text += '\n' - ui_.status(text) + # IMPORTANT: Must include versions that are in the trust map + # but which we haven't seen before. + full_version_table = stored_cfg.version_table.copy() + for usk_hash in known_hashes(trust_map): + if not usk_hash in full_version_table: + full_version_table[usk_hash] = None # works + + changed, untrusted = parser.get_updated(trust_map, full_version_table) + + if params['VERBOSITY'] >= 2 and len(untrusted) > 0: + text = 'Skipped untrusted updates:\n' + for usk_hash in untrusted: + text += " %i:%s\n" % (untrusted[usk_hash], usk_hash) + text += '\n' + ui_.status(text) if len(changed) == 0: ui_.status('No updates found.\n') @@ -221,3 +243,106 @@ def execute_fmsnotify(ui_, repo, params, finally: cleanup(update_sm) +def check_trust_map(ui_, stored_cfg, repo_hash, notifiers, trusted_notifiers): + """ INTERNAL: Function to interactively update the trust map. """ + if len(trusted_notifiers) > 0: + return + ui_.warn("\nYou MUST trust at least one FMS Id to " + + "provide update notifications.\n\n") + + added = False + fms_ids = notifiers.keys() + fms_ids.sort() + + done = False + for fms_id in fms_ids: + if done: + break + ui_.status("Trust notifications from %s\n" % fms_id) + while not done: + result = ui_.prompt("(y)es, (n)o, (d)one, (a)bort?").lower() + if result is None: + raise util.Abort("Interactive input required.") + elif result == 'y': + trust_id_for_repo(stored_cfg.fmsread_trust_map, fms_id, + repo_hash) + added = True + break + elif result == 'n': + break + elif result == 'd': + done = True + break + elif result == 'a': + raise util.Abort("User aborted editing trust map.") + + if not added: + raise util.Abort("No trusted notifiers!") + + Config.to_file(stored_cfg) + ui_.status("Saved updated config file.\n\n") + +def get_uri_from_hash(ui_, dummy, params, stored_cfg): + """ Use FMS to get the URI for a repo hash. """ + if params['VERBOSITY'] >= 2: + ui_.status(('Connecting to fms on %s:%i\n' + + 'Searching groups: %s\n') % + (stored_cfg.defaults['FMS_HOST'], + stored_cfg.defaults['FMS_PORT'], + ' '.join(stored_cfg.fmsread_groups))) + trust_map = None + if params['FMSREAD_ONLYTRUSTED']: + # HACK to deal with spam of the announcement group.' + trust_map = stored_cfg.fmsread_trust_map.copy() # paranoid copy + fms_ids = trust_map.keys() + fms_ids.sort() + ui_.status(("Only using announcements from trusted " + + "FMS IDs:\n %s\n\n") % '\n '.join(fms_ids)) + + parser = USKNotificationParser(trust_map) + parser.add_default_repos(KNOWN_REPOS) + ui_.status("Raking through fms messages. This may take a while...\n") + recv_msgs(stored_cfg.defaults['FMS_HOST'], + stored_cfg.defaults['FMS_PORT'], + parser, + stored_cfg.fmsread_groups) + + target_hash = params['FMSREAD_HASH'] + target_usk = None + fms_id_map, announce_map, update_map = parser.invert_table() + + # Find URI + for usk in announce_map: + if target_hash == get_usk_hash(usk): + # We don't care who announced. The hash matches. + target_usk = usk + break + + if target_usk is None: + raise util.Abort(("No announcement found for [%s]. " + +"Use --uri to set the URI.") % target_hash) + + if params['VERBOSITY'] >= 2: + ui_.status("Found URI announcement:\n%s\n" % target_usk) + + trusted_notifiers = stored_cfg.trusted_notifiers(target_hash) + + notifiers = {} + for clean_id in update_map[target_hash]: + notifiers[fms_id_map[clean_id]] = parser.table[clean_id][1][target_hash] + + fms_ids = notifiers.keys() + fms_ids.sort() + + ui_.status("Found Updates:\n") + for fms_id in fms_ids: + if fms_id in trusted_notifiers: + value = "trusted" + else: + value = "untrusted" + ui_.status(" [%s]:%i:%s\n" % (value, notifiers[fms_id], fms_id)) + + check_trust_map(ui_, stored_cfg, target_hash, notifiers, trusted_notifiers) + + return target_usk + diff --git a/infocalypse/infcmds.py b/infocalypse/infcmds.py --- a/infocalypse/infcmds.py +++ b/infocalypse/infcmds.py @@ -44,6 +44,7 @@ from updatesm import UpdateStateMachine, INSERTING_URI, FAILING, REQUESTING_URI_4_COPY, CANCELING, CleaningUp from config import Config, DEFAULT_CFG_PATH, FORMAT_VERSION, normalize + from knownrepos import DEFAULT_TRUST, DEFAULT_GROUPS DEFAULT_PARAMS = { @@ -668,10 +669,16 @@ def execute_pull(ui_, repo, params, stor if not params['NO_SEARCH'] and is_usk_file(params['REQUEST_URI']): index = stored_cfg.get_index(params['REQUEST_URI']) if not index is None: - # Update index to the latest known value - # for the --uri case. - params['REQUEST_URI'] = get_usk_for_usk_version( - params['REQUEST_URI'], index) + if index >= get_version(params['REQUEST_URI']): + # Update index to the latest known value + # for the --uri case. + params['REQUEST_URI'] = get_usk_for_usk_version( + params['REQUEST_URI'], index) + else: + ui_.status(("Cached index [%i] < index in USK [%i]. " + + "Using the index from the USK.\n" + + "You're sure that index exists, right?\n") % + (index, get_version(params['REQUEST_URI']))) update_sm = setup(ui_, repo, params, stored_cfg) ui_.status("%sRequest URI:\n%s\n" % (is_redundant(params[ @@ -693,12 +700,15 @@ def execute_pull(ui_, repo, params, stor finally: cleanup(update_sm) -NO_INFO_FMT = """There's no stored information about that USK. +NO_INFO_FMT = """There's no stored information about this USK. USK hash: %s """ INFO_FMT = """USK hash: %s -index : %i +Index : %i + +Trusted Notifiers: +%s Request URI: %s @@ -724,8 +734,14 @@ def execute_info(ui_, params, stored_cfg # fix index request_uri = get_usk_for_usk_version(request_uri, max_index) + trusted = stored_cfg.trusted_notifiers(usk_hash) + if not trusted: + trusted = ' None' + else: + trusted = ' ' + '\n '.join(trusted) + ui_.status(INFO_FMT % - (usk_hash, max_index or -1, request_uri, insert_uri)) + (usk_hash, max_index or -1, trusted, request_uri, insert_uri)) def setup_tmp_dir(ui_, tmp): """ INTERNAL: Setup the temp directory. """ diff --git a/infocalypse/knownrepos.py b/infocalypse/knownrepos.py --- a/infocalypse/knownrepos.py +++ b/infocalypse/knownrepos.py @@ -25,15 +25,18 @@ from fcpclient import get_usk_hash # Not sure about this. Flat text file instead? +INFOCALYPSE_INDEX = 38 +FRED_STAGING_INDEX = 96 + KNOWN_REPOS = ( ('djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks', 'USK@kRM~jJVREwnN2qnA8R0Vt8HmpfRzBZ0j4rHC2cQ-0hw,' + '2xcoQVdQLyqfTpF2DpkdUIbHFCeL4W~2X1phUYymnhM,AQACAAE/' - + 'infocalypse.hgext.R1/31'), # This code. + + 'infocalypse.hgext.R1/%i' % INFOCALYPSE_INDEX), # This code. ('djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks', 'USK@kRM~jJVREwnN2qnA8R0Vt8HmpfRzBZ0j4rHC2cQ-0hw,' + '2xcoQVdQLyqfTpF2DpkdUIbHFCeL4W~2X1phUYymnhM,AQACAAE/' - + 'fred_staging.R1/24'), # Experimental git->hg mirror + + 'fred_staging.R1/%i' % FRED_STAGING_INDEX), # Experimental git->hg mirror ) diff --git a/infocalypse/validate.py b/infocalypse/validate.py new file mode 100644 --- /dev/null +++ b/infocalypse/validate.py @@ -0,0 +1,61 @@ +""" Helper functions to validate input from fms and config file. + + Copyright (C) 2009 Darrell Karbott + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2.0 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks +""" + +HEX_CHARS = frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f']) + +# Really no library function to do this? +# REQUIRES: Lowercase! +def is_hex_string(value, length=12): + """ Returns True if value is a lowercase hex digit string, + False otherwise. """ + if not length is None: + if len(value) != length: + return False + + for char in value: + if not char in HEX_CHARS: + return False + return True + +# http://wiki.freenetproject.org/Base64 +FREENET_BASE64_CHARS = frozenset( + [ chr(c) for c in + (range(ord('0'), ord('9') + 1) + + range(ord('a'), ord('z') + 1) + + range(ord('A'), ord('Z') + 1) + + [ord('~'), ord('-')]) + ]) + +def is_fms_id(value): + """ Returns True if value looks like a plausible FMS id.""" + fields = value.split('@') + if len(fields) != 2: + return False + + # REDFLAG: Faster way? Does it matter? + for character in fields[1]: + if not character in FREENET_BASE64_CHARS: + return False + + return True + +