(Steve Dougherty)
2013-07-26: Add abstraction for WoT identities. Add abstraction for WoT identities. The WoT_ID class performs lookup from an identifier and exposes attributes. Local_WoT_ID extends WoT_ID and also exposes an insert URI. This avoids problems with being stringly typed and makes intent much clearer. Also adds a clone() to USK to avoid modifying the WoT_ID member variables.
diff --git a/infocalypse/commands.py b/infocalypse/commands.py
--- a/infocalypse/commands.py
+++ b/infocalypse/commands.py
@@ -41,8 +41,8 @@ def infocalypse_update_repo_list(ui, **o
raise util.Abort("Update which repository list? Use --wot")
import wot
- local_id = wot.resolve_local_identity(ui, opts['wot'])
- wot.update_repo_listing(ui, local_id['Identity'])
+ from wot_id import Local_WoT_ID
+ wot.update_repo_listing(ui, Local_WoT_ID(opts['wot']))
def infocalypse_create(ui_, repo, **opts):
@@ -50,46 +50,34 @@ def infocalypse_create(ui_, repo, **opts
params, stored_cfg = get_config_info(ui_, opts)
insert_uri = ''
- attributes = None
+ local_id = None
if opts['uri'] != '' and opts['wot'] != '':
ui_.warn("Please specify only one of --uri or --wot.\n")
return
elif opts['uri'] != '':
insert_uri = opts['uri']
elif opts['wot'] != '':
- # Expecting nick_prefix/repo_name.R<redundancy num>/edition/
- nick_prefix, repo_desc = opts['wot'].split('/', 1)
+ # Expecting nick_prefix/repo_name.R<redundancy num>/edition
+ nick_prefix, repo_name, repo_edition = opts['wot'].split('/', 2)
- import wot
+ from wot_id import Local_WoT_ID
ui_.status("Querying WoT for local identities.\n")
- attributes = wot.resolve_local_identity(ui_, nick_prefix)
- if attributes is None:
- # Something went wrong; the function already printed an error.
- return
+ local_id = Local_WoT_ID(nick_prefix)
- ui_.status('Found {0}@{1}\n'.format(attributes['Nickname'],
- attributes['Identity']))
+ ui_.status('Found {0}\n'.format(local_id))
- insert_uri = attributes['InsertURI']
+ insert_uri = local_id.insert_uri.clone()
- # LCWoT returns URIs with a "freenet:" prefix, and WoT does not. The
- # rest of Infocalypse does not support the prefix. The local way to fix
- # this is to remove it here, but the more flexible way that is also
- # more work is to expand support to the rest of Infocalypse.
- # TODO: More widespread support for "freenet:" URI prefix.
- prefix = "freenet:"
- if insert_uri.startswith(prefix):
- insert_uri = insert_uri[len(prefix):]
-
- # URI is USK@key/WebOfTrust/<edition>, but we only want USK@key
- insert_uri = insert_uri.split('/', 1)[0]
- insert_uri += '/' + repo_desc
+ insert_uri.name = repo_name
+ insert_uri.edition = repo_edition
+ # Before passing along into execute_create().
+ insert_uri = str(insert_uri)
# Add "vcs" context. No-op if the identity already has it.
msg_params = {'Message': 'AddContext',
- 'Identity': attributes['Identity'],
+ 'Identity': local_id.identity_id,
'Context': 'vcs'}
import fcp
@@ -116,14 +104,13 @@ def infocalypse_create(ui_, repo, **opts
if inserted_to and opts['wot']:
# TODO: Would it be friendlier to include the nickname as well?
# creation returns a list of request URIs; use the first.
- stored_cfg.set_wot_identity(inserted_to[0],
- attributes['Identity'])
+ stored_cfg.set_wot_identity(inserted_to[0], local_id)
Config.to_file(stored_cfg)
# TODO: Imports don't go out of scope, right? The variables
# from the import are only visible in the function, so yes.
import wot
- wot.update_repo_listing(ui_, attributes['Identity'])
+ wot.update_repo_listing(ui_, local_id)
def infocalypse_copy(ui_, repo, **opts):
@@ -201,9 +188,6 @@ def infocalypse_pull(ui_, repo, **opts):
truster = get_truster(ui_, repo, opts)
request_uri = wot.resolve_pull_uri(ui_, opts['wot'], truster)
-
- if request_uri is None:
- return
else:
request_uri = opts['uri']
@@ -221,22 +205,25 @@ def infocalypse_pull(ui_, repo, **opts):
def infocalypse_pull_request(ui, repo, **opts):
import wot
+ from wot_id import WoT_ID
if not opts['wot']:
raise util.Abort("Who do you want to send the pull request to? Set "
"--wot.\n")
wot_id, repo_name = opts['wot'].split('/', 1)
- wot.send_pull_request(ui, repo, get_truster(ui, repo, opts), wot_id,
- repo_name)
+ from_identity = get_truster(ui, repo, opts)
+ to_identity = WoT_ID(wot_id, from_identity)
+ wot.send_pull_request(ui, repo, from_identity, to_identity, repo_name)
def infocalypse_check_notifications(ui, repo, **opts):
import wot
+ from wot_id import Local_WoT_ID
if not opts['wot']:
raise util.Abort("What ID do you want to check for notifications? Set"
" --wot.\n")
- wot.check_notifications(ui, opts['wot'])
+ wot.check_notifications(ui, Local_WoT_ID(opts['wot']))
def infocalypse_connect(ui, repo, **opts):
@@ -480,8 +467,7 @@ def infocalypse_setup(ui_, **opts):
ui_.status("Skipped FMS configuration because --nofms was set.\n")
if not opts['nowot']:
- import wot
- wot.execute_setup_wot(ui_, opts)
+ infocalypse_setupwot(ui_, **opts)
else:
ui_.status("Skipped WoT configuration because --nowot was set.\n")
@@ -494,8 +480,12 @@ def infocalypse_setupfms(ui_, **opts):
# TODO: Why ui with trailing underscore? Is there a global "ui" somewhere?
def infocalypse_setupwot(ui_, **opts):
+ if not opts['truster']:
+ util.Abort("Specify default truster with --truster")
+
import wot
- wot.execute_setup_wot(ui_, opts)
+ from wot_id import Local_WoT_ID
+ wot.execute_setup_wot(ui_, Local_WoT_ID(opts['truster']))
def infocalypse_setupfreemail(ui, repo, **opts):
@@ -511,21 +501,22 @@ def infocalypse_setupfreemail(ui, repo,
def get_truster(ui, repo, opts):
"""
- Return a local WoT ID.
-
- TODO: Check that it actually is local? Classes would be nice for this.
- Being stringly typed as present requires a lot of checking and re-checking.
+ Return a local WoT ID - either one that published this repository or the
+ default.
+ :rtype : Local_WoT_ID
"""
+ from wot_id import Local_WoT_ID
if opts['truster']:
- return opts['truster']
+ return Local_WoT_ID(opts['truster'])
else:
cfg = Config().from_ui(ui)
+ # Value is identity ID.
identity = cfg.get_wot_identity(cfg.get_request_uri(repo.root))
if not identity:
identity = cfg.defaults['DEFAULT_TRUSTER']
- return '@' + identity
+ return Local_WoT_ID('@' + identity)
#----------------------------------------------------------"
diff --git a/infocalypse/config.py b/infocalypse/config.py
--- a/infocalypse/config.py
+++ b/infocalypse/config.py
@@ -127,6 +127,7 @@ class Config:
self.insert_usks = {}
# repo id -> publisher WoT identity
self.wot_identities = {}
+ # TODO: Should this be keyed by str(WoT_ID) ?
# WoT public key hash -> Freemail password
self.freemail_passwords = {}
# fms_id -> (usk_hash, ...) map
@@ -208,8 +209,10 @@ class Config:
def set_wot_identity(self, for_usk_or_id, wot_identity):
"""
Set the WoT identity associated with the request USK.
+ :type wot_identity: WoT_ID
"""
- self.wot_identities[normalize(for_usk_or_id)] = wot_identity
+ self.wot_identities[normalize(for_usk_or_id)] =\
+ wot_identity.identity_id
def get_wot_identity(self, for_usk_or_id):
"""
@@ -225,20 +228,21 @@ class Config:
"""
Set the password for the given WoT identity.
"""
- self.freemail_passwords[wot_identity] = password
+ self.freemail_passwords[wot_identity.identity_id] = password
def get_freemail_password(self, wot_identity):
"""
Return the password associated with the given WoT identity.
Raise util.Abort if one is not set.
+ :type wot_identity: WoT_ID
"""
- if wot_identity in self.freemail_passwords:
- return self.freemail_passwords[wot_identity]
+ identity_id = wot_identity.identity_id
+ if identity_id in self.freemail_passwords:
+ return self.freemail_passwords[identity_id]
else:
raise util.Abort("{0} does not have a Freemail password set.\n"
- "Run hg fn-setupfreemail --truster {0}@{1}\n"
- .format(wot_identity['Nickname'],
- wot_identity['Identity']))
+ "Run hg fn-setupfreemail --truster {0}\n"
+ .format(wot_identity))
# Hmmm... really nescessary?
def get_dir_insert_uri(self, repo_dir):
diff --git a/infocalypse/keys.py b/infocalypse/keys.py
--- a/infocalypse/keys.py
+++ b/infocalypse/keys.py
@@ -17,6 +17,9 @@ class USK:
elif self.key.startswith('freenet://'):
self.key = self.key[len('freenet://'):]
+ def clone(self):
+ return USK(str(self))
+
def __str__(self):
return '%s/%s/%s' % (self.key, self.name, self.edition)
diff --git a/infocalypse/wot.py b/infocalypse/wot.py
--- a/infocalypse/wot.py
+++ b/infocalypse/wot.py
@@ -1,17 +1,15 @@
-import string
import fcp
from mercurial import util
from config import Config
import xml.etree.ElementTree as ET
from defusedxml.ElementTree import fromstring
import smtplib
-from base64 import b32encode
-from fcp.node import base64decode
from keys import USK
import yaml
from email.mime.text import MIMEText
import imaplib
import threading
+from wot_id import Local_WoT_ID, WoT_ID
FREEMAIL_SMTP_PORT = 4025
FREEMAIL_IMAP_PORT = 4143
@@ -78,20 +76,20 @@ def connect(ui, repo):
print ack
-def send_pull_request(ui, repo, from_identifier, to_identifier, to_repo_name):
- local_identity = resolve_local_identity(ui, from_identifier)
+def send_pull_request(ui, repo, from_identity, to_identity, to_repo_name):
+ """
- target_identity = resolve_identity(ui, local_identity['Identity'],
- to_identifier)
- from_address = to_freemail_address(local_identity)
- to_address = to_freemail_address(target_identity)
+ :type to_identity: WoT_ID
+ :type from_identity: Local_WoT_ID
+ """
+ from_address = require_freemail(from_identity)
+ to_address = require_freemail(to_identity)
cfg = Config.from_ui(ui)
- password = cfg.get_freemail_password(local_identity['Identity'])
+ password = cfg.get_freemail_password(from_identity.identity_id)
- to_repo = find_repo(ui, local_identity['Identity'], to_identifier,
- to_repo_name)
+ to_repo = find_repo(ui, to_identity, to_repo_name)
repo_context = repo['tip']
# TODO: Will there always be a request URI set in the config? What about
@@ -123,7 +121,7 @@ HG: Enter pull request message here. Lin
HG: The first line has "{0}" added before it in transit and is the subject.
HG: The second line should be blank.
HG: Following lines are the body of the message.
-""".format(VCS_TOKEN), from_identifier)
+""".format(VCS_TOKEN), str(from_identity))
# TODO: Save message and load later in case sending fails.
source_lines = source_text.splitlines()
@@ -147,14 +145,17 @@ HG: Following lines are the body of the
ui.status("Pull request sent.\n")
-def check_notifications(ui, sent_to_identifier):
- local_identity = resolve_local_identity(ui, sent_to_identifier)
- address = to_freemail_address(local_identity)
+def check_notifications(ui, local_identity):
+ """
+
+ :type local_identity: Local_WoT_ID
+ """
+ address = require_freemail(local_identity)
# Log in and open inbox.
cfg = Config.from_ui(ui)
imap = imaplib.IMAP4(cfg.defaults['HOST'], FREEMAIL_IMAP_PORT)
- imap.login(address, cfg.get_freemail_password(local_identity['Identity']))
+ imap.login(address, cfg.get_freemail_password(local_identity))
imap.select()
# Parenthesis to work around erroneous quotes:
@@ -262,17 +263,31 @@ def read_message_yaml(ui, from_address,
% (subject, request['request']))
+def require_freemail(wot_identity):
+ """
+ Return the given identity's Freemail address.
+ Abort with an error message if the given identity does not have a
+ Freemail address / context.
+ :type wot_identity: WoT_ID
+ """
+ if not wot_identity.freemail_address:
+ raise util.Abort("{0} is not using Freemail.\n".format(wot_identity))
+
+ return wot_identity.freemail_address
+
+
def update_repo_listing(ui, for_identity):
# TODO: WoT property containing edition. Used when requesting.
# Version number to support possible format changes.
+ """
+
+ :type for_identity: Local_WoT_ID
+ """
root = ET.Element('vcs', {'version': '0'})
ui.status("Updating repo listing for '%s'\n" % for_identity)
- # Key goes after @ - before is nickname.
- wot_id = '@' + for_identity
-
- for request_uri in build_repo_list(ui, wot_id):
+ for request_uri in build_repo_list(ui, for_identity):
repo = ET.SubElement(root, 'repository', {
'vcs': 'Infocalypse',
})
@@ -281,8 +296,7 @@ def update_repo_listing(ui, for_identity
# TODO: Nonstandard IP and port.
node = fcp.FCPNode()
- attributes = resolve_local_identity(ui, wot_id)
- insert_uri = USK(attributes['InsertURI'])
+ insert_uri = for_identity.insert_uri.clone()
# TODO: Somehow store the edition, perhaps in ~/.infocalypse. WoT
# properties are apparently not appropriate.
@@ -300,61 +314,50 @@ def update_repo_listing(ui, for_identity
ui.status("Updated repository listing:\n{0}\n".format(uri))
-def build_repo_list(ui, wot_id):
+def build_repo_list(ui, for_identity):
"""
Return a list of request URIs to repos for the given local identity.
- TODO: Does local identities only make sense?
+ :type for_identity: Local_WoT_ID
:param ui: to provide feedback
- :param wot_id: local WoT identity to list repos for.
+ :param for_identity: local WoT identity to list repos for.
"""
- # TODO: Repetitive local ID resolution between here and repo listing
- # update. This only needs key - perhaps higher-level stuff should always
- # take a wot_id?
- local_id = resolve_local_identity(ui, wot_id)
-
config = Config.from_ui(ui)
repos = []
# Add request URIs associated with the given identity.
for request_uri in config.request_usks.itervalues():
- if config.get_wot_identity(request_uri) == local_id['Identity']:
+ if config.get_wot_identity(request_uri) == for_identity.identity_id:
repos.append(request_uri)
return repos
-def find_repo(ui, truster, wot_identifier, repo_name):
+def find_repo(ui, identity, repo_name):
"""
Return a request URI for a repo of the given name published by an
identity matching the given identifier.
Raise util.Abort if unable to read repo listing or a repo by that name
does not exist.
+ :type identity: WoT_ID
"""
- listing = read_repo_listing(ui, truster, wot_identifier)
+ listing = read_repo_listing(ui, identity)
if repo_name not in listing:
- # TODO: Perhaps resolve again; print full nick / key?
- # TODO: Maybe print key found in the resolve_*identity?
raise util.Abort("{0} does not publish a repo named '{1}'\n"
- .format(wot_identifier, repo_name))
+ .format(identity, repo_name))
return listing[repo_name]
-def read_repo_listing(ui, truster, wot_identifier):
+def read_repo_listing(ui, identity):
"""
Read a repo listing for a given identity.
Return a dictionary of repository request URIs keyed by name.
- Raise util.Abort if unable to resolve identity.
+ :type identity: WoT_ID
"""
- identity = resolve_identity(ui, truster, wot_identifier)
-
- ui.status("Found {0}@{1}.\n".format(identity['Nickname'],
- identity['Identity']))
-
- uri = USK(identity['RequestURI'])
+ uri = identity.request_uri.clone()
uri.name = 'vcs'
uri.edition = 0
@@ -383,7 +386,8 @@ def read_repo_listing(ui, truster, wot_i
def resolve_pull_uri(ui, path, truster):
"""
Return a pull URI for the given path.
- Print an error message and return None on failure.
+ Print an error message and abort on failure.
+ :type truster: Local_WoT_ID
TODO: Is it appropriate to outline possible errors?
Possible failures are being unable to fetch a repo list for the given
identity, which may be a fetch failure or being unable to find the
@@ -397,10 +401,12 @@ def resolve_pull_uri(ui, path, truster):
# Expecting <id stuff>/reponame
wot_id, repo_name = path.split('/', 1)
+ identity = WoT_ID(wot_id, truster)
+
# TODO: How to handle redundancy? Does Infocalypse automatically try
# an R0 if an R1 fails?
- return find_repo(ui, truster, wot_id, repo_name)
+ return find_repo(ui, identity, repo_name)
def resolve_push_uri(ui, path):
@@ -413,21 +419,20 @@ def resolve_push_uri(ui, path):
where the identity is a local one. (Such that the insert URI is known.)
"""
# Expecting <id stuff>/repo_name
- # TODO: Duplicate with resolve_pull
wot_id, repo_name = path.split('/', 1)
- local_id = resolve_local_identity(ui, wot_id)
+ local_id = Local_WoT_ID(wot_id)
- insert_uri = USK(local_id['InsertURI'])
+ insert_uri = local_id.insert_uri
- identifier = local_id['Nickname'] + '@' + local_id['Identity']
-
- repo = find_repo(ui, local_id['Identity'], identifier, repo_name)
+ # 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 path, edition.
+ # Maintains name, edition.
repo_uri.key = insert_uri.key
return str(repo_uri)
@@ -437,33 +442,27 @@ def resolve_push_uri(ui, path):
# TODO: "cmds" suffix to module name to fit fms, arc, inf?
-def execute_setup_wot(ui_, opts):
+def execute_setup_wot(ui_, local_id):
cfg = Config.from_ui(ui_)
- response = resolve_local_identity(ui_, opts['truster'])
- ui_.status("Setting default truster to {0}@{1}\n".format(
- response['Nickname'],
- response['Identity']))
+ ui_.status("Setting default truster to {0}.\n".format(local_id))
- cfg.defaults['DEFAULT_TRUSTER'] = response['Identity']
+ cfg.defaults['DEFAULT_TRUSTER'] = local_id.identity_id
Config.to_file(cfg)
-def execute_setup_freemail(ui, wot_identifier):
+def execute_setup_freemail(ui, local_id):
"""
Prompt for, test, and set a Freemail password for the identity.
"""
- local_id = resolve_local_identity(ui, wot_identifier)
-
- address = to_freemail_address(local_id)
+ address = require_freemail(local_id)
password = ui.getpass()
if password is None:
raise util.Abort("Cannot prompt for a password in a non-interactive "
"context.\n")
- ui.status("Checking password for {0}@{1}.\n".format(local_id['Nickname'],
- local_id['Identity']))
+ ui.status("Checking password for {0}.\n".format(local_id))
cfg = Config.from_ui(ui)
@@ -479,245 +478,6 @@ def execute_setup_freemail(ui, wot_ident
raise util.Abort("Could not connect to server.\nGot '{0}'\n"
.format(e.smtp_error))
- cfg.set_freemail_password(local_id['Identity'], password)
+ cfg.set_freemail_password(local_id, password)
Config.to_file(cfg)
ui.status("Password set.\n")
-
-
-def resolve_local_identity(ui, wot_identifier):
- """
- Mercurial ui for error messages.
-
- Returns a dictionary of the nickname, insert and request URIs,
- and identity that match the given criteria.
- In the case of an error prints a message and returns None.
- """
- nickname_prefix, key_prefix = parse_name(wot_identifier)
-
- node = fcp.FCPNode()
- response = \
- node.fcpPluginMessage(async=False,
- 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}' is ambiguous.\n".format(wot_identifier))
-
- if len(matches) == 0:
- raise util.Abort("No local identities match '{0}'.\n".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 read_local_identity(response, id_num)
-
-
-def resolve_identity(ui, truster, wot_identifier):
- """
- 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.
-
- Returns a dictionary of the nickname, request URI,
- and identity that matches the given criteria.
- In the case of an error prints a message and returns None.
-
- :param ui: Mercurial ui for error messages.
- :param truster: Check trust list of this local identity.
- :param wot_identifier: Nickname and key, delimited by @. Either half can be
- omitted.
- """
- nickname_prefix, key_prefix = parse_name(wot_identifier)
- # TODO: Support different FCP IP / port.
- node = fcp.FCPNode()
-
- # Test for GetIdentitiesByPartialNickname support. currently LCWoT-only.
- # src/main/java/plugins/WebOfTrust/fcp/GetIdentitiesByPartialNickname.java
- # TODO: LCWoT allows limiting by context, but how to make sure otherwise?
- # TODO: Should this manually ensure an identity has a vcs context
- # otherwise?
-
- # LCWoT can have * to allow a wildcard match, but a wildcard alone is not
- # allowed. See Lucine Term Modifiers documentation. The nickname uses
- # this syntax but the ID is inherently startswith().
- params = {'Message': 'GetIdentitiesByPartialNickname',
- 'Truster': truster,
- 'PartialNickname':
- nickname_prefix + '*' if nickname_prefix else '',
- 'PartialID': key_prefix,
- 'MaxIdentities': 2,
- 'Context': 'vcs'}
-
- response = \
- node.fcpPluginMessage(async=False,
- 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))
- elif response['Replies.Message'] == 'Identities':
- matches = response['Replies.IdentitiesMatched']
- if matches == 0:
- raise util.Abort("No identities match '{0}'\n".format(
- wot_identifier))
- elif matches == 1:
- return read_identity(response, 0)
- else:
- raise util.Abort("'{0}' is ambiguous.\n".format(wot_identifier))
-
- # Partial matching not supported, or unknown truster. The only difference
- # in the errors is human-readable, so just try the exact match.
- assert response['Replies.Message'] == 'Error'
-
- # key_prefix must be a complete key for the lookup to succeed.
- params = {'Message': 'GetIdentity',
- 'Truster': truster,
- 'Identity': key_prefix}
- response = \
- node.fcpPluginMessage(async=False,
- 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 such identity '{0}'.\n".format(wot_identifier))
-
- # There should be only one result.
- # Depends on https://bugs.freenetproject.org/view.php?id=5729
- return read_identity(response, 0)
-
-
-def read_local_identity(message, id_num):
- """
- Reads an FCP response from a WoT plugin describing a local identity and
- returns a dictionary of Nickname, InsertURI, RequestURI, Identity, and
- each numbered Context.
- """
- result = read_identity(message, id_num)
- result['InsertURI'] = message['Replies.InsertURI{0}'.format(id_num)]
- return result
-
-
-def read_identity(message, id_num):
- """
- Reads an FCP response from a WoT plugin describing an identity and
- returns a dictionary of Nickname, RequestURI, Identity, and Contexts.
- """
- # Return properties for the selected identity. (by number)
- result = {}
- for item in ['Nickname', 'RequestURI', 'Identity']:
- result[item] = message['Replies.{0}{1}'.format(item, id_num)]
-
- # LCWoT also puts these things as properties, which would be nicer to
- # depend on and would allow just returning all properties for the identity.
- #property_prefix = "Replies.Properties{0}".format(id_num)
-
- # Add contexts and other properties.
- # TODO: Unflattening WoT response? Several places check for prefix like
- # this.
- 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):
- num = key[len(context_prefix):]
- result["Context{0}".format(num)] = 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 result and value != result[name]:
- print("WARNING: '{0}' has a different value as a property."
- .format(name))
-
- result[name] = value
-
- return result
-
-
-def parse_name(wot_identifier):
- """
- Parse identifier of the forms: nick
- nick@key
- @key
- Return nick, key. If a part is not given return an empty string for it.
- """
- split = wot_identifier.split('@', 1)
- nickname_prefix = split[0]
-
- key_prefix = ''
- if len(split) == 2:
- key_prefix = split[1]
-
- return nickname_prefix, key_prefix
-
-
-def to_freemail_address(identity):
- """
- Return a Freemail address to contact the given identity if it has a
- Freemail context.
- Raise util.Abort if it does not have a Freemail context.
- """
-
- # 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
-
- for item in identity.iteritems():
- if item[1] == 'Freemail' and item[0].startswith('Context'):
- re_encode = b32encode(base64decode(identity['Identity']))
- # Remove trailing '=' padding.
- re_encode = re_encode.rstrip('=')
-
- # Freemail addresses are lower case.
- return string.lower(identity['Nickname'] + '@' + re_encode +
- '.freemail')
-
- raise util.Abort("{0}@{1} is not using Freemail.\n".format(
- identity['Nickname'], identity['Identity']))
diff --git a/infocalypse/wot_id.py b/infocalypse/wot_id.py
new file mode 100644
--- /dev/null
+++ b/infocalypse/wot_id.py
@@ -0,0 +1,247 @@
+import fcp
+from mercurial import util
+import string
+from keys import USK
+from base64 import b32encode
+from fcp.node import base64decode
+
+
+class WoT_ID(object):
+ """
+ Represents a WoT ID.
+
+ TODO: Is this list appropriate to have?
+ * nickname - str
+ * request_uri - USK
+ * identity_id - str
+ * contexts - list
+ * properties - dict
+ * freemail_address - str
+ """
+
+ def __init__(self, wot_identifier, truster, id_num=0, message=None):
+ """
+ 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.
+ if not message:
+ message = get_identity(wot_identifier, truster)
+
+ 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')
+
+ # Add contexts and other properties.
+ # TODO: Unflattening WoT response? Several places check for prefix like
+ # this.
+ 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):
+ #num = key[len(context_prefix):]
+ # TODO: Is there a reason to keep the numbers? (As in key it.)
+ #identity.contexts["Context{0}".format(num)] = message[key]
+ 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')
+
+ def __str__(self):
+ return self.nickname + '@' + self.identity_id
+
+
+class Local_WoT_ID(WoT_ID):
+ """
+ Represents a local WoT ID.
+
+ * nickname - str
+ * request_uri - USK
+ * insert_uri - USK
+ * identity_id - str
+ * contexts - list
+ * properties - dict
+ """
+
+ def __init__(self, wot_identifier):
+ nickname_prefix, key_prefix = parse_name(wot_identifier)
+
+ node = fcp.FCPNode()
+ response = \
+ node.fcpPluginMessage(async=False,
+ 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}' is ambiguous.\n".format(wot_identifier))
+
+ if len(matches) == 0:
+ raise util.Abort("No local identities match '{0}'.\n".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]
+
+ self.insert_uri = USK(response['Replies.InsertURI{0}'.format(id_num)])
+
+ WoT_ID.__init__(self, None, None, id_num=id_num, message=response)
+
+
+def get_identity(wot_identifier, truster):
+ nickname_prefix, key_prefix = parse_name(wot_identifier)
+ # TODO: Support different FCP IP / port.
+ node = fcp.FCPNode()
+
+ # Test for GetIdentitiesByPartialNickname support. currently LCWoT-only.
+ # src/main/java/plugins/WebOfTrust/fcp/GetIdentitiesByPartialNickname
+ # TODO: LCWoT allows limiting by context; how to make sure otherwise?
+ # TODO: Should this manually ensure an identity has a vcs context
+ # otherwise?
+
+ # LCWoT can have * to allow a wildcard match, but a wildcard alone is
+ # not allowed. See Lucine Term Modifiers documentation. The nickname
+ # uses this syntax but the ID is inherently startswith().
+ params = {'Message': 'GetIdentitiesByPartialNickname',
+ 'Truster': truster.identity_id,
+ 'PartialNickname':
+ nickname_prefix + '*' if nickname_prefix else '',
+ 'PartialID': key_prefix,
+ 'MaxIdentities': 2,
+ 'Context': 'vcs'}
+
+ response = \
+ node.fcpPluginMessage(async=False,
+ 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))
+ elif response['Replies.Message'] == 'Identities':
+ matches = response['Replies.IdentitiesMatched']
+ if matches == 0:
+ raise util.Abort("No identities match '{0}'\n".format(
+ wot_identifier))
+ elif matches == 1:
+ return response
+ else:
+ raise util.Abort("'{0}' is ambiguous.\n".format(wot_identifier))
+
+ # Partial matching not supported, or unknown truster. The only
+ # difference in the errors is human-readable, so just try the exact
+ # match.
+ assert response['Replies.Message'] == 'Error'
+
+ # key_prefix must be a complete key for the lookup to succeed.
+ params = {'Message': 'GetIdentity',
+ 'Truster': truster,
+ 'Identity': key_prefix}
+ response = \
+ node.fcpPluginMessage(async=False,
+ 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 such identity '{0}'.\n".format(wot_identifier))
+
+ # There should be only one result.
+ # Depends on https://bugs.freenetproject.org/view.php?id=5729
+ return response
+
+
+def parse_name(wot_identifier):
+ """
+ Parse identifier of the forms: nick
+ nick@key
+ @key
+ Return nick, key. If a part is not given return an empty string for it.
+ """
+ split = wot_identifier.split('@', 1)
+ nickname_prefix = split[0]
+
+ key_prefix = ''
+ if len(split) == 2:
+ key_prefix = split[1]
+
+ return nickname_prefix, key_prefix