[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[minion-cvs] Break and fix server repeatedly until DNS-based routing...
Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv21642/lib/mixminion
Modified Files:
BuildMessage.py ClientDirectory.py ClientMain.py Config.py
NetUtils.py Packet.py ServerInfo.py __init__.py test.py
Log Message:
Break and fix server repeatedly until DNS-based routing works. Hurray.
TODO:
- Reflect state of work
- Defer client-side fragment reassembly
setup.py
- Bump version to 0.0.6alpha2
BuildMessage.py
- Choose relay types and routing info based on server capabilities -- don't
just assume that IPV4 is right for everyone.
ClientDirectory.py
- Document everything.
- Remove spurious isSURB field from ExitAddress
- Fix theoretical bug that would crash path generation with non-mixminion
servers.
- Deprecate unadorned '*' in paths.
- Note bug with path length checking.
ClientMain.py
- Deprecate -H; use -P foo instead.
- Make list-servers work
Config.py, NetUtils.py:
- Refactor _parseIP and _parseIP6 validation functions into NetUtils.
Config.py
- Add documentation
NetUtils.py
- Add documentation
- Debug getIP
- Add function to detect static IP4 or IP6 addresses.
Packet.py
- Debug parseRelayInfoByType
- Documentation
ServerInfo.py:
- Documentation
- Disable hostname-based routing with 0.0.6alpha1 servers. (I don't
want to break Tonga and peertech.)
test.py:
- Add tests for DNS functionality
- Add tests for DNS farm functionality
- Add tests for new ServerInfo methods
DNSFarm.py
- Add documentation
- Make DNSCache.shutdown() more failsafe, and make shutdown(wait=1) not
deadlock the server.
- Add special-case test to skip hostname lookup for static IP addresses
MMTPServer.py
- Make sure that we get real RelayPacket objects.
- Choose whether to use IP4 or IP6 connections in MMTPClientConnection
PacketHandler.py
- Accept HOST relay types and MMTPHostInfo routinginfo.
ServerConfig.py:
- Remove obsolete __DEBUG_GC option.
ServerMain:
- Change same-server detection mechanism to only look at key ID.
ServerQueue.py:
- Add assert to catch weird bug case.
Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.60
retrieving revision 1.61
diff -u -d -r1.60 -r1.61
--- BuildMessage.py 19 Oct 2003 03:12:01 -0000 1.60
+++ BuildMessage.py 10 Nov 2003 04:12:20 -0000 1.61
@@ -258,7 +258,8 @@
header = _buildHeader(path, headerSecrets, exitType, tag+exitInfo,
paddingPRNG=Crypto.getCommonPRNG())
- # XXXX007 switch to Host info.
+ # XXXX007 switch to Host info. We need to use IPV4 for reply blocks
+ # XXXX007 for now, since we don't know which servers will support HOST.
return ReplyBlock(header, expiryTime,
SWAP_FWD_IPV4_TYPE,
path[0].getIPV4Info().pack(), sharedKey), secrets, tag
@@ -308,7 +309,8 @@
err = 0 # 0: no error. 1: 1st leg too big. 2: 1st leg okay, 2nd too big.
if path1 is not None:
try:
- _getRouting(path1, SWAP_FWD_IPV4_TYPE, path2[0].getRoutingInfo().pack())
+ rt,ri = path1[-1].getRoutingFor(path2[0],swap=1)
+ _getRouting(path1, rt, ri)
except MixError:
err = 1
# Add a dummy tag as needed to last exitinfo.
@@ -457,7 +459,7 @@
#----------------------------------------------------------------------
def _buildPacket(payload, exitType, exitInfo,
- path1, path2, paddingPRNG=None, paranoia=0):
+ path1, path2, paddingPRNG=None, paranoia=0):
"""Helper method to create a message.
The following fields must be set:
@@ -511,8 +513,7 @@
path1exittype = reply.routingType
path1exitinfo = reply.routingInfo
else:
- path1exittype = SWAP_FWD_IPV4_TYPE
- path1exitinfo = path2[0].getRoutingInfo().pack()
+ path1exittype, path1exitinfo = path1[-1].getRoutingFor(path2[0],swap=1)
# Generate secrets for path1.
secrets1 = [ secretRNG.getBytes(SECRET_LEN) for _ in path1 ]
@@ -700,8 +701,9 @@
Raises MixError if the routing info is too big to fit into a single
header. """
# Construct a list 'routing' of exitType, exitInfo.
- routing = [ (FWD_IPV4_TYPE, node.getRoutingInfo().pack()) for
- node in path[1:] ]
+ routing = []
+ for i in xrange(len(path)-1):
+ routing.append(path[i].getRoutingFor(path[i+1],swap=0))
routing.append((exitType, exitInfo))
# sizes[i] is number of bytes added to header for subheader i.
Index: ClientDirectory.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientDirectory.py,v
retrieving revision 1.13
retrieving revision 1.14
diff -u -d -r1.13 -r1.14
--- ClientDirectory.py 9 Nov 2003 23:28:10 -0000 1.13
+++ ClientDirectory.py 10 Nov 2003 04:12:20 -0000 1.14
@@ -21,7 +21,7 @@
import types
import urllib2
-import mixminion.ClientMain #XXXX
+import mixminion.ClientMain #XXXX -- it would be better not to need this.
import mixminion.Config
import mixminion.Crypto
import mixminion.NetUtils
@@ -377,8 +377,18 @@
self.byNickname.setdefault(nn, []).append((info, where))
def getFeatureMap(self, features, at=None, goodOnly=0):
- """DOCDOC
- Returns a dict from nickname to (va,vu) to feature to value."""
+ """Given a list of feature names (see Config.resolveFeatureName for
+ more on features, returns a dict mapping server nicknames to maps
+ from (valid-after,valid-until) tuples to maps from feature to
+ value.
+
+ That is: { nickname : { (time1,time2) : { feature : val } } }
+
+ If 'at' is provided, use only server descriptors that are valid at
+ the time 'at'.
+
+ If 'goodOnly' is true, use only recommended servers.
+ """
result = {}
if not self.fullServerList:
return {}
@@ -449,6 +459,10 @@
return "/".join(r)
def getNameByRelay(self, routingType, routingInfo):
+ """Given a routingType, routingInfo (as string) tuple, return the
+ nickname of the corresponding server. If no such server is
+ known, return a string representation of the routingInfo.
+ """
routingInfo = mixminion.Packet.parseRelayInfoByType(
routingType, routingInfo)
nn = self.getNicknameByKeyID(routingInfo.keyinfo)
@@ -458,7 +472,10 @@
return nn
def getLiveServers(self, startAt=None, endAt=None):
- """DOCDOC"""
+ """Return a list of all server desthat are live from startAt through
+ endAt. The list is in the standard (ServerInfo,where) format,
+ as returned by __find.
+ """
if startAt is None:
startAt = time.time()
if endAt is None:
@@ -467,7 +484,6 @@
def clean(self, now=None):
"""Remove all expired or superseded descriptors from DIR/servers."""
-
if now is None:
now = time.time()
cutoff = now - 600
@@ -553,7 +569,20 @@
def generatePaths(self, nPaths, pathSpec, exitAddress,
startAt=None, endAt=None,
prng=None):
- """Return a list of pairs of lists of serverinfo DOCDOC."""
+ """Generate a list of paths for delivering packets to a given
+ exit address, using a given path spec. Each path is returned
+ as a tuple of lists of ServerInfo.
+
+ nPaths -- the number of paths to generate. (You need
+ to generate multiple paths at once when you want them
+ to converge at the same exit server -- for example,
+ for delivering server-side fragmented messages.)
+ pathSpec -- A PathSpecifier object.
+ exitAddress -- An ExitAddress object.
+ startAt, endAt -- A duration of time over which the
+ paths must remain valid.
+ """
+ assert pathSpec.isReply == exitAddress.isReply
if prng is None:
prng = mixminion.Crypto.getCommonPRNG()
@@ -593,7 +622,7 @@
path = self.getPath(p, startAt=startAt, endAt=endAt)
path1,path2 = path[:n1], path[n1:]
paths.append( (path1,path2) )
- if exitAddress.isReply or exitAddress.isSURB:
+ if pathSpec.isReply or pathSpec.isSURB:
LOG.info("Selected path is %s",
",".join([s.getNickname() for s in path]))
else:
@@ -616,8 +645,7 @@
All servers are chosen to be valid continuously from
startAt to endAt.
- The path selection algorithm perfers to choose without
- replacement it it can.
+ The path selection algorithm is described in path-spec.txxt
"""
# Fill in startAt, endAt, prng if not provided
if startAt is None:
@@ -660,15 +688,20 @@
# ...and see if there are any relays left that aren't adjacent?
candidates = []
for c in relays:
+ # Avoid same-server hops
if ((prev and c.hasSameNicknameAs(prev)) or
- (next and c.hasSameNicknameAs(next)) or
- (prev and not prev.canRelayTo(c)) or
- ((not prev) and not c.canStartAt()) or
+ (next and c.hasSameNicknameAs(next))):
+ continue
+ # Avoid hops that can't relay to one another.
+ if ((prev and not prev.canRelayTo(c)) or
(next and not c.canRelayTo(next))):
continue
+ # Avoid first hops that we can't deliver to.
+ if (not prev) and not c.canStartAt():
+ continue
candidates.append(c)
if candidates:
- # Good. There are.
+ # Good. There aresome okay servers/
servers[i] = prng.pick(candidates)
else:
# Nope. Can we duplicate a relay?
@@ -694,8 +727,13 @@
def validatePath(self, pathSpec, exitAddress, startAt=None, endAt=None,
warnUnrecommended=1):
- """DOCDOC
- takes pathspec; raises UIError or does nothing."""
+ """Given a PathSpecifier and an ExitAddress, check whether any
+ valid paths can satisfy the spec for delivery to the address.
+ Raise UIError if no such path exists; else returns.
+
+ If warnUnrecommended is true, give a warning if the user has
+ requested any unrecommended servers.
+ """
if startAt is None: startAt = time.time()
if endAt is None: endAt = startAt+self.DEFAULT_REQUIRED_LIFETIME
@@ -784,10 +822,19 @@
"on the recommended list.")
#----------------------------------------------------------------------
-def compressServerList(featureMap, ignoreGaps=0, terse=0):
- """DOCDOC
- featureMap is nickname -> va,vu -> feature -> value .
- result is same format, but time is compressed.
+def compressFeatureMap(featureMap, ignoreGaps=0, terse=0):
+ """Given a feature map as returned by ClientDirectory.getFeatureMap,
+ compress the data from each server's server descriptors. The
+ default behavior is: if a server has two server descriptors such
+ that one becomes valid immediately after the other becomes invalid,
+ and they have the same features, compress the two entries into one.
+
+ If ignoreGaps is true, the requirement for sequential lifetimes is
+ omitted.
+
+ If terse is true, server descriptors are compressed even if their
+ features don't match. If a feature has different values at different
+ times, they are concatenated with ' / '.
"""
result = {}
for nickname in featureMap.keys():
@@ -799,7 +846,7 @@
r.append((va,vu,features))
continue
lastva, lastvu, lastfeatures = r[-1]
- if (ignoreGaps or lastvu == va) and lastfeatures == features:
+ if (ignoreGaps or lastva <= va <= lastvu) and lastfeatures == features:
r[-1] = lastva, vu, features
else:
r.append((va,vu,features))
@@ -823,21 +870,26 @@
return result
def formatFeatureMap(features, featureMap, showTime=0, cascade=0, sep=" "):
- # No cascade:
- # nickname:time1: value value value
- # nickname:time2: value value value
+ """Given a list of features (by name; see Config.resolveFeatureName) and
+ a featureMap as returned by ClientDirectory.getFeatureMap or
+ compressFeatureMap, formats the map for display to an end users.
+ Returns a list of strings suitable for printing on separate lines.
- # Cascade=1:
- # nickname:
- # time1: value value value
- # time2: value value value
+ If 'showTime' is false, omit descriptor validity times from the
+ output.
- # Cascade = 2:
- # nickname:
- # time:
- # feature:value
- # feature:value
- # feature:value
+ 'cascade' is an integer between 0 and 2. Its values generate the
+ following output formats:
+ 0 -- Put nickname, time, and feature values on one line.
+ If there are multiple times for a given nickname,
+ generate multiple lines. This format is best for grep.
+ 1 -- Put nickname on its own line; put time and feature lists
+ one per line.
+ 2 -- Put nickname, time, and each feature value on its own line.
+
+ 'sep' is used to concatenate feauture values when putting them on
+ the same line.
+ """
nicknames = [ (nn.lower(), nn) for nn in featureMap.keys() ]
nicknames.sort()
lines = []
@@ -872,14 +924,40 @@
#----------------------------------------------------------------------
+# What exit type names do we know about?
KNOWN_STRING_EXIT_TYPES = [
"mbox", "smtp", "drop"
]
class ExitAddress:
- #FFFF Perhaps this crams too much into ExitAddress.
+ """An ExitAddress represents the target of a Mixminion message or SURB.
+ It also encodes other properties off the message that must be known to
+ choose the exit hop (including fragmentation and message size).
+ """
+ ## Fields:
+ # exitType, exitAddress: None (for a reply message), or the delivery
+ # routing type and routing info for the address.
+ # isReply: boolean: is target address a SURB or set of SURBs?
+ # lastHop: None, or the nickname of a server that must be used as the
+ # last hop of the path.
+ # isSSFragmented: boolean: Is the message going to be fragmented and
+ # reassembled at the exit server?
+ # nFragments: How many fragments are going to be assembled at the exit
+ # server?
+ # exitSize: How large (in bytes) will the message be at the exit server?
+ # headers: A map from header name to value.
def __init__(self,exitType=None,exitAddress=None,lastHop=None,isReply=0,
- isSURB=0,isSSFragmented=0):
+ isSSFragmented=0):
+ """Create a new ExitAddress.
+ exitType,exitAddress -- the routing type and routing info
+ for the delivery (if not a reply)
+ lastHop -- the nickname of the last hop in the path, if the
+ exit address is specific to a single hop.
+ isReply -- true iff this message is a reply
+ isSSFragmented -- true iff this message is fragmented for
+ server-side reassembly.
+ """
+ #FFFF Perhaps this crams too much into ExitAddress.
if isReply:
assert exitType is None
assert exitAddress is None
@@ -898,40 +976,53 @@
self.exitAddress = exitAddress
self.lastHop = lastHop
self.isReply = isReply
- self.isSURB = isSURB
self.isSSFragmented = isSSFragmented #server-side frag reassembly only.
self.nFragments = self.exitSize = 0
self.headers = {}
def getFragmentedMessagePrefix(self):
+ """Return the prefix to be prepended to server-side fragmented
+ messages"""
routingType, routingInfo, _ = self.getRouting()
return mixminion.Packet.ServerSideFragmentedMessage(
routingType, routingInfo, "").pack()
def setFragmented(self, isSSFragmented, nFragments):
+ """Set the fragmentation parameters of this exit address
+ """
self.isSSFragmented = isSSFragmented
self.nFragments = nFragments
def hasPayload(self):
+ """Return true iff this exit type requires a payload"""
return self.exitType not in ('drop', DROP_TYPE)
def setExitSize(self, exitSize):
+ """Set the size of the message at the exit."""
self.exitSize = exitSize
def setHeaders(self, headers):
+ """Set the headers of the message at the exit."""
self.headers = headers
def getLastHop(self):
+ """Return the forced last hop of this exit address (or None)"""
return self.lastHop
def isSupportedByServer(self, desc):
+ """Return true iff the server described by 'desc' supports this
+ exit type."""
try:
self.checkSupportedByServer(desc,verbose=0)
return 1
except UIError:
return 0
def checkSupportedByServer(self, desc,verbose=1):
+ """Check whether the server described by 'desc' supports this
+ exit type. Returns if yes, raises a UIError if no. If
+ 'verbose' is true, give warnings for iffy cases."""
+
if self.isReply:
return
nickname = desc.getNickname()
if self.headers:
- #XXXX006 remove this eventually.
- sware = desc['Server'].get("Software")
+ #XXXX007 remove this eventually.
+ sware = desc['Server'].get("Software","")
if (sware.startswith("Mixminion 0.0.4") or
sware.startswith("Mixminion 0.0.5alpha1")):
raise UIError("Server %s is running old software that doesn't support exit headers.", nickname)
@@ -973,17 +1064,22 @@
nickname, self.getPrettyExitType())
def getPrettyExitType(self):
+ """Return a human-readable representation of the exit type."""
if type(self.exitType) == types.IntType:
return "0x%04X"%self.exitType
else:
return self.exitType
def isServerRelative(self):
+ """Return true iff the exit type's addresses are specific to a
+ given exit hop."""
return self.exitType in ('mbox', MBOX_TYPE)
def getExitServers(self, directory, startAt=None, endAt=None):
- """DOCDOC
- Return a list of all exit servers that might work."""
+ """Given a ClientDirectory and a time range, return a list of
+ server descriptors for all servers that might work for this
+ exit address.
+ """
assert self.lastHop is None
liveServers = directory.getLiveServers(startAt, endAt)
result = [ desc for desc in liveServers
@@ -991,7 +1087,8 @@
return result
def getRouting(self):
- """DOCDOC"""
+ """Return a routingType, routingInfo, last-hop-nickname tuple for
+ this exit address."""
ri = self.exitAddress
if self.isSSFragmented:
rt = FRAGMENT_TYPE
@@ -1018,7 +1115,6 @@
OR drop
OR 0x<routing type>:<routing info>
"""
- # ???? Should this should get refactored into clientmodules, or someplace?
if s.lower() == 'drop':
return ExitAddress('drop',"")
elif s.lower() == 'test':
@@ -1053,16 +1149,28 @@
raise ParseError("Unrecognized address type: %s"%s)
class PathElement:
+ """A PathElement is a single user-specified component of a path. This
+ is an abstract class; it's only used to describe the interface."""
def validate(self, directory, start, end):
+ """Check whether this path element could be valid; if not, raise
+ UIError."""
raise NotImplemented()
def getFixedServer(self, directory, start, end):
+ """If this element describes a single fixed server, look up
+ and return the ServerInfo for that server."""
raise NotImplemented()
def getServerNames(self):
+ """Return a list containing either names of servers for this
+ path element, or None for randomly chosen servers.
+ """
raise NotImplemented()
def getMinLength(self):
+ """Return the fewest number of servers that this element might
+ contain."""
raise NotImplemented()
class ServerPathElement(PathElement):
+ """A path element for a single server specified by filename or nickname"""
def __init__(self, nickname):
self.nickname = nickname
def validate(self, directory, start, end):
@@ -1076,8 +1184,11 @@
return 1
def __repr__(self):
return "ServerPathElement(%r)"%self.nickname
+ def __str__(self):
+ return self.nickname
class DescriptorPathElement(PathElement):
+ """A path element for a single server descriptor"""
def __init__(self, desc):
self.desc = desc
def validate(self, directory, start, end):
@@ -1092,8 +1203,13 @@
return 1
def __repr__(self):
return "DescriptorPathElement(%r)"%self.desc
+ def __str__(self):
+ return self.desc.getNickname()
class RandomServersPathElement(PathElement):
+ """A path element for randomly chosen servers. If 'n' is set, exactly
+ n servers are chosen. If 'approx' is set, approximately 'approx'
+ servers are chosen."""
def __init__(self, n=None, approx=None):
assert not (n and approx)
assert n is None or approx is None
@@ -1111,6 +1227,7 @@
n = int(prng.getNormal(self.approx,1.5)+0.5)
return [ None ] * n
def getMinLength(self):
+ #XXXX006 need getAvgLength too, probably. Ugh.
if self.n is not None:
return self.n
else:
@@ -1121,9 +1238,29 @@
return "RandomServersPathElement(n=%r)"%self.n
else:
return "RandomServersPathElement(approx=%r)"%self.approx
+ def __str__(self):
+ if self.n == 1:
+ return "?"
+ elif self.n > 1:
+ return "*%d"%self.n
+ else:
+ assert self.approx
+ return "~%d"%self.approx
#----------------------------------------------------------------------
class PathSpecifier:
+ """A PathSpecifer represents a user-provided description of a path.
+ It's generated by parsePath.
+ """
+ ## Fields:
+ # path1, path2: Two lists containing PathElements for the two
+ # legs of the path.
+ # isReply: boolean: Is this a path for a reply? (If so, path2
+ # should be empty.)
+ # isSURB: boolean: Is this a path for a SURB? (If so, path1
+ # should be empty.)
+ # lateSplit: boolean: Does the path have an explicit swap point,
+ # or do we split it in two _after_ generating it?
def __init__(self, path1, path2, isReply, isSURB, lateSplit):
if isSURB:
assert path2 and not path1
@@ -1140,33 +1277,36 @@
self.lateSplit=lateSplit
def getFixedLastServer(self,directory,startAt,endAt):
- """DOCDOC"""
+ """If there is a fixed exit server on the path, return a descriptor
+ for it; else return None."""
if self.path2:
return self.path2[-1].getFixedServer(directory,startAt,endAt)
else:
return None
+ def __str__(self):
+ p1s = map(str,self.path1)
+ p2s = map(str,self.path2)
+ if self.isSURB or self.isReply or self.lateSplit:
+ return ",".join(p1s+p2s)
+ else:
+ return "%s:%s"%(",".join(p1s), ",".join(p2s))
+
#----------------------------------------------------------------------
+WARN_STAR = 1 #XXXX007 remove
+
def parsePath(config, path, nHops=None, isReply=0, isSURB=0,
defaultNHops=None):
- """DOCDOC ; Returns a pathSpecifier. This documentation is no longer
- even vaguely accurate.
-
- Resolve a path as specified on the command line. Returns a
- (path-leg-1, path-leg-2) tuple, where each leg is a list of ServerInfo.
+ """Resolve a path as specified on the command line. Returns a
+ PathSpecifier object.
- directory -- the ClientDirectory to use.
config -- unused for now.
path -- the path, in a format described below. If the path is
- None, all servers are chosen as if the path were '*'.
- address -- the address to deliver the message to; if it specifies
- an exit node, the exit node is appended to the second leg of the
- path and does not count against the number of hops. If 'address'
- is None, the exit node must support relay.
+ None, all servers are chosen as if the path were '*<nHops>'.
nHops -- the number of hops to use. Defaults to defaultNHops.
startAt/endAt -- A time range during which all servers must be valid.
- halfPath -- If true, we generate only the second leg of the path
- and leave the first leg empty.
+ isSURB -- Boolean: is this a path for a reply block?
+ isReply -- Boolean: is this a path for a reply?
defaultNHops -- The default path length to use when we encounter a
wildcard in the path. Defaults to 6.
@@ -1186,7 +1326,7 @@
that number of randomly chosen servers:
'foo,bar,*2,quux'.
You can use a star without a number to specify a fill point
- where randomly-selected servers will be added:
+ where randomly-selected servers will be added: {DEPRECATED}
'foo,bar,*,quux'.
Finally, you can use a tilde followed by a number to specify an
approximate number of servers to add. (The actual number will be
@@ -1201,7 +1341,7 @@
"""
halfPath = isReply or isSURB
if not path:
- path = '*'
+ path = "*%d"%(nHops or defaultNHops or 6)
# Break path into a list of entries of the form:
# Nickname
# or "<swap>"
@@ -1266,6 +1406,10 @@
extraHops = max(myNHops-approxHops, 0)
pathEntries[starPos:starPos+1] =[RandomServersPathElement(n=extraHops)]
+ if WARN_STAR:
+ LOG.warn("'*' without a number is deprecated. Try '*%d' instead.",
+ extraHops)
+
# Figure out how long the first leg should be.
lateSplit = 0
if "<swap>" in pathEntries:
@@ -1287,6 +1431,9 @@
# Split the path into 2 legs.
path1, path2 = pathEntries[:firstLegLen], pathEntries[firstLegLen:]
+
+ # XXXX006 when checking lengths, if the specifier is something like ~5,
+ # XXXX006 we should convert it to something more like *2,~3.
if not lateSplit and not halfPath:
if len(path1)+len(path2) < 2:
raise UIError("The path must have at least 2 hops")
Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.128
retrieving revision 1.129
diff -u -d -r1.128 -r1.129
--- ClientMain.py 7 Nov 2003 10:43:18 -0000 1.128
+++ ClientMain.py 10 Nov 2003 04:12:20 -0000 1.129
@@ -153,12 +153,8 @@
#UserDir: ~/.mixminion
[Security]
-## Default length of forward message paths.
-#PathLength: 4
## Address to use by default when generating reply blocks
#SURBAddress: <your address here>
-## Default length of paths for reply blocks
-#SURBPathLength: 3
## Deault reply block lifetime
#SURBLifetime: 7 days
@@ -771,6 +767,12 @@
elif o in ('--no-queue',):
self.forceNoQueue = 1
+ if self.nHops and not self.path:
+ LOG.warn("-H/--hops is deprecated; use -P '*%d' instead",
+ self.nHops)
+ elif self.nHops:
+ LOG.warn("-H/--hops is deprecated")
+
def init(self):
"""Configure objects and initialize subsystems as specified by the
command line."""
@@ -1272,7 +1274,7 @@
# Collapse consecutive server descriptors with matching features.
if showTime < 2:
- featureMap = mixminion.ClientDirectory.compressServerList(
+ featureMap = mixminion.ClientDirectory.compressFeatureMap(
featureMap, ignoreGaps=(not showTime), terse=(not showTime))
# Now display the result.
Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.64
retrieving revision 1.65
diff -u -d -r1.64 -r1.65
--- Config.py 7 Nov 2003 10:43:18 -0000 1.64
+++ Config.py 10 Nov 2003 04:12:20 -0000 1.65
@@ -66,6 +66,7 @@
import mixminion.Common
import mixminion.Crypto
+import mixminion.NetUtils
from mixminion.Common import MixError, LOG, ceilDiv, englishSequence, \
formatBase64, isPrintingAscii, stripSpace, stringContains, UIError
@@ -167,6 +168,8 @@
return ilist
def _unparseIntervalList(lst):
+ """Helper function: given an interval list, converts it back to the
+ expected format."""
if lst == []:
return ""
r = [ (lst[0], 1) ]
@@ -225,75 +228,25 @@
idx += 1
size >>= 10
-# Regular expression to match a dotted quad.
-_ip_re = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
-
def _parseIP(ip):
"""Validation function. Converts a config value to an IP address.
Raises ConfigError on failure."""
- i = ip.strip()
-
- # inet_aton is a bit more permissive about spaces and incomplete
- # IP's than we want to be. Thus we use a regex to catch the cases
- # it doesn't.
- if not _ip_re.match(i):
- raise ConfigError("Invalid IP %r" % i)
try:
- socket.inet_aton(i)
- except socket.error:
- raise ConfigError("Invalid IP %r" % i)
-
- return i
-
-_IP6_CHARS="01233456789ABCDEFabcdef:."
+ return mixminion.NetUtils.normalizeIP4(ip)
+ except ValueError, e:
+ raise ConfigError(str(e))
def _parseIP6(ip6):
- """DOCDOC"""
- ip = ip6.strip()
- bad = ip6.translate(mixminion.Common._ALLCHARS, _IP6_CHARS)
- if bad:
- raise ConfigError("Invalid characters %r in address %r"%(bad,ip))
- if len(ip) < 2:
- raise ConfigError("IPv6 address %r is too short"%ip)
-
- items = ip.split(":")
- if not items:
- raise ConfigError("Empty IPv6 address")
- if items[:2] == ["",""]:
- del items[0]
- if items[-2:] == ["",""]:
- del items[-1]
- foundNils = 0
- foundWords = 0 # 16-bit words
-
- for item in items:
- if item == "":
- foundNils += 1
- elif '.' in item:
- _parseIP(item)
- if item is not items[-1]:
- raise ConfigError("Embedded IPv4 address %r must appear at end of IPv6 address %r"%(item,ip))
- foundWords += 2
- else:
- try:
- val = string.atoi(item,16)
- except ValueError:
- raise ConfigError("IPv6 word %r did not parse"%item)
- if not (0 <= val <= 0xFFFF):
- raise ConfigError("IPv6 word %r out of range"%item)
- foundWords += 1
-
- if foundNils > 1:
- raise ConfigError("Too many ::'s in IPv6 address %r"%ip)
- elif foundNils == 0 and foundWords < 8:
- raise ConfigError("IPv6 address %r is too short"%ip)
- elif foundWords > 8:
- raise ConfigError("IPv6 address %r is too long"%ip)
-
- return ip
+ """Validation function. Converts a config value to an IP address.
+ Raises ConfigError on failure."""
+ try:
+ return mixminion.NetUtils.normalizeIP6(ip6)
+ except ValueError, e:
+ raise ConfigError(str(e))
def _parseHost(host):
- """DOCDOC"""
+ """Validation function. Checks a config value as a valid hostname.
+ Raises ConfigError on failure."""
host = host.strip()
if not mixminion.Common.isPlausibleHostname(host):
raise ConfigError("%r doesn't look like a valid hostname",host)
@@ -553,6 +506,8 @@
return sections
def _readRestrictedConfigFile(contents):
+ """Same interface as _readConfigFile, but only supports the restrictd
+ file format as used by directories and descriptors."""
# List of (heading, [(key, val, lineno), ...])
sections = []
# [(key, val, lineno)] for the current section.
@@ -617,10 +572,21 @@
lines.append("") # so the last line ends with \n
return "\n".join(lines)
-
def resolveFeatureName(name, klass):
- """DOCDOC"""
- #XXXX006 this should be case insensitive.
+ """Given a feature name and a subclass of _ConfigFile, check whether
+ the feature exists, and return a sec/name tuple that, when passed to
+ _ConfigFile.getFeature, gives the value of the appropriate feature.
+ Raises a UIError if the feature name is invalid.
+
+ A feature is either: a special string handled by the class (like
+ 'caps' for ServerInfo), a special string handled outside the class
+ (like 'status' for ClientDirectory), a Section:Entry string, or an
+ Entry string. (If the Entry string is not unique within a section,
+ raises UIError.) All features are case-insensitive.
+
+ Example features are: 'caps', 'status', 'Incoming/MMTP:Version',
+ 'hostname'.
+ """
syn = klass._syntax
name = name.lower()
if klass._features.has_key(name):
@@ -681,6 +647,8 @@
# unrecognized key, or do we simply generate a warning?
# _restrictSections is 1/0: do we raise a ConfigError when we see an
# unrecognized section, or do we simply generate a warning?
+ # _features is a map from lowercase feature name to 1 for
+ # features that should be handled by getFeature.
## Validation rules:
# A key without a corresponding entry in _syntax gives an error.
@@ -719,7 +687,7 @@
}
_syntax = None
- _features = {}
+ _features = {}
_restrictFormat = 0
_restrictKeys = 1
_restrictSections = 1
@@ -890,7 +858,8 @@
return contents
def getFeature(self,sec,name):
- """DOCDOC"""
+ """Given a sec/name pair returned by resolveFeatureName, return a
+ string value of that feature for the class."""
assert sec not in ("+","-")
parseType = self._syntax[sec].get(name)[1]
_, unparseFn = self.CODING_FNS.get(parseType, (None,str))
@@ -959,9 +928,9 @@
'SURBAddress' : ('ALLOW', None, None),
'SURBPathLength' : ('ALLOW', "int", "4"),
'SURBLifetime' : ('ALLOW', "interval", "7 days"),
- 'ForwardPath' : ('ALLOW', None, "*"),
- 'ReplyPath' : ('ALLOW', None, "*"),
- 'SURBPath' : ('ALLOW', None, "*"),
+ 'ForwardPath' : ('ALLOW', None, "*6"),
+ 'ReplyPath' : ('ALLOW', None, "*4"),
+ 'SURBPath' : ('ALLOW', None, "*4"),
},
'Network' : { 'ConnectionTimeout' : ('ALLOW', "interval", None) }
}
@@ -1008,4 +977,4 @@
# in configure_trng and configureShredCommand, respectively.
# Host is checked in setupTrustedUIDs.
-
+
Index: NetUtils.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/NetUtils.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -d -r1.2 -r1.3
--- NetUtils.py 20 Oct 2003 18:19:19 -0000 1.2
+++ NetUtils.py 10 Nov 2003 04:12:20 -0000 1.3
@@ -8,14 +8,22 @@
__all__ = [ ]
import errno
+import re
import select
import signal
import socket
+import string
import time
-from mixminion.Common import LOG, TimeoutError
+from mixminion.Common import LOG, TimeoutError, _ALLCHARS
#======================================================================
+# Global vars
+
+# When we get IPv4 and IPv6 addresses for the same host, which do we use?
PREFER_INET4 = 1
+
+# Local copies of socket.AF_INET4 and socket.AF_INET6. (AF_INET6 may be
+# unsupported.)
AF_INET = socket.AF_INET
try:
AF_INET6 = socket.AF_INET6
@@ -32,7 +40,6 @@
#======================================================================
if hasattr(socket, 'getaddrinfo'):
def getIPs(name):
- """DOCDOC"""
r = []
ai = socket.getaddrinfo(name,None)
now = time.time()
@@ -45,8 +52,18 @@
def getIPs(name):
addr = socket.gethostbyname(name)
return [ (AF_INET, addr, time.time()) ]
-
+
+getIPs.__doc__ = \
+ """Resolve the hostname 'name' and return a list of answers. Each
+ answer is a 3-tuple of the form: (Family, Address, Time), where
+ Family is AF_INET or AF_INET6, Address is an IPv4 or IPv6 address,
+ and Time is the time at which the answer was returned. Raise
+ a subclass of socket.error if no answers are found."""
+
def getIP(name, preferIP4=PREFER_INET4):
+ """Resolve the hostname 'name' and return the 'best' answer. An
+ answer is either a 3-tuple as returned by getIPs, or a 3-tuple of
+ ('NOENT', reason, Time) if no answers were found."""
try:
r = getIPs(name)
inet4 = [ addr for addr in r if addr[0] == AF_INET ]
@@ -54,12 +71,14 @@
if not (inet4 or inet6):
LOG.error("getIP returned no inet addresses!")
return ("NOENT", "No inet addresses returned", time.time())
+ best4=best6=None
if inet4: best4=inet4[0]
if inet6: best6=inet6[0]
if preferIP4:
res = best4 or best6
else:
res = best6 or best4
+ assert res
protoname = (res[0] == AF_INET) and "inet" or "inet6"
LOG.trace("Result for getIP(%r): %s:%s (%d others dropped)",
name,protoname,res[1],len(r)-1)
@@ -75,7 +94,8 @@
_SOCKETS_SUPPORT_TIMEOUT = hasattr(socket.SocketType, "settimeout")
def connectWithTimeout(sock,dest,timeout=None):
- """DOCDOC; sock must be blocking."""
+ """Same as sock.connect, but timeout after 'timeout' seconds. This
+ functionality is built-in to Python2.3 and later."""
if timeout is None:
return sock.connect(dest)
elif _SOCKETS_SUPPORT_TIMEOUT:
@@ -119,16 +139,22 @@
_PREV_DEFAULT_TIMEOUT = None
def setAlarmTimeout(timeout):
+ """Begin a timeout with signal.alarm"""
if hasattr(signal, 'alarm'):
+ # Windows doesn't have signal.alarm.
def sigalrmHandler(sig,_): pass
signal.signal(signal.SIGALRM, sigalrmHandler)
signal.alarm(timeout)
def clearAlarmTimeout(timeout):
+ """End a timeout set with signal.alarm"""
if hasattr(signal, 'alarm'):
signal.alarm(0)
def setGlobalTimeout(timeout,noalarm=0):
+ """Set the global connection timeout to 'timeout' -- either with
+ signal.alarm or socket.setdefaulttimeout, whiche ever we support.
+ (If noalarm is true, don't use signal.alarm.)"""
global _PREV_DEFAULT_TIMEOUT
assert timeout > 0
if _SOCKETS_SUPPORT_TIMEOUT:
@@ -138,6 +164,7 @@
setAlarmTimeout(timeout)
def exceptionIsTimeout(ex):
+ """Return true iff ex is likely to be a timeout."""
if isinstance(ex, socket.error):
if ex[0] in IN_PROGRESS_ERRNOS:
return 1
@@ -146,6 +173,7 @@
return 0
def unsetGlobalTimeout(noalarm=0):
+ """Clear the global timeout."""
global _PREV_DEFAULT_TIMEOUT
if _SOCKETS_SUPPORT_TIMEOUT:
socket.setdefaulttimeout(_PREV_DEFAULT_TIMEOUT)
@@ -156,7 +184,8 @@
_PROTOCOL_SUPPORT = None
def getProtocolSupport():
- """DOCDOC"""
+ """Return a 2-tuple of booleans: do we support IPv4, and do we
+ support IPv6?"""
global _PROTOCOL_SUPPORT
if _PROTOCOL_SUPPORT is not None:
return _PROTOCOL_SUPPORT
@@ -176,5 +205,96 @@
if s is not None:
s.close()
- _PROTOCOL_SUPPORT = res
+ _PROTOCOL_SUPPORT = tuple(res)
return res
+
+#----------------------------------------------------------------------
+
+# Regular expression to match a dotted quad.
+_ip_re = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
+
+def normalizeIP4(ip):
+ """If IP is an IPv4 address, return it in canonical form. Raise
+ ValueError if it isn't."""
+
+ i = ip.strip()
+
+ # inet_aton is a bit more permissive about spaces and incomplete
+ # IP's than we want to be. Thus we use a regex to catch the cases
+ # it doesn't.
+ if not _ip_re.match(i):
+ raise ValueError("Invalid IP %r" % i)
+ try:
+ socket.inet_aton(i)
+ except socket.error:
+ raise ValueError("Invalid IP %r" % i)
+
+ return i
+
+_IP6_CHARS="01233456789ABCDEFabcdef:."
+
+def normalizeIP6(ip6):
+ """If IP is an IPv6 address, return it in canonical form. Raise
+ ValueError if it isn't."""
+ ip = ip6.strip()
+ bad = ip6.translate(_ALLCHARS, _IP6_CHARS)
+ if bad:
+ raise ValueError("Invalid characters %r in address %r"%(bad,ip))
+ if len(ip) < 2:
+ raise ValueError("IPv6 address %r is too short"%ip)
+
+ items = ip.split(":")
+ if not items:
+ raise ValueError("Empty IPv6 address")
+ if items[:2] == ["",""]:
+ del items[0]
+ if items[-2:] == ["",""]:
+ del items[-1]
+ foundNils = 0
+ foundWords = 0 # 16-bit words
+
+ for item in items:
+ if item == "":
+ foundNils += 1
+ elif '.' in item:
+ normalizeIP4(item)
+ if item is not items[-1]:
+ raise ValueError("Embedded IPv4 address %r must appear at end of IPv6 address %r"%(item,ip))
+ foundWords += 2
+ else:
+ try:
+ val = string.atoi(item,16)
+ except ValueError:
+ raise ValueError("IPv6 word %r did not parse"%item)
+ if not (0 <= val <= 0xFFFF):
+ raise ValueError("IPv6 word %r out of range"%item)
+ foundWords += 1
+
+ if foundNils > 1:
+ raise ValueError("Too many ::'s in IPv6 address %r"%ip)
+ elif foundNils == 0 and foundWords < 8:
+ raise ValueError("IPv6 address %r is too short"%ip)
+ elif foundWords > 8:
+ raise ValueError("IPv6 address %r is too long"%ip)
+
+ return ip
+
+def nameIsStaticIP(name):
+ """If 'name' is a static IPv4 or IPv6 address, return a 3-tuple as getIP
+ would return. Else return None."""
+ name = name.strip()
+ if ':' in name:
+ try:
+ val = normalizeIP6(name)
+ return (AF_INET6, val, time.time())
+ except ValueError, e:
+ return None
+ elif name and name[0].isdigit():
+ try:
+ val = normalizeIP4(name)
+ return (AF_INET, val, time.time())
+ except ValueError, e:
+ return None
+ else:
+ return None
+
Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.63
retrieving revision 1.64
diff -u -d -r1.63 -r1.64
--- Packet.py 19 Oct 2003 03:12:02 -0000 1.63
+++ Packet.py 10 Nov 2003 04:12:20 -0000 1.64
@@ -12,18 +12,20 @@
__all__ = [ 'compressData', 'CompressedDataTooLong', 'DROP_TYPE',
'ENC_FWD_OVERHEAD', 'ENC_SUBHEADER_LEN',
'encodeMailHeaders', 'encodeMessageHeaders',
- 'FRAGMENT_PAYLOAD_OVERHEAD', 'FWD_IPV4_TYPE', 'FragmentPayload',
+ 'FRAGMENT_PAYLOAD_OVERHEAD', 'FWD_HOST_TYPE', 'FWD_IPV4_TYPE',
+ 'FragmentPayload',
'FRAGMENT_MESSAGEID_LEN', 'FRAGMENT_TYPE',
'HEADER_LEN', 'IPV4Info', 'MAJOR_NO', 'MBOXInfo',
'MBOX_TYPE', 'MINOR_NO', 'MIN_EXIT_TYPE',
'MIN_SUBHEADER_LEN', 'MMTPHostInfo', 'Packet',
'OAEP_OVERHEAD', 'PAYLOAD_LEN', 'ParseError', 'ReplyBlock',
'ReplyBlock', 'SECRET_LEN', 'SINGLETON_PAYLOAD_OVERHEAD',
- 'SMTPInfo', 'SMTP_TYPE', 'SWAP_FWD_IPV4_TYPE', 'SingletonPayload',
+ 'SMTPInfo', 'SMTP_TYPE', 'SWAP_FWD_IPV4_TYPE',
+ 'SWAP_FWD_HOST_TYPE', 'SingletonPayload',
'Subheader', 'TAG_LEN', 'TextEncodedMessage',
'parseHeader', 'parseIPV4Info', 'parseMMTPHostInfo',
'parseMBOXInfo', 'parsePacket', 'parseMessageAndHeaders',
- 'parsePayload', 'parseReplyBlock',
+ 'parsePayload', 'parseRelayInfoByType', 'parseReplyBlock',
'parseReplyBlocks', 'parseSMTPInfo', 'parseSubheader',
'parseTextEncodedMessages', 'parseTextReplyBlocks',
'uncompressData'
@@ -556,15 +558,15 @@
# Routing info
def parseRelayInfoByType(routingType,routingInfo):
- """DOCDOC: Returns rt, (IPV4Info/MMTPHostInfo)."""
- if routingType in (mixminion.Packet.FWD_IPV4_TYPE,
- mixminion.Packet.SWAP_FWD_IPV4_TYPE):
- parseFn = mixminion.Packet.parseIPV4Info
- parsedType = mixminion.Packet.IPV4Info
- elif routingType in (mixminion.Packet.FWD_HOST_TYPE,
- mixminion.Packet.SWAP_FWD_HOST_TYPE):
- parseFn = mixminion.Packet.parseMMTPHost
- parsedType = mixminion.Packet.MMTPHostInfo
+ """Parse the routingInfo contained in the string 'routinginfo',
+ according to the type in 'routingType'. Only relay types are
+ supported."""
+ if routingType in (FWD_IPV4_TYPE, SWAP_FWD_IPV4_TYPE):
+ parseFn = parseIPV4Info
+ parsedType = IPV4Info
+ elif routingType in (FWD_HOST_TYPE, SWAP_FWD_HOST_TYPE):
+ parseFn = parseMMTPHostInfo
+ parsedType = MMTPHostInfo
else:
raise MixFatalError("Unrecognized relay type 0x%04X"%routingType)
if type(routingInfo) == types.StringType:
@@ -578,7 +580,7 @@
def parseIPV4Info(s):
"""Converts routing info for an IPV4 address into an IPV4Info object,
- suitable for use by FWD or SWAP_FWD modules."""
+ suitable for use by FWD_IPV4 or SWAP_FWD_IPV4 modules."""
if len(s) != 4+2+DIGEST_LEN:
raise ParseError("IPV4 information with wrong length (%d)" % len(s))
try:
@@ -589,11 +591,13 @@
return IPV4Info(ip, port, keyinfo)
class IPV4Info:
- """An IPV4Info object represents the routinginfo for a FWD or
- SWAP_FWD hop.
+ """An IPV4Info object represents the routinginfo for a FWD_IPV4 or
+ SWAP_FWD_IPV4 hop. This kind of routing is only used with older
+ servers that don't support hostname-based routing.
Fields: ip (a dotted quad string), port (an int from 0..65535),
and keyinfo (a digest)."""
+ #XXXX007/8 phase this out.
def __init__(self, ip, port, keyinfo):
"""Construct a new IPV4Info"""
assert 0 <= port <= 65535
@@ -630,7 +634,8 @@
MMTP_HOST_PAT = "!H%ds" % DIGEST_LEN
def parseMMTPHostInfo(s):
- """DOCDOC"""
+ """Converts routing info for a hostname address into an MMTPHostInfo
+ object, suitable for use by FWD_HOST or SWAP_FWD_HOST modules."""
if len(s) < 2+DIGEST_LEN+1:
raise ParseError("Routing information is too short.")
try:
@@ -643,7 +648,11 @@
return MMTPHostInfo(s[2+DIGEST_LEN:], port, keyinfo)
class MMTPHostInfo:
- """DOCDOC"""
+ """An MMTPHostInfo object represents the routinginfo for a FWD_HOST or
+ SWAP_FWD_HOST hop.
+
+ Fields: hostname, port (an int from 0..65535), and keyinfo (a
+ digest)."""
def __init__(self, hostname, port, keyinfo):
assert 0 <= port <= 65535
self.hostname = hostname.lower()
Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.60
retrieving revision 1.61
diff -u -d -r1.60 -r1.61
--- ServerInfo.py 7 Nov 2003 07:03:28 -0000 1.60
+++ ServerInfo.py 10 Nov 2003 04:12:20 -0000 1.61
@@ -250,11 +250,17 @@
return self['Server']['Digest']
def getIP(self):
- """Returns this server's IP address"""
+ """Returns this server's IP address. (Returns None for servers
+ running version 0.0.7 or later.)"""
return self['Incoming/MMTP'].get('IP')
def getHostname(self):
- """DOCDOC"""
+ """Return this server's Hostname. (Returns None for servers running
+ version 0.0.5 or earlier.)"""
+ #XXXX006 remove this. 0.0.6alpha1 could crash when it got hostnames.
+ #XXXX006 Sadly, some people installed it anyway.
+ if self['Server'].get("Software","").startswith("0.0.6alpha1"):
+ return None
return self['Incoming/MMTP'].get("Hostname")
def getPort(self):
@@ -269,37 +275,55 @@
"""Returns a hash of this server's MMTP key"""
return mixminion.Crypto.sha1(
mixminion.Crypto.pk_encode_public_key(self['Server']['Identity']))
- #return self['Incoming/MMTP']['Key-Digest']
def getIPV4Info(self):
"""Returns a mixminion.Packet.IPV4Info object for routing messages
- to this server."""
- return IPV4Info(self.getIP(), self.getPort(), self.getKeyDigest())
+ to this server. (Returns None for servers running version 0.0.5
+ or earlier.)"""
+ ip = self.getIP()
+ if ip is None: return None
+ return IPV4Info(ip, self.getPort(), self.getKeyDigest())
def getMMTPHostInfo(self):
- """DOCDOC"""
- return MMTPHostInfo(get.getHostname(), self.getPort(), self.getKeyDigest())
+ """Returns a mixminion.Packet.MMTPHostInfo object for routing messages
+ to this server. (Returns None for servers running version 0.0.7
+ or later.)"""
+ host = self.getHostname()
+ if host is None: return None
+ return MMTPHostInfo(host, self.getPort(), self.getKeyDigest())
def getRoutingInfo(self):
- return self.getIPV4Info()
+ """Return whichever of MMTPHostInfo or IPV4 info is best for
+ delivering to this server (assuming that the sending host
+ supports both."""
+ if self.getHostname():
+ return self.getMMTPHostInfo()
+ else:
+ return self.getIPV4Info()
def getIdentity(self):
+ """Return this server's public identity key."""
return self['Server']['Identity']
def getIncomingMMTPProtocols(self):
+ """Return a list of the MMTP versions supported by this this server
+ for incoming packets."""
inc = self['Incoming/MMTP']
if not inc.get("Version"):
return []
return [ s.strip() for s in inc["Protocols"].split(",") ]
def getOutgoingMMTPProtocols(self):
+ """Return a list of the MMTP versions supported by this this server
+ for outgoing packets."""
inc = self['Outgoing/MMTP']
if not inc.get("Version"):
return []
return [ s.strip() for s in inc["Protocols"].split(",") ]
def canRelayTo(self, otherDesc):
- """DOCDOC"""
+ """Return true iff this server can relay packets to the server
+ described by otherDesc."""
if self.hasSameNicknameAs(otherDesc):
return 1
myOutProtocols = self.getOutgoingMMTPProtocols()
@@ -310,7 +334,8 @@
return 0
def canStartAt(self):
- """DOCDOC"""
+ """Return true iff this server is one we (that is, this
+ version of Mixminion) can send packets to directly."""
myInProtocols = self.getIncomingMMTPProtocols()
for out in mixminion.MMTPClient.BlockingClientConnection.PROTOCOL_VERSIONS:
if out in myInProtocols:
@@ -318,8 +343,10 @@
return 0
def getRoutingFor(self, otherDesc, swap=0):
- """DOCDOC"""
- #XXXX006 use this!
+ """Return a 2-tuple of (routingType, routingInfo) for relaying
+ a packet from this server to the server described by
+ otherDesc. If swap is true, the relay is at a crossover
+ point."""
assert self.canRelayTo(otherDesc)
assert 0 <= swap <= 1
if self.getHostname() and otherDesc.getHostname():
@@ -334,7 +361,8 @@
return rt, ri
def getCaps(self):
- # FFFF refactor this once we have client addresses.
+ """Return a list of strings to describe this servers abilities in
+ a concise human-readable format."""
caps = []
if not self['Incoming/MMTP'].get('Version'):
return caps
@@ -350,11 +378,12 @@
return caps
def isSameDescriptorAs(self, other):
- """DOCDOC"""
+ """Return true iff this is the same server descriptor as other."""
return self.getDigest() == other.getDigest()
def hasSameNicknameAs(self, other):
- """DOCDOC"""
+ """Return true iff this server descriptor has the same nickname as
+ other."""
return self.getNickname().lower() == other.getNickname().lower()
def isValidated(self):
@@ -419,7 +448,7 @@
return valid.isEmpty()
def getFeature(self,sec,name):
- """DOCDOC"""
+ """Overrides getFeature from _ConfigFile."""
if sec == '-':
if name in ("caps", "capabilities"):
return " ".join(self.getCaps())
Index: __init__.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/__init__.py,v
retrieving revision 1.48
retrieving revision 1.49
diff -u -d -r1.48 -r1.49
--- __init__.py 5 Sep 2003 21:59:49 -0000 1.48
+++ __init__.py 10 Nov 2003 04:12:20 -0000 1.49
@@ -7,7 +7,7 @@
"""
# This version string is generated from setup.py; don't edit it.
-__version__ = "0.0.6alpha1"
+__version__ = "0.0.6alpha2"
# This 5-tuple encodes the version number for comparison. Don't edit it.
# The first 3 numbers are the version number; the 4th is:
# 0 for alpha
@@ -18,7 +18,7 @@
# The 4th or 5th number may be a string. If so, it is not meant to
# succeed or precede any other sub-version with the same a.b.c version
# number.
-version_info = (0, 0, 6, 0, 1)
+version_info = (0, 0, 6, 0, 2)
__all__ = [ 'server', 'directory' ]
def version_tuple_to_string(t):
Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.163
retrieving revision 1.164
diff -u -d -r1.163 -r1.164
--- test.py 8 Nov 2003 05:57:38 -0000 1.163
+++ test.py 10 Nov 2003 04:12:20 -0000 1.164
@@ -116,10 +116,10 @@
return i
return last
-def floatEq(f1,f2):
+def floatEq(f1,f2,tolerance=.00001):
"""Return true iff f1 is very close to f2."""
if min(f1, f2) != 0:
- return abs(f1-f2)/min(f1,f2) < .00001
+ return abs(f1-f2)/min(f1,f2) < tolerance
else:
return abs(f1-f2) < .00001
@@ -129,6 +129,22 @@
return "file:%s"%fname
#----------------------------------------------------------------------
+# DNS override
+def overrideDNS(overrideDict,delay=0):
+ """DOCDOC"""
+ def getIPs_replacement(addr,overrideDict=overrideDict,delay=delay):
+ v = overrideDict.get(addr)
+ if delay: time.sleep(delay)
+ if v is None:
+ raise socket.error, "not there"
+ elif '.' in v:
+ return [ (mixminion.NetUtils.AF_INET, v, time.time()) ]
+ else:
+ return [ (mixminion.NetUtils.AF_INET6, v, time.time()) ]
+
+ replaceAttribute(mixminion.NetUtils, "getIPs", getIPs_replacement)
+
+#----------------------------------------------------------------------
# RSA key caching functionality
# Map from (n, bits) to a RSA key with bits bits. Used to cache RSA keys
@@ -176,9 +192,9 @@
'assertFoo' functions."""
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
- def assertFloatEq(self, f1, f2):
+ def assertFloatEq(self, f1, f2, tolerance=.00001):
"""Fail unless f1 and f2 are very close to one another."""
- if not floatEq(f1, f2):
+ if not floatEq(f1, f2, tolerance):
self.fail("%s != %s" % (f1, f2))
def assertLongStringEq(self, s1, s2):
"""Fail unless the string s1 equals the string s2. If they aren't
@@ -1339,6 +1355,9 @@
self.assertEquals(IPV4Info("18.244.0.188", 48099, ri[-20:]).pack(),
ri)
+ self.assertEquals(inf, parseRelayInfoByType(FWD_IPV4_TYPE,inf.pack()))
+ self.assertEquals(inf, parseRelayInfoByType(SWAP_FWD_IPV4_TYPE,inf.pack()))
+
self.failUnlessRaises(ParseError, parseIPV4Info, ri[:-1])
self.failUnlessRaises(ParseError, parseIPV4Info, ri+"x")
@@ -1353,6 +1372,9 @@
self.assertEquals(MMTPHostInfo("the.hostname.is.here", 0x3055,
keyid).pack(), ri)
+ self.assertEquals(inf, parseRelayInfoByType(FWD_HOST_TYPE,inf.pack()))
+ self.assertEquals(inf, parseRelayInfoByType(SWAP_FWD_HOST_TYPE,inf.pack()))
+
self.failUnlessRaises(ParseError, parseMMTPHostInfo, "z")
self.failUnlessRaises(ParseError, parseMMTPHostInfo, "\x30\x55"+keyid)
self.failUnlessRaises(ParseError, parseMMTPHostInfo,
@@ -1662,6 +1684,78 @@
h[0].close()
#----------------------------------------------------------------------
+class NetUtilTests(TestCase):
+ def testGetIP(self):
+ overridedict = {}
+ if hasattr(socket, 'getaddrinfo'):
+ def override_getaddrinfo(name,port,overridedict=overridedict):
+ v = overridedict.get(name)
+ if v:
+ r = []
+ for addr in v:
+ if '.' in addr:
+ r.append( (mixminion.NetUtils.AF_INET, -1, -1, name, (addr,port) ) )
+ else:
+ r.append( (mixminion.NetUtils.AF_INET6, -1, -1, name, (addr,port)) )
+ return r
+ else:
+ raise socket.error, "foo"
+ replaceAttribute(socket, "getaddrinfo", override_getaddrinfo)
+ else:
+ def override_gethostbyname(name,overridedict=overridedict):
+ v = overridedict.get(name)
+ if v:
+ return v[0]
+ else:
+ raise socket.error, "foo"
+ replaceAttribute(socket, "gethostbyname", override_gethostbyname)
+
+ overridedict['revolving.restaurant'] = [ '30.1.0.50', "00FE::3" ]
+ now = time.time()
+ try:
+ r = mixminion.NetUtils.getIPs('revolving.restaurant')
+
+ self.assertEquals((socket.AF_INET, "30.1.0.50"), r[0][:2])
+ self.assertFloatEq(now, r[0][2])
+
+ if hasattr(socket, 'getaddrinfo'):
+ self.assertEquals(2, len(r))
+ self.assertEquals((mixminion.NetUtils.AF_INET6, "00FE::3"), r[1][:2])
+ self.assertFloatEq(now, r[1][2])
+ self.assertEquals((socket.AF_INET, "30.1.0.50"),
+ mixminion.NetUtils.getIP("revolving.restaurant",1)[:2])
+ self.assertEquals((mixminion.NetUtils.AF_INET6, "00FE::3"),
+ mixminion.NetUtils.getIP("revolving.restaurant",0)[:2])
+ else:
+ self.assertEquals(1, len(r))
+ self.assertEquals((socket.AF_INET, "30.1.0.50"),
+ mixminion.NetUtils.getIP("revolving.restaurant",0)[:2])
+ self.assertEquals((socket.AF_INET, "30.1.0.50"),
+ mixminion.NetUtils.getIP("revolving.restaurant",1)[:2])
+
+ self.assertRaises(socket.error, mixminion.NetUtils.getIPs,
+ "nowhere.nowhen.nohow")
+ self.assertEquals(("NOENT", "foo"),
+ mixminion.NetUtils.getIP("nowhere.nowhen.nohow")[:2])
+ finally:
+ undoReplacedAttributes()
+
+ def testGetProtocolSupport(self):
+ ps = mixminion.NetUtils.getProtocolSupport()
+ self.assertEquals(len(ps),2)
+ self.assertEquals(ps[0], 1) # IPv4 support is required.
+ self.assert_(ps[1] in (0,1))
+
+ def testNameIsStaticIP(self):
+ nisi = mixminion.NetUtils.nameIsStaticIP
+ from mixminion.NetUtils import AF_INET, AF_INET6
+ self.assertEquals(nisi("foo"), None)
+ self.assertEquals(nisi("18.244.0.0")[:2], (AF_INET, "18.244.0.0"))
+ self.assertEquals(nisi("::F00f")[:2], (AF_INET6, "::F00f"))
+ self.assertEquals(nisi("4starts-with-digit.tld"), None)
+ self.assertEquals(nisi("bogus-with-a-colon:wow"), None)
+
+#----------------------------------------------------------------------
# Dummy PRNG class that just returns 0-valued bytes. We use this to make
# generated padding predictable in our BuildMessage tests below.
@@ -1689,6 +1783,10 @@
return IPV4Info(self.addr, self.port, self.keyid)
def getIPV4Info(self):
return self.getRoutingInfo()
+ def getRoutingFor(self,other,swap):
+ tp = [FWD_IPV4_TYPE,SWAP_FWD_IPV4_TYPE][swap]
+ return (tp, other.getRoutingInfo().pack())
+
class BuildMessageTests(TestCase):
def setUp(self):
@@ -2709,11 +2807,12 @@
# (We temporarily override the setting from 'BuildMessage',
# not Packet; BuildMessage has already imported a copy of this
# constant.)
- save = mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE
- mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE = 50
+ global SWAP_FWD_IPV4_TYPE# override the copy used by FakeServerInfo
+ save = SWAP_FWD_IPV4_TYPE
+ SWAP_FWD_IPV4_TYPE = 50
m_x = bfm(zPayload, 500, "", [self.server1], [self.server2])
finally:
- mixminion.BuildMessage.SWAP_FWD_IPV4_TYPE = save
+ SWAP_FWD_IPV4_TYPE = save
self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
# Subhead with bad length
@@ -2724,7 +2823,6 @@
m_x = pk_encrypt("f"*(256-42), self.pk1)+m[256:]
self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
-
# Bad IPV4 info
subh_real = pk_decrypt(m[:256], self.pk1)
subh = parseSubheader(subh_real)
@@ -4458,6 +4556,11 @@
eq(info['Server']['Published'], loaded['Server']['Published'])
eq(info.isValidated(), loaded.isValidated())
+ # Other functionality.
+ eq(info.getIPV4Info(), IPV4Info("192.168.0.1", 48099, info.getKeyDigest()))
+ eq(info.getMMTPHostInfo(), MMTPHostInfo("Theserver", 48099, info.getKeyDigest()))
+ eq(info.getMMTPHostInfo(), info.getRoutingInfo())
+ self.assert_(info.canStartAt())
#XXXX006 this is a workaround to deal with the fact that we've
#XXXX006 opened a fragment DB just to configure the server. Not
@@ -4563,11 +4666,21 @@
eq(info3['Incoming/MMTP']['IP'], "192.168.100.3")
self.assert_('smtp' in info3.getCaps())
+ # Check routing info
+ self.assert_(info.canRelayTo(info))
+ self.assert_(info.getRoutingFor(info), info.getRoutingInfo())
+ self.assert_(info.canRelayTo(info3))
+ self.assert_(not info3.canRelayTo(info)) # info3 has no outgoing/mmtp
+ self.assertEquals(info.getRoutingFor(info3)[1],
+ info3.getRoutingInfo().pack())
+ #XXXX006 Test negative (IPv4) cases, somehow.
+
key3.regenerateServerDescriptor(conf2, identity)
info3 = key3.getServerDescriptor()
eq(info3['Incoming/MMTP']['Hostname'], "Theserver4")
eq(info3['Incoming/MMTP']['IP'], "192.168.100.4")
+
def test_directory(self):
eq = self.assertEquals
examples = getExampleServerDescriptors()
@@ -5765,9 +5878,62 @@
# Test getTLSContext
keyring._getTLSContext()
+#----------------------------------------------------------------------
+class DNSFarmTests(TestCase):
+ def testDNSCache(self):
+ import mixminion.server.DNSFarm
+ cache = mixminion.server.DNSFarm.DNSCache()
+ receiveDict = {}
+ lock = threading.RLock()
+ def callback(name,val,receiveDict=receiveDict,lock=lock):
+ lock.acquire()
+ receiveDict[name]=val
+ lock.release()
+ try:
+ DELAY = 0.2
+ overrideDNS({'foo' : '10.2.4.11',
+ 'bar' : '18:0FFF::4:1',
+ 'baz.com': '10.99.22.8'},
+ delay=DELAY)
+ self.assertEquals(None, cache.getNonblocking("foo"))
+ start = time.time()
+ cache.lookup('foo',callback)
+ cache.lookup('bar',callback)
+ self.assertEquals(cache.getNonblocking("bar"),
+ mixminion.server.DNSFarm.PENDING)
+ time.sleep(DELAY/4)
+ cache.lookup('baz.com',callback)
+ cache.lookup('nowhere.noplace',callback)
+ cache.lookup('1.2.3.4', callback)
+ while len(receiveDict)<5:
+ time.sleep(DELAY/4)
+ self.assertEquals(receiveDict['foo'][:2],
+ (socket.AF_INET, '10.2.4.11'))
+ self.assertEquals(receiveDict['bar'][:2],
+ (mixminion.NetUtils.AF_INET6, '18:0FFF::4:1'))
+ self.assertFloatEq(receiveDict['foo'][2]-start, DELAY, .3)
+ self.assertFloatEq(receiveDict['bar'][2]-start, DELAY, .3)
+ self.assertEquals(receiveDict['nowhere.noplace'][0], "NOENT")
+ self.assertEquals(cache.getNonblocking("foo"),
+ receiveDict['foo'])
+ self.assertEquals(cache.getNonblocking("baz.com")[:2],
+ (socket.AF_INET, '10.99.22.8'))
+ self.assertFloatEq(receiveDict['baz.com'][2]-start, DELAY*1.25, .3)
+ cache.cleanCache(receiveDict['foo'][2]+
+ mixminion.server.DNSFarm.MAX_ENTRY_TTL+.001)
+ self.assertEquals(cache.getNonblocking('foo'), None)
+ self.assertEquals(cache.getNonblocking('nowhere.noplace'),
+ receiveDict['nowhere.noplace'])
+ self.assertEquals(receiveDict['1.2.3.4'][:2],
+ (socket.AF_INET, '1.2.3.4'))
+ cache.shutdown(wait=1)
+ self.assertEquals(5, len(receiveDict))
+ finally:
+ undoReplacedAttributes()
+
#----------------------------------------------------------------------
class ServerMainTests(TestCase):
@@ -5950,7 +6116,6 @@
# variable to hold the latest instance of FakeBCC.
BCC_INSTANCE = None
-
class ClientUtilTests(TestCase):
def testEncryptedFiles(self):
CU = mixminion.ClientUtils
@@ -6351,6 +6516,9 @@
return paths[0]
else:
return paths
+
+ #XXXX007 remove
+ mixminion.ClientDirectory.WARN_STAR = 0
paddr = mixminion.ClientDirectory.parseAddress
email = paddr("smtp:lloyd@dobler.com")
@@ -6766,6 +6934,7 @@
replaceAttribute(mixminion.MMTPClient, "BlockingClientConnection",
FakeBCC)
+ overrideDNS({'alice' : "10.0.0.100"})
try:
client.sendForwardMessage(
directory,
@@ -6774,8 +6943,9 @@
"You only give me your information.",
time.time(), time.time()+300)
bcc = BCC_INSTANCE
+
# first hop is alice
- self.assertEquals(bcc.addr, "10.0.0.9")
+ self.assertEquals(bcc.addr, "10.0.0.100")
self.assertEquals(bcc.port, 48099)
self.assertEquals(0, bcc.connected)
self.assertEquals(1, len(bcc.packets))
@@ -7001,7 +7171,7 @@
tc = loader.loadTestsFromTestCase
if 0:
- suite.addTest(tc(ClientDirectoryTests))
+ suite.addTest(tc(PacketTests))
return suite
testClasses = [MiscTests,
MinionlibCryptoTests,
@@ -7018,6 +7188,8 @@
FragmentTests,
QueueTests,
EventStatsTests,
+ NetUtilTests,
+ DNSFarmTests,
ModuleTests,
ClientDirectoryTests,