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
+
+