aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRob Austein <sra@hactrn.net>2014-07-07 23:13:35 +0000
committerRob Austein <sra@hactrn.net>2014-07-07 23:13:35 +0000
commit13a65b463cd0acedd3bc36c9437d5ee8b2e26b60 (patch)
treed129b276d8e77591943d03f1995ac76c3eb6d5f8
parentbfba2f0ce8f8416b9e5f91542068d0d6470bc19f (diff)
Checkpoint of SQL-based publish and withdraw processing. Doesn't
handle publish-with-overwrite correctly yet, not generating RRDP files yet, but passes "make test" without doing anything obviously insane. svn path=/branches/tk705/; revision=5887
-rw-r--r--ca/tests/yamltest.py27
-rw-r--r--rpki/adns.py14
-rw-r--r--rpki/async.py16
-rw-r--r--rpki/config.py5
-rw-r--r--rpki/csv_utils.py2
-rw-r--r--rpki/gui/app/check_expired.py6
-rw-r--r--rpki/gui/app/forms.py17
-rw-r--r--rpki/gui/app/glue.py3
-rw-r--r--rpki/gui/app/models.py14
-rwxr-xr-xrpki/gui/app/range_list.py2
-rw-r--r--rpki/gui/app/views.py41
-rw-r--r--rpki/gui/cacheview/models.py8
-rw-r--r--rpki/gui/cacheview/tests.py1
-rw-r--r--rpki/gui/cacheview/util.py3
-rw-r--r--rpki/gui/cacheview/views.py1
-rw-r--r--rpki/gui/decorators.py2
-rw-r--r--rpki/gui/default_settings.py1
-rw-r--r--rpki/gui/models.py4
-rw-r--r--rpki/gui/routeview/api.py2
-rw-r--r--rpki/gui/routeview/util.py2
-rw-r--r--rpki/gui/script_util.py1
-rw-r--r--rpki/http.py47
-rw-r--r--rpki/ipaddrs.py9
-rw-r--r--rpki/irdb/models.py1
-rw-r--r--rpki/irdb/zookeeper.py1
-rw-r--r--rpki/left_right.py59
-rw-r--r--rpki/pubd.py151
-rw-r--r--rpki/publication.py27
-rw-r--r--rpki/publication_control.py9
-rw-r--r--rpki/rcynic.py4
-rw-r--r--rpki/resource_set.py48
-rw-r--r--rpki/rootd.py1
-rw-r--r--rpki/rpkid.py66
-rw-r--r--rpki/sql.py20
-rw-r--r--rpki/sql_schemas.py29
-rw-r--r--rpki/sundial.py15
-rw-r--r--rpki/up_down.py59
-rw-r--r--rpki/x509.py127
-rw-r--r--rpki/xml_utils.py38
-rw-r--r--schemas/sql/pubd.sql27
40 files changed, 814 insertions, 96 deletions
diff --git a/ca/tests/yamltest.py b/ca/tests/yamltest.py
index 1482c4e2..a97e2554 100644
--- a/ca/tests/yamltest.py
+++ b/ca/tests/yamltest.py
@@ -67,6 +67,7 @@ def cleanpath(*names):
"""
Construct normalized pathnames.
"""
+
return os.path.normpath(os.path.join(*names))
# Pathnames for various things we need
@@ -110,6 +111,7 @@ class roa_request(object):
"""
Parse a ROA request from YAML format.
"""
+
return cls(y.get("asn"), y.get("ipv4"), y.get("ipv6"))
@@ -180,6 +182,7 @@ class allocation_db(list):
"""
Show contents of allocation database.
"""
+
for a in self:
a.dump()
@@ -210,6 +213,7 @@ class allocation(object):
"""
Allocate a TCP port.
"""
+
cls.base_port += 1
return cls.base_port
@@ -221,6 +225,7 @@ class allocation(object):
Allocate an engine number, mostly used to construct MySQL database
names.
"""
+
cls.base_engine += 1
return cls.base_engine
@@ -275,6 +280,7 @@ class allocation(object):
Compute resource closure of this node and its children, to avoid a
lot of tedious (and error-prone) duplication in the YAML file.
"""
+
resources = self.base
for kid in self.kids:
resources |= kid.closure()
@@ -285,6 +291,7 @@ class allocation(object):
"""
Show content of this allocation node.
"""
+
print str(self)
def __str__(self):
@@ -309,6 +316,7 @@ class allocation(object):
"""
Is this the root node?
"""
+
return self.parent is None
@property
@@ -316,6 +324,7 @@ class allocation(object):
"""
Is this entity hosted?
"""
+
return self.hosted_by is not None
@property
@@ -323,18 +332,21 @@ class allocation(object):
"""
Does this entity run a pubd?
"""
+
return self.is_root or not (self.is_hosted or only_one_pubd)
def path(self, *names):
"""
Construct pathnames in this entity's test directory.
"""
+
return cleanpath(test_dir, self.host.name, *names)
def csvout(self, fn):
"""
Open and log a CSV output file.
"""
+
path = self.path(fn)
print "Writing", path
return rpki.csv_utils.csv_writer(path)
@@ -343,6 +355,7 @@ class allocation(object):
"""
Construct service URL for this node's parent.
"""
+
return "http://localhost:%d/up-down/%s/%s" % (self.parent.host.rpkid_port,
self.parent.name,
self.name)
@@ -351,6 +364,7 @@ class allocation(object):
"""
Write Autonomous System Numbers CSV file.
"""
+
fn = "%s.asns.csv" % d.name
if not args.skip_config:
f = self.csvout(fn)
@@ -364,6 +378,7 @@ class allocation(object):
"""
Write prefixes CSV file.
"""
+
fn = "%s.prefixes.csv" % d.name
if not args.skip_config:
f = self.csvout(fn)
@@ -377,6 +392,7 @@ class allocation(object):
"""
Write ROA CSV file.
"""
+
fn = "%s.roas.csv" % d.name
if not args.skip_config:
f = self.csvout(fn)
@@ -391,6 +407,7 @@ class allocation(object):
"""
Write Ghostbusters vCard file.
"""
+
if self.ghostbusters:
fn = "%s.ghostbusters.vcard" % d.name
if not args.skip_config:
@@ -409,6 +426,7 @@ class allocation(object):
"""
Write EE certificates (router certificates, etc).
"""
+
if self.router_certs:
fn = "%s.routercerts.xml" % d.name
if not args.skip_config:
@@ -432,6 +450,7 @@ class allocation(object):
"""
Walk up tree until we find somebody who runs pubd.
"""
+
s = self
while not s.runs_pubd:
s = s.parent
@@ -442,6 +461,7 @@ class allocation(object):
"""
Work out what pubd configure_publication_client will call us.
"""
+
path = []
s = self
if not args.flat_publication:
@@ -537,6 +557,7 @@ class allocation(object):
"""
Run rpkic for this entity.
"""
+
cmd = [prog_rpkic, "-i", self.name, "-c", self.path("rpki.conf")]
if args.profile:
cmd.append("--profile")
@@ -552,6 +573,7 @@ class allocation(object):
Start a Python daemon and return a subprocess.Popen object
representing the running daemon.
"""
+
basename = os.path.splitext(os.path.basename(prog))[0]
cmd = [prog, "--foreground", "--log-level", "debug",
"--log-file", self.path(basename + ".log"),
@@ -567,30 +589,35 @@ class allocation(object):
"""
Run rpkid.
"""
+
return self.run_python_daemon(prog_rpkid)
def run_irdbd(self):
"""
Run irdbd.
"""
+
return self.run_python_daemon(prog_irdbd)
def run_pubd(self):
"""
Run pubd.
"""
+
return self.run_python_daemon(prog_pubd)
def run_rootd(self):
"""
Run rootd.
"""
+
return self.run_python_daemon(prog_rootd)
def run_rsyncd(self):
"""
Run rsyncd.
"""
+
p = subprocess.Popen(("rsync", "--daemon", "--no-detach", "--config", "rsyncd.conf"),
cwd = self.path())
print "Running rsyncd for %s: pid %d process %r" % (self.name, p.pid, p)
diff --git a/rpki/adns.py b/rpki/adns.py
index 968684b5..018bb7cf 100644
--- a/rpki/adns.py
+++ b/rpki/adns.py
@@ -88,6 +88,7 @@ class dispatcher(asyncore.dispatcher):
"""
Receive a packet, hand it off to query class callback.
"""
+
wire, from_address = self.recvfrom(self.bufsize)
self.cb(self.af, from_address[0], from_address[1], wire)
@@ -95,18 +96,21 @@ class dispatcher(asyncore.dispatcher):
"""
Pass errors to query class errback.
"""
+
self.eb(sys.exc_info()[1])
def handle_connect(self):
"""
Quietly ignore UDP "connection" events.
"""
+
pass
def writable(self):
"""
We don't need to hear about UDP socket becoming writable.
"""
+
return False
@@ -138,6 +142,7 @@ class query(object):
query; if we find an answer there, just return it. Otherwise
start the network query.
"""
+
if resolver.cache:
answer = resolver.cache.get((self.qname, self.qtype, self.qclass))
else:
@@ -161,6 +166,7 @@ class query(object):
Outer loop. If we haven't got a response yet and still have
nameservers to check, start inner loop. Otherwise, we're done.
"""
+
self.timer.cancel()
if self.response is None and self.nameservers:
self.iterator = rpki.async.iterator(self.nameservers[:], self.loop2, self.done2)
@@ -172,6 +178,7 @@ class query(object):
Inner loop. Send query to next nameserver in our list, unless
we've hit the overall timeout for this query.
"""
+
self.timer.cancel()
try:
timeout = resolver._compute_timeout(self.start)
@@ -191,6 +198,7 @@ class query(object):
"""
No answer from nameserver, move on to next one (inner loop).
"""
+
self.response = None
self.iterator()
@@ -200,6 +208,7 @@ class query(object):
error, handle as if we've timed out on this nameserver; otherwise,
pass error back to caller.
"""
+
self.timer.cancel()
if isinstance(e, socket.error):
self.response = None
@@ -215,6 +224,7 @@ class query(object):
we're done, otherwise handle error appropriately and move on to
next nameserver.
"""
+
sender = (af, dns.inet.inet_pton(af, from_host))
if from_port != resolver.port or sender not in self.nameservers:
return
@@ -240,6 +250,7 @@ class query(object):
while before starting the cycle again, unless we've hit the
timeout threshold for the whole query.
"""
+
if self.response is None and self.nameservers:
try:
delay = rpki.sundial.timedelta(seconds = min(resolver._compute_timeout(self.start), self.backoff))
@@ -256,6 +267,7 @@ class query(object):
"""
Shut down our timer and sockets.
"""
+
self.timer.cancel()
for s in self.sockets.itervalues():
s.close()
@@ -264,6 +276,7 @@ class query(object):
"""
Something bad happened. Clean up, then pass error back to caller.
"""
+
self.cleanup()
self.eb(self, e)
@@ -273,6 +286,7 @@ class query(object):
pass it back to caller; if we got an error, pass the appropriate
exception back to caller.
"""
+
self.cleanup()
try:
if not self.nameservers:
diff --git a/rpki/async.py b/rpki/async.py
index da4b88b4..90117476 100644
--- a/rpki/async.py
+++ b/rpki/async.py
@@ -131,6 +131,7 @@ class timer(object):
"""
Debug logging.
"""
+
if self.gc_debug:
bt = traceback.extract_stack(limit = 3)
logger.debug("%s from %s:%d", msg, bt[0][0], bt[0][1])
@@ -140,6 +141,7 @@ class timer(object):
Set a timer. Argument can be a datetime, to specify an absolute
time, or a timedelta, to specify an offset time.
"""
+
if self.gc_debug:
self.trace("Setting %r to %r" % (self, when))
if isinstance(when, rpki.sundial.timedelta):
@@ -162,6 +164,7 @@ class timer(object):
"""
Cancel a timer, if it was set.
"""
+
if self.gc_debug:
self.trace("Canceling %r" % self)
try:
@@ -174,6 +177,7 @@ class timer(object):
"""
Test whether this timer is currently set.
"""
+
return self in timer_queue
def set_handler(self, handler):
@@ -184,12 +188,14 @@ class timer(object):
bound method to an object in a class representing a network
connection).
"""
+
self.handler = handler
def set_errback(self, errback):
"""
Set a timer's errback. Like set_handler(), for errbacks.
"""
+
self.errback = errback
@classmethod
@@ -202,6 +208,7 @@ class timer(object):
called, so that even if new events keep getting scheduled, we'll
return to the I/O loop reasonably quickly.
"""
+
now = rpki.sundial.now()
while timer_queue and now >= timer_queue[0].when:
t = timer_queue.pop(0)
@@ -233,6 +240,7 @@ class timer(object):
the same units (argh!), and we're not doing anything that
hair-triggered, so rounding up is simplest.
"""
+
if not timer_queue:
return None
now = rpki.sundial.now()
@@ -251,6 +259,7 @@ class timer(object):
queue content, but this way we can notify subclasses that provide
their own cancel() method.
"""
+
while timer_queue:
timer_queue.pop(0).cancel()
@@ -258,12 +267,14 @@ def _raiseExitNow(signum, frame):
"""
Signal handler for event_loop().
"""
+
raise ExitNow
def exit_event_loop():
"""
Force exit from event_loop().
"""
+
raise ExitNow
def event_defer(handler, delay = rpki.sundial.timedelta(seconds = 0)):
@@ -271,6 +282,7 @@ def event_defer(handler, delay = rpki.sundial.timedelta(seconds = 0)):
Use a near-term (default: zero interval) timer to schedule an event
to run after letting the I/O system have a turn.
"""
+
timer(handler).set(delay)
## @var debug_event_timing
@@ -282,6 +294,7 @@ def event_loop(catch_signals = (signal.SIGINT, signal.SIGTERM)):
"""
Replacement for asyncore.loop(), adding timer and signal support.
"""
+
old_signal_handlers = {}
while True:
save_sigs = len(old_signal_handlers) == 0
@@ -346,6 +359,7 @@ class sync_wrapper(object):
Wrapped code has requested normal termination. Store result, and
exit the event loop.
"""
+
self.res = res
raise ExitNow
@@ -354,6 +368,7 @@ class sync_wrapper(object):
Wrapped code raised an exception. Store exception data, then exit
the event loop.
"""
+
exc_info = sys.exc_info()
self.err = exc_info if exc_info[1] is err else err
raise ExitNow
@@ -394,6 +409,7 @@ class gc_summary(object):
"""
Collect and log GC state for this period, reset timer.
"""
+
logger.debug("gc_summary: Running gc.collect()")
gc.collect()
logger.debug("gc_summary: Summarizing (threshold %d)", self.threshold)
diff --git a/rpki/config.py b/rpki/config.py
index f38427c4..0e30982b 100644
--- a/rpki/config.py
+++ b/rpki/config.py
@@ -141,6 +141,7 @@ class parser(object):
Replacement function for indirect variable substitution.
This is intended for use with re.subn().
"""
+
section, option = m.group(1, 2)
if section == "ENV":
return os.getenv(option, "")
@@ -151,6 +152,7 @@ class parser(object):
"""
Get an option, perhaps with a default value.
"""
+
if section is None:
section = self.default_section
if default is not None and not self.cfg.has_option(section, option):
@@ -165,6 +167,7 @@ class parser(object):
"""
Get a boolean option, perhaps with a default value.
"""
+
v = self.get(option, default, section)
if isinstance(v, str):
v = v.lower()
@@ -177,12 +180,14 @@ class parser(object):
"""
Get an integer option, perhaps with a default value.
"""
+
return int(self.get(option, default, section))
def getlong(self, option, default = None, section = None):
"""
Get a long integer option, perhaps with a default value.
"""
+
return long(self.get(option, default, section))
def set_global_flags(self):
diff --git a/rpki/csv_utils.py b/rpki/csv_utils.py
index 9ba04a02..9034e96b 100644
--- a/rpki/csv_utils.py
+++ b/rpki/csv_utils.py
@@ -99,6 +99,7 @@ class csv_writer(object):
"""
Close this writer.
"""
+
if self.file is not None:
self.file.close()
self.file = None
@@ -109,4 +110,5 @@ class csv_writer(object):
"""
Fake inheritance from whatever object csv.writer deigns to give us.
"""
+
return getattr(self.writer, attr)
diff --git a/rpki/gui/app/check_expired.py b/rpki/gui/app/check_expired.py
index a084af79..2907f071 100644
--- a/rpki/gui/app/check_expired.py
+++ b/rpki/gui/app/check_expired.py
@@ -41,8 +41,8 @@ def check_cert(handle, p, errs):
The displayed object name defaults to the class name, but can be overridden
using the `object_name` argument.
-
"""
+
t = p.certificate.getNotAfter()
if t <= expire_time:
e = 'expired' if t <= now else 'will expire'
@@ -102,8 +102,8 @@ def check_expire(conf, errs):
def check_child_certs(conf, errs):
"""Fetch the list of published objects from rpkid, and inspect the issued
resource certs (uri ending in .cer).
-
"""
+
z = Zookeeper(handle=conf.handle)
req = list_published_objects_elt.make_pdu(action="list",
tag="list_published_objects",
@@ -139,8 +139,8 @@ def notify_expired(expire_days=14, from_email=None):
expire_days: the number of days ahead of today to warn
from_email: set the From: address for the email
-
"""
+
global expire_time # so i don't have to pass it around
global now
diff --git a/rpki/gui/app/forms.py b/rpki/gui/app/forms.py
index 5394a804..02561303 100644
--- a/rpki/gui/app/forms.py
+++ b/rpki/gui/app/forms.py
@@ -52,6 +52,7 @@ class GhostbusterRequestForm(forms.ModelForm):
Generate a ModelForm with the subset of parents for the current
resource handle.
"""
+
# override default form field
parent = forms.ModelChoiceField(queryset=None, required=False,
help_text='Specify specific parent, or none for all parents')
@@ -86,6 +87,7 @@ class GhostbusterRequestForm(forms.ModelForm):
class ImportForm(forms.Form):
"""Form used for uploading parent/child identity xml files."""
+
handle = forms.CharField(required=False,
widget=forms.TextInput(attrs={'class': 'xlarge'}),
help_text='Optional. Your name for this entity, or blank to accept name in XML')
@@ -101,6 +103,7 @@ class ImportRepositoryForm(forms.Form):
class ImportClientForm(forms.Form):
"""Form used for importing publication client requests."""
+
xml = forms.FileField(label='XML file')
@@ -137,6 +140,7 @@ class UserCreateForm(forms.Form):
class UserEditForm(forms.Form):
"""Form for editing a user."""
+
email = forms.CharField()
pw = forms.CharField(widget=forms.PasswordInput, label='Password',
required=False)
@@ -185,8 +189,8 @@ class ROARequest(forms.Form):
"""Takes an optional `conf` keyword argument specifying the user that
is creating the ROAs. It is used for validating that the prefix the
user entered is currently allocated to that user.
-
"""
+
conf = kwargs.pop('conf', None)
kwargs['auto_id'] = False
super(ROARequest, self).__init__(*args, **kwargs)
@@ -199,8 +203,8 @@ class ROARequest(forms.Form):
rpki.resource_set.resource_range_ip object.
If there is no mask provided, assume the closest classful mask.
-
"""
+
prefix = self.cleaned_data.get('prefix')
if '/' not in prefix:
p = IPAddress(prefix)
@@ -296,7 +300,6 @@ class AddASNForm(forms.Form):
Returns a forms.Form subclass which verifies that the entered ASN range
does not overlap with a previous allocation to the specified child, and
that the ASN range is within the range allocated to the parent.
-
"""
asns = forms.CharField(
@@ -335,8 +338,8 @@ class AddNetForm(forms.Form):
Returns a forms.Form subclass which validates that the entered address
range is within the resources allocated to the parent, and does not overlap
with what is already allocated to the specified child.
-
"""
+
address_range = forms.CharField(
help_text='CIDR or range',
widget=forms.TextInput(attrs={'autofocus': 'true'})
@@ -383,7 +386,6 @@ def ChildForm(instance):
This is roughly based on the equivalent ModelForm, but uses Form as a base
class so that selection boxes for the AS and Prefixes can be edited in a
single form.
-
"""
class _wrapped(forms.Form):
@@ -401,11 +403,13 @@ def ChildForm(instance):
class Empty(forms.Form):
"""Stub form for views requiring confirmation."""
+
pass
class ResourceHolderForm(forms.Form):
"""form for editing ACL on Conf objects."""
+
users = forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
help_text='users allowed to mange this resource holder'
@@ -413,7 +417,8 @@ class ResourceHolderForm(forms.Form):
class ResourceHolderCreateForm(forms.Form):
- """form for creating new resource holdres."""
+ """form for creating new resource holders."""
+
handle = forms.CharField(max_length=30)
parent = forms.ModelChoiceField(
required=False,
diff --git a/rpki/gui/app/glue.py b/rpki/gui/app/glue.py
index 0bf5f942..f17ba5ac 100644
--- a/rpki/gui/app/glue.py
+++ b/rpki/gui/app/glue.py
@@ -16,7 +16,6 @@
"""
This file contains code that interfaces between the django views implementing
the portal gui and the rpki.* modules.
-
"""
from __future__ import with_statement
@@ -39,6 +38,7 @@ from django.db.transaction import commit_on_success
def ghostbuster_to_vcard(gbr):
"""Convert a GhostbusterRequest object into a vCard object."""
+
import vobject
vcard = vobject.vCard()
@@ -86,7 +86,6 @@ def list_received_resources(log, conf):
The semantics are to clear the entire table and populate with the list of
certs received. Other models should not reference the table directly with
foreign keys.
-
"""
z = Zookeeper(handle=conf.handle)
diff --git a/rpki/gui/app/models.py b/rpki/gui/app/models.py
index 32a897c7..d6332796 100644
--- a/rpki/gui/app/models.py
+++ b/rpki/gui/app/models.py
@@ -120,16 +120,16 @@ class Conf(rpki.irdb.models.ResourceHolderCA):
def parents(self):
"""Simulates irdb.models.Parent.objects, but returns app.models.Parent
proxy objects.
-
"""
+
return Parent.objects.filter(issuer=self)
@property
def children(self):
"""Simulates irdb.models.Child.objects, but returns app.models.Child
proxy objects.
-
"""
+
return Child.objects.filter(issuer=self)
@property
@@ -148,8 +148,8 @@ class Conf(rpki.irdb.models.ResourceHolderCA):
def routes(self):
"""Return all IPv4 routes covered by RPKI certs issued to this resource
holder.
-
"""
+
# build a Q filter to select all RouteOrigin objects covered by
# prefixes in the resource holder's certificates
q = models.Q()
@@ -162,8 +162,8 @@ class Conf(rpki.irdb.models.ResourceHolderCA):
def routes_v6(self):
"""Return all IPv6 routes covered by RPKI certs issued to this resource
holder.
-
"""
+
# build a Q filter to select all RouteOrigin objects covered by
# prefixes in the resource holder's certificates
q = models.Q()
@@ -174,6 +174,7 @@ class Conf(rpki.irdb.models.ResourceHolderCA):
def send_alert(self, subject, message, from_email, severity=Alert.INFO):
"""Store an alert for this resource holder."""
+
self.alerts.create(subject=subject, text=message, severity=severity)
send_mail(
@@ -189,8 +190,8 @@ class Conf(rpki.irdb.models.ResourceHolderCA):
Contact emails are extract from any ghostbuster requests, and any
linked user accounts.
-
"""
+
notify_emails = [gbr.email_address for gbr in self.ghostbusters if gbr.email_address]
notify_emails.extend(
[acl.user.email for acl in ConfACL.objects.filter(conf=self) if acl.user.email]
@@ -209,7 +210,6 @@ class ResourceCert(models.Model):
"""Represents a resource certificate.
This model is used to cache the output of <list_received_resources/>.
-
"""
# Handle to which this cert was issued
@@ -237,6 +237,7 @@ class ResourceCert(models.Model):
def get_cert_chain(self):
"""Return a list containing the complete certificate chain for this
certificate."""
+
cert = self
x = [cert]
while cert.issuer:
@@ -410,7 +411,6 @@ class RouteOriginV6(rpki.gui.routeview.models.RouteOriginV6):
class ConfACL(models.Model):
"""Stores access control for which users are allowed to manage a given
resource handle.
-
"""
conf = models.ForeignKey(Conf)
diff --git a/rpki/gui/app/range_list.py b/rpki/gui/app/range_list.py
index 21fd1f29..5cb4f5e4 100755
--- a/rpki/gui/app/range_list.py
+++ b/rpki/gui/app/range_list.py
@@ -70,6 +70,7 @@ class RangeList(list):
def difference(self, other):
"""Return a RangeList object which contains ranges in this object which
are not in "other"."""
+
it = iter(other)
try:
@@ -85,6 +86,7 @@ class RangeList(list):
def V(v):
"""convert the integer value to the appropriate type for this
range"""
+
return x.__class__.datum_type(v)
try:
diff --git a/rpki/gui/app/views.py b/rpki/gui/app/views.py
index 9a1c4cfe..228d5c6c 100644
--- a/rpki/gui/app/views.py
+++ b/rpki/gui/app/views.py
@@ -71,6 +71,7 @@ def superuser_required(f):
def get_conf(user, handle):
"""return the Conf object for 'handle'.
user is a request.user object to use enforce ACLs."""
+
if user.is_superuser:
qs = models.Conf.objects.all()
else:
@@ -81,8 +82,8 @@ def get_conf(user, handle):
def handle_required(f):
"""Decorator for view functions which require the user to be logged in and
a resource handle selected for the session.
-
"""
+
@login_required
@tls_required
def wrapped_fn(request, *args, **kwargs):
@@ -126,8 +127,8 @@ def generic_import(request, queryset, configure, form_class=None,
if None (default), the user will be redirected to the detail page for
the imported object. Otherwise, the user will be redirected to the
specified URL.
-
"""
+
conf = get_conf(request.user, request.session['handle'])
if form_class is None:
form_class = forms.ImportForm
@@ -251,6 +252,7 @@ def dashboard(request):
@login_required
def conf_list(request, **kwargs):
"""Allow the user to select a handle."""
+
log = request.META['wsgi.errors']
next_url = request.GET.get('next', reverse(dashboard))
if request.user.is_superuser:
@@ -266,6 +268,7 @@ def conf_list(request, **kwargs):
@login_required
def conf_select(request):
"""Change the handle for the current session."""
+
if not 'handle' in request.GET:
return redirect(conf_list)
handle = request.GET['handle']
@@ -288,8 +291,8 @@ def serve_xml(content, basename, ext='xml'):
`basename` is the prefix to specify for the XML filename.
`csv` is the type (default: xml)
-
"""
+
resp = http.HttpResponse(content, mimetype='application/%s' % ext)
resp['Content-Disposition'] = 'attachment; filename=%s.%s' % (basename, ext)
return resp
@@ -298,6 +301,7 @@ def serve_xml(content, basename, ext='xml'):
@handle_required
def conf_export(request):
"""Return the identity.xml for the current handle."""
+
conf = get_conf(request.user, request.session['handle'])
z = Zookeeper(handle=conf.handle)
xml = z.generate_identity()
@@ -307,6 +311,7 @@ def conf_export(request):
@handle_required
def export_asns(request):
"""Export CSV file containing ASN allocations to children."""
+
conf = get_conf(request.user, request.session['handle'])
s = cStringIO.StringIO()
csv_writer = csv.writer(s, delimiter=' ')
@@ -342,6 +347,7 @@ def import_asns(request):
@handle_required
def export_prefixes(request):
"""Export CSV file containing ASN allocations to children."""
+
conf = get_conf(request.user, request.session['handle'])
s = cStringIO.StringIO()
csv_writer = csv.writer(s, delimiter=' ')
@@ -411,6 +417,7 @@ def parent_delete(request, pk):
@handle_required
def parent_export(request, pk):
"""Export XML repository request for a given parent."""
+
conf = get_conf(request.user, request.session['handle'])
parent = get_object_or_404(conf.parents, pk=pk)
z = Zookeeper(handle=conf.handle)
@@ -474,6 +481,7 @@ def child_detail(request, pk):
@handle_required
def child_edit(request, pk):
"""Edit the end validity date for a resource handle's child."""
+
log = request.META['wsgi.errors']
conf = get_conf(request.user, request.session['handle'])
child = get_object_or_404(conf.children.all(), pk=pk)
@@ -505,8 +513,8 @@ def child_response(request, pk):
"""
Export the XML file containing the output of the configure_child
to send back to the client.
-
"""
+
conf = get_conf(request.user, request.session['handle'])
child = get_object_or_404(models.Child, issuer=conf, pk=pk)
z = Zookeeper(handle=conf.handle)
@@ -551,7 +559,6 @@ def get_covered_routes(rng, max_prefixlen, asn):
A "newstatus" attribute is monkey-patched on the RouteOrigin objects which
can be used in the template. "status" remains the current validation
status of the object.
-
"""
# find all routes that match or are completed covered by the proposed new roa
@@ -591,7 +598,6 @@ def roa_create(request):
Doesn't use the generic create_object() form because we need to
create both the ROARequest and ROARequestPrefix objects.
-
"""
conf = get_conf(request.user, request.session['handle'])
@@ -628,7 +634,6 @@ def roa_create(request):
class ROARequestFormSet(BaseFormSet):
"""There is no way to pass arbitrary keyword arguments to the form
constructor, so we have to override BaseFormSet to allow it.
-
"""
def __init__(self, *args, **kwargs):
self.conf = kwargs.pop('conf')
@@ -666,7 +671,6 @@ def roa_create_multi(request):
?roa=1.1.1.1-2.2.2.2,42
The ASN may optionally be omitted.
-
"""
conf = get_conf(request.user, request.session['handle'])
@@ -717,8 +721,8 @@ def roa_create_multi(request):
def roa_create_confirm(request):
"""This function is called when the user confirms the creation of a ROA
request. It is responsible for updating the IRDB.
-
"""
+
conf = get_conf(request.user, request.session['handle'])
log = request.META['wsgi.errors']
if request.method == 'POST':
@@ -746,8 +750,8 @@ def roa_create_confirm(request):
def roa_create_multi_confirm(request):
"""This function is called when the user confirms the creation of a ROA
request. It is responsible for updating the IRDB.
-
"""
+
conf = get_conf(request.user, request.session['handle'])
log = request.META['wsgi.errors']
if request.method == 'POST':
@@ -778,7 +782,6 @@ def roa_delete(request, pk):
Uses a form for double confirmation, displaying how the route
validation status may change as a result.
-
"""
conf = get_conf(request.user, request.session['handle'])
@@ -835,6 +838,7 @@ def roa_clone(request, pk):
@handle_required
def roa_import(request):
"""Import CSV containing ROA declarations."""
+
if request.method == 'POST':
form = forms.ImportCSVForm(request.POST, request.FILES)
if form.is_valid():
@@ -860,6 +864,7 @@ def roa_import(request):
@handle_required
def roa_export(request):
"""Export CSV containing ROA declarations."""
+
# FIXME: remove when Zookeeper can do this
f = cStringIO.StringIO()
csv_writer = csv.writer(f, delimiter=' ')
@@ -941,8 +946,8 @@ def ghostbuster_edit(request, pk):
def refresh(request):
"""
Query rpkid, update the db, and redirect back to the dashboard.
-
"""
+
conf = get_conf(request.user, request.session['handle'])
glue.list_received_resources(request.META['wsgi.errors'], conf)
return http.HttpResponseRedirect(reverse(dashboard))
@@ -953,8 +958,8 @@ def route_view(request):
"""
Display a list of global routing table entries which match resources
listed in received certificates.
-
"""
+
conf = get_conf(request.user, request.session['handle'])
count = request.GET.get('count', 25)
page = request.GET.get('page', 1)
@@ -972,6 +977,7 @@ def route_view(request):
def route_detail(request, pk):
"""Show a list of ROAs that match a given IPv4 route."""
+
route = get_object_or_404(models.RouteOrigin, pk=pk)
# when running rootd, viewing the 0.0.0.0/0 route will cause a fetch of all
# roas, so we paginate here, even though in the general case the number of
@@ -989,8 +995,8 @@ def route_suggest(request):
"""Handles POSTs from the route view and redirects to the ROA creation
page based on selected route objects. The form should contain elements of
the form "pk-NUM" where NUM is the RouteOrigin object id.
-
"""
+
if request.method == 'POST':
routes = []
for pk in request.POST.iterkeys():
@@ -1040,6 +1046,7 @@ def repository_delete(request, pk):
@handle_required
def repository_import(request):
"""Import XML response file from repository operator."""
+
return generic_import(request,
models.Repository.objects,
Zookeeper.configure_repository,
@@ -1094,8 +1101,8 @@ def client_import(request):
def client_export(request, pk):
"""Return the XML file resulting from a configure_publication_client
request.
-
"""
+
client = get_object_or_404(models.Client, pk=pk)
z = Zookeeper()
xml = z.generate_repository_response(client)
@@ -1107,6 +1114,7 @@ def client_export(request, pk):
@superuser_required
def resource_holder_list(request):
"""Display a list of all the RPKI handles managed by this server."""
+
return render(request, 'app/resource_holder_list.html', {
'object_list': models.Conf.objects.all()
})
@@ -1115,6 +1123,7 @@ def resource_holder_list(request):
@superuser_required
def resource_holder_edit(request, pk):
"""Display a list of all the RPKI handles managed by this server."""
+
conf = get_object_or_404(models.Conf, pk=pk)
if request.method == 'POST':
form = forms.ResourceHolderForm(request.POST, request.FILES)
@@ -1221,6 +1230,7 @@ def user_create(request):
@superuser_required
def user_list(request):
"""Display a list of all the RPKI handles managed by this server."""
+
return render(request, 'app/user_list.html', {
'object_list': User.objects.all()
})
@@ -1316,6 +1326,7 @@ class AlertDeleteView(DeleteView):
@handle_required
def alert_clear_all(request):
"""Clear all alerts associated with the current resource holder."""
+
if request.method == 'POST':
form = forms.Empty(request.POST, request.FILES)
if form.is_valid():
diff --git a/rpki/gui/cacheview/models.py b/rpki/gui/cacheview/models.py
index c3ee8421..08acfa2d 100644
--- a/rpki/gui/cacheview/models.py
+++ b/rpki/gui/cacheview/models.py
@@ -58,6 +58,7 @@ class ValidationLabel(models.Model):
Represents a specific error condition defined in the rcynic XML
output file.
"""
+
label = models.CharField(max_length=79, db_index=True, unique=True)
status = models.CharField(max_length=255)
kind = models.PositiveSmallIntegerField(choices=kinds)
@@ -70,6 +71,7 @@ class RepositoryObject(models.Model):
"""
Represents a globally unique RPKI repository object, specified by its URI.
"""
+
uri = models.URLField(unique=True, db_index=True)
generations = list(enumerate(('current', 'backup')))
@@ -89,6 +91,7 @@ class SignedObject(models.Model):
The signing certificate is ommitted here in order to give a proper
value for the 'related_name' attribute.
"""
+
repo = models.ForeignKey(RepositoryObject, related_name='cert', unique=True)
# on-disk file modification time
@@ -108,6 +111,7 @@ class SignedObject(models.Model):
"""
convert the local timestamp to UTC and convert to a datetime object
"""
+
return datetime.utcfromtimestamp(self.mtime + time.timezone)
def status_id(self):
@@ -116,6 +120,7 @@ class SignedObject(models.Model):
The selector is chosen based on the current generation only. If there is any bad status,
return bad, else if there are any warn status, return warn, else return good.
"""
+
for x in reversed(kinds):
if self.repo.statuses.filter(generation=generations_dict['current'], status__kind=x[0]):
return x[1]
@@ -129,6 +134,7 @@ class Cert(SignedObject):
"""
Object representing a resource certificate.
"""
+
addresses = models.ManyToManyField(AddressRange, related_name='certs')
addresses_v6 = models.ManyToManyField(AddressRangeV6, related_name='certs')
asns = models.ManyToManyField(ASRange, related_name='certs')
@@ -141,6 +147,7 @@ class Cert(SignedObject):
def get_cert_chain(self):
"""Return a list containing the complete certificate chain for this
certificate."""
+
cert = self
x = [cert]
while cert != cert.issuer:
@@ -180,6 +187,7 @@ class ROAPrefixV4(ROAPrefix, rpki.gui.models.PrefixV4):
@property
def routes(self):
"""return all routes covered by this roa prefix"""
+
return RouteOrigin.objects.filter(prefix_min__gte=self.prefix_min,
prefix_max__lte=self.prefix_max)
diff --git a/rpki/gui/cacheview/tests.py b/rpki/gui/cacheview/tests.py
index 2247054b..daca07bf 100644
--- a/rpki/gui/cacheview/tests.py
+++ b/rpki/gui/cacheview/tests.py
@@ -12,6 +12,7 @@ class SimpleTest(TestCase):
"""
Tests that 1 + 1 always equals 2.
"""
+
self.failUnlessEqual(1 + 1, 2)
__test__ = {"doctest": """
diff --git a/rpki/gui/cacheview/util.py b/rpki/gui/cacheview/util.py
index 9e8748bf..31ad8b8b 100644
--- a/rpki/gui/cacheview/util.py
+++ b/rpki/gui/cacheview/util.py
@@ -310,8 +310,8 @@ def fetch_published_objects():
"""Query rpkid for all objects published by local users, and look up the
current validation status of each object. The validation status is used
later to send alerts for objects which have transitioned to invalid.
-
"""
+
logger.info('querying for published objects')
handles = [conf.handle for conf in Conf.objects.all()]
@@ -353,7 +353,6 @@ class Handle(object):
def notify_invalid():
"""Send email alerts to the addresses registered in ghostbuster records for
any invalid objects that were published by users of this system.
-
"""
logger.info('sending notifications for invalid objects')
diff --git a/rpki/gui/cacheview/views.py b/rpki/gui/cacheview/views.py
index 94870eb2..451c0d1e 100644
--- a/rpki/gui/cacheview/views.py
+++ b/rpki/gui/cacheview/views.py
@@ -29,6 +29,7 @@ def cert_chain(obj):
"""
returns an iterator covering all certs from the root cert down to the EE.
"""
+
chain = [obj]
while obj != obj.issuer:
obj = obj.issuer
diff --git a/rpki/gui/decorators.py b/rpki/gui/decorators.py
index 69d20c46..ed10f3d9 100644
--- a/rpki/gui/decorators.py
+++ b/rpki/gui/decorators.py
@@ -20,8 +20,8 @@ from django import http
def tls_required(f):
"""Decorator which returns a 500 error if the connection is not secured
with TLS (https).
-
"""
+
def _tls_required(request, *args, **kwargs):
if not request.is_secure():
return http.HttpResponseServerError(
diff --git a/rpki/gui/default_settings.py b/rpki/gui/default_settings.py
index 3859247c..e0626965 100644
--- a/rpki/gui/default_settings.py
+++ b/rpki/gui/default_settings.py
@@ -93,6 +93,7 @@ TIME_ZONE = select_tz()
def get_secret_key():
"""Retrieve the secret-key value from rpki.conf or generate a random value
if it is not present."""
+
d = string.letters + string.digits
val = ''.join([random.choice(d) for _ in range(50)])
return rpki_config.get('secret-key', val)
diff --git a/rpki/gui/models.py b/rpki/gui/models.py
index 184383c0..62400d2a 100644
--- a/rpki/gui/models.py
+++ b/rpki/gui/models.py
@@ -42,8 +42,8 @@ class IPv6AddressField(models.Field):
"""
Note that we add a custom conversion to encode long values as hex
strings in SQL statements. See settings.get_conv() for details.
-
"""
+
return value.toBytes()
@@ -82,6 +82,7 @@ class Prefix(models.Model):
"""
Returns the prefix as a rpki.resource_set.resource_range_ip object.
"""
+
return self.range_cls(self.prefix_min, self.prefix_max)
@property
@@ -96,6 +97,7 @@ class Prefix(models.Model):
def __unicode__(self):
"""This method may be overridden by subclasses. The default
implementation calls get_prefix_display(). """
+
return self.get_prefix_display()
class Meta:
diff --git a/rpki/gui/routeview/api.py b/rpki/gui/routeview/api.py
index cf699c9a..b4ff297a 100644
--- a/rpki/gui/routeview/api.py
+++ b/rpki/gui/routeview/api.py
@@ -29,8 +29,8 @@ def route_list(request):
By default, only returns up to 10 matching routes, but the client may
request a different limit with the 'count=' query string parameter.
-
"""
+
hard_limit = 100
if request.method == 'GET' and 'prefix__in' in request.GET:
diff --git a/rpki/gui/routeview/util.py b/rpki/gui/routeview/util.py
index 54d50f24..a2b515c8 100644
--- a/rpki/gui/routeview/util.py
+++ b/rpki/gui/routeview/util.py
@@ -179,8 +179,8 @@ def import_routeviews_dump(filename=DEFAULT_URL, filetype='auto'):
filename [optional]: the full path to the downloaded file to parse
filetype [optional]: 'text' or 'mrt'
-
"""
+
start_time = time.time()
if filename.startswith('http://'):
diff --git a/rpki/gui/script_util.py b/rpki/gui/script_util.py
index c3a864fd..46545d83 100644
--- a/rpki/gui/script_util.py
+++ b/rpki/gui/script_util.py
@@ -28,6 +28,7 @@ def setup():
"""
Configure Django enough to use the ORM.
"""
+
cfg = config.parser(section='web_portal')
# INSTALLED_APPS doesn't seem necessary so long as you are only accessing
# existing tables.
diff --git a/rpki/http.py b/rpki/http.py
index 546dd310..e41b0080 100644
--- a/rpki/http.py
+++ b/rpki/http.py
@@ -112,6 +112,7 @@ def supported_address_families(enable_ipv6):
IP address families on which servers should listen, and to consider
when selecting addresses for client connections.
"""
+
if enable_ipv6 and have_ipv6:
return (socket.AF_INET, socket.AF_INET6)
else:
@@ -121,6 +122,7 @@ def localhost_addrinfo():
"""
Return pseudo-getaddrinfo results for localhost.
"""
+
result = [(socket.AF_INET, "127.0.0.1")]
if enable_ipv6_clients and have_ipv6:
result.append((socket.AF_INET6, "::1"))
@@ -144,6 +146,7 @@ class http_message(object):
Clean up (some of) the horrible messes that HTTP allows in its
headers.
"""
+
if headers is None:
headers = () if self.headers is None else self.headers.items()
translate_underscore = True
@@ -166,6 +169,7 @@ class http_message(object):
"""
Parse and normalize an incoming HTTP message.
"""
+
self = cls()
headers = headers.split("\r\n")
self.parse_first_line(*headers.pop(0).split(None, 2))
@@ -180,6 +184,7 @@ class http_message(object):
"""
Format an outgoing HTTP message.
"""
+
s = self.format_first_line()
if self.body is not None:
assert isinstance(self.body, str)
@@ -198,6 +203,7 @@ class http_message(object):
"""
Parse HTTP version, raise an exception if we can't.
"""
+
if version[:5] != "HTTP/":
raise rpki.exceptions.HTTPBadVersion("Couldn't parse version %s" % version)
self.version = tuple(int(i) for i in version[5:].split("."))
@@ -207,6 +213,7 @@ class http_message(object):
"""
Figure out whether this HTTP message encourages a persistent connection.
"""
+
c = self.headers.get("Connection")
if self.version == (1, 1):
return c is None or "close" not in c.lower()
@@ -233,6 +240,7 @@ class http_request(http_message):
"""
Parse first line of HTTP request message.
"""
+
self.parse_version(version)
self.cmd = cmd
self.path = path
@@ -242,6 +250,7 @@ class http_request(http_message):
Format first line of HTTP request message, and set up the
User-Agent header.
"""
+
self.headers.setdefault("User-Agent", self.software_name)
return "%s %s HTTP/%d.%d\r\n" % (self.cmd, self.path, self.version[0], self.version[1])
@@ -262,6 +271,7 @@ class http_response(http_message):
"""
Parse first line of HTTP response message.
"""
+
self.parse_version(version)
self.code = int(code)
self.reason = reason
@@ -271,6 +281,7 @@ class http_response(http_message):
Format first line of HTTP response message, and set up Date and
Server headers.
"""
+
self.headers.setdefault("Date", time.strftime("%a, %d %b %Y %T GMT"))
self.headers.setdefault("Server", self.software_name)
return "HTTP/%d.%d %s %s\r\n" % (self.version[0], self.version[1], self.code, self.reason)
@@ -319,6 +330,7 @@ class http_stream(asynchat.async_chat):
"""
(Re)start HTTP message parser, reset timer.
"""
+
assert not self.buffer
self.chunk_handler = None
self.set_terminator("\r\n\r\n")
@@ -330,6 +342,7 @@ class http_stream(asynchat.async_chat):
stream's timeout value if we're doing timeouts, otherwise clear
it.
"""
+
if self.timeout is not None:
self.logger.debug("Setting timeout %s", self.timeout)
self.timer.set(self.timeout)
@@ -341,6 +354,7 @@ class http_stream(asynchat.async_chat):
"""
Buffer incoming data from asynchat.
"""
+
self.buffer.append(data)
self.update_timeout()
@@ -348,6 +362,7 @@ class http_stream(asynchat.async_chat):
"""
Consume data buffered from asynchat.
"""
+
val = "".join(self.buffer)
self.buffer = []
return val
@@ -369,6 +384,7 @@ class http_stream(asynchat.async_chat):
separate mechanisms (chunked, content-length, TCP close) is going
to tell us how to find the end of the message body.
"""
+
self.update_timeout()
if self.chunk_handler:
self.chunk_handler()
@@ -392,6 +408,7 @@ class http_stream(asynchat.async_chat):
stream up to read it; otherwise, this is the last chunk, so start
the process of exiting the chunk decoder.
"""
+
n = int(self.get_buffer().partition(";")[0], 16)
self.logger.debug("Chunk length %s", n)
if n:
@@ -407,6 +424,7 @@ class http_stream(asynchat.async_chat):
body of a chunked message (sic). Save it, and prepare to move on
to the next chunk.
"""
+
self.logger.debug("Chunk body")
self.msg.body += self.buffer
self.buffer = []
@@ -418,6 +436,7 @@ class http_stream(asynchat.async_chat):
Consume the CRLF that terminates a chunk, reinitialize chunk
decoder to be ready for the next chunk.
"""
+
self.logger.debug("Chunk CRLF")
s = self.get_buffer()
assert s == "", "%r: Expected chunk CRLF, got '%s'" % (self, s)
@@ -428,6 +447,7 @@ class http_stream(asynchat.async_chat):
Consume chunk trailer, which should be empty, then (finally!) exit
the chunk decoder and hand complete message off to the application.
"""
+
self.logger.debug("Chunk trailer")
s = self.get_buffer()
assert s == "", "%r: Expected end of chunk trailers, got '%s'" % (self, s)
@@ -438,6 +458,7 @@ class http_stream(asynchat.async_chat):
"""
Hand normal (not chunked) message off to the application.
"""
+
self.msg.body = self.get_buffer()
self.handle_message()
@@ -447,6 +468,7 @@ class http_stream(asynchat.async_chat):
whether it's one we should just pass along, otherwise log a stack
trace and close the stream.
"""
+
self.timer.cancel()
etype = sys.exc_info()[0]
if etype in (SystemExit, rpki.async.ExitNow):
@@ -459,6 +481,7 @@ class http_stream(asynchat.async_chat):
"""
Inactivity timer expired, close connection with prejudice.
"""
+
self.logger.debug("Timeout, closing")
self.close()
@@ -467,6 +490,7 @@ class http_stream(asynchat.async_chat):
Wrapper around asynchat connection close handler, so that we can
log the event, cancel timer, and so forth.
"""
+
self.logger.debug("Close event in HTTP stream handler")
self.timer.cancel()
asynchat.async_chat.handle_close(self)
@@ -497,12 +521,14 @@ class http_server(http_stream):
Content-Length header (that is: this message will be the last one
in this server stream). No special action required.
"""
+
self.handle_message()
def find_handler(self, path):
"""
Helper method to search self.handlers.
"""
+
for s, h in self.handlers:
if path.startswith(s):
return h
@@ -515,6 +541,7 @@ class http_server(http_stream):
Content-Type, look for a handler, and if everything looks right,
pass the message body, path, and a reply callback to the handler.
"""
+
self.logger.debug("Received request %r", self.msg)
if not self.msg.persistent:
self.expect_close = True
@@ -541,12 +568,14 @@ class http_server(http_stream):
"""
Send an error response to this request.
"""
+
self.send_message(code = code, reason = reason)
def send_reply(self, code, body = None, reason = "OK"):
"""
Send a reply to this request.
"""
+
self.send_message(code = code, body = body, reason = reason)
def send_message(self, code, reason = "OK", body = None):
@@ -556,6 +585,7 @@ class http_server(http_stream):
listen for next message; otherwise, queue up a close event for
this stream so it will shut down once the reply has been sent.
"""
+
self.logger.debug("Sending response %s %s", code, reason)
if code >= 400:
self.expect_close = True
@@ -611,6 +641,7 @@ class http_listener(asyncore.dispatcher):
Asyncore says we have an incoming connection, spawn an http_server
stream for it and pass along all of our handler data.
"""
+
try:
res = self.accept()
if res is None:
@@ -627,6 +658,7 @@ class http_listener(asyncore.dispatcher):
"""
Asyncore signaled an error, pass it along or log it.
"""
+
if sys.exc_info()[0] in (SystemExit, rpki.async.ExitNow):
raise
self.logger.exception("Error in HTTP listener")
@@ -662,6 +694,7 @@ class http_client(http_stream):
"""
Create socket and request a connection.
"""
+
if not use_adns:
self.logger.debug("Not using ADNS")
self.gotaddrinfo([(socket.AF_INET, self.host)])
@@ -678,12 +711,14 @@ class http_client(http_stream):
Handle DNS lookup errors. For now, just whack the connection.
Undoubtedly we should do something better with diagnostics here.
"""
+
self.handle_error()
def gotaddrinfo(self, addrinfo):
"""
Got address data from DNS, create socket and request connection.
"""
+
try:
self.af, self.address = random.choice(addrinfo)
self.logger.debug("Connecting to AF %s host %s port %s addr %s", self.af, self.host, self.port, self.address)
@@ -701,6 +736,7 @@ class http_client(http_stream):
"""
Asyncore says socket has connected.
"""
+
self.logger.debug("Socket connected")
self.set_state("idle")
assert self.queue.client is self
@@ -710,6 +746,7 @@ class http_client(http_stream):
"""
Set HTTP client connection state.
"""
+
self.logger.debug("State transition %s => %s", self.state, state)
self.state = state
@@ -720,12 +757,14 @@ class http_client(http_stream):
in this server stream). In this case we want to read until we
reach the end of the data stream.
"""
+
self.set_terminator(None)
def send_request(self, msg):
"""
Queue up request message and kickstart connection.
"""
+
self.logger.debug("Sending request %r", msg)
assert self.state == "idle", "%r: state should be idle, is %s" % (self, self.state)
self.set_state("request-sent")
@@ -782,6 +821,7 @@ class http_client(http_stream):
message now; if we were waiting for the response to a request we
sent, signal the error.
"""
+
http_stream.handle_close(self)
self.logger.debug("State %s", self.state)
if self.get_terminator() is None:
@@ -796,6 +836,7 @@ class http_client(http_stream):
Connection idle timer has expired. Shut down connection in any
case, noisily if we weren't idle.
"""
+
bad = self.state not in ("idle", "closing")
if bad:
self.logger.warning("Timeout while in state %s", self.state)
@@ -813,6 +854,7 @@ class http_client(http_stream):
Asyncore says something threw an exception. Log it, then shut
down the connection and pass back the exception.
"""
+
eclass, edata = sys.exc_info()[0:2]
self.logger.warning("Error on HTTP client connection %s:%s %s %s", self.host, self.port, eclass, edata)
http_stream.handle_error(self)
@@ -840,6 +882,7 @@ class http_queue(object):
"""
Append http_request object(s) to this queue.
"""
+
self.logger.debug("Adding requests %r", requests)
self.queue.extend(requests)
@@ -852,6 +895,7 @@ class http_queue(object):
exception, or timeout) for the query currently in progress will
call this method when it's time to kick out the next query.
"""
+
try:
if self.client is None:
self.client = http_client(self, self.hostport)
@@ -871,6 +915,7 @@ class http_queue(object):
"""
Kick out the next query in this queue, if any.
"""
+
if self.queue:
self.client.send_request(self.queue[0])
@@ -881,6 +926,7 @@ class http_queue(object):
handling of what otherwise would be a nasty set of race
conditions.
"""
+
if client_ is self.client:
self.logger.debug("Detaching client %r", client_)
self.client = None
@@ -1032,6 +1078,7 @@ class caller(object):
"""
Handle CMS-wrapped XML response message.
"""
+
try:
r_cms = self.proto.cms_msg(DER = r_der)
r_msg = r_cms.unwrap((self.server_ta, self.server_cert))
diff --git a/rpki/ipaddrs.py b/rpki/ipaddrs.py
index 68b2d27d..25eefd0d 100644
--- a/rpki/ipaddrs.py
+++ b/rpki/ipaddrs.py
@@ -61,6 +61,7 @@ class v4addr(long):
"""
Construct a v4addr object.
"""
+
if isinstance(x, unicode):
x = x.encode("ascii")
if isinstance(x, str):
@@ -72,6 +73,7 @@ class v4addr(long):
"""
Convert a v4addr object to a raw byte string.
"""
+
return struct.pack("!I", long(self))
@classmethod
@@ -79,12 +81,14 @@ class v4addr(long):
"""
Convert from a raw byte string to a v4addr object.
"""
+
return cls(struct.unpack("!I", x)[0])
def __str__(self):
"""
Convert a v4addr object to string format.
"""
+
return socket.inet_ntop(socket.AF_INET, self.to_bytes())
class v6addr(long):
@@ -101,6 +105,7 @@ class v6addr(long):
"""
Construct a v6addr object.
"""
+
if isinstance(x, unicode):
x = x.encode("ascii")
if isinstance(x, str):
@@ -112,6 +117,7 @@ class v6addr(long):
"""
Convert a v6addr object to a raw byte string.
"""
+
return struct.pack("!QQ", long(self) >> 64, long(self) & 0xFFFFFFFFFFFFFFFF)
@classmethod
@@ -119,6 +125,7 @@ class v6addr(long):
"""
Convert from a raw byte string to a v6addr object.
"""
+
x = struct.unpack("!QQ", x)
return cls((x[0] << 64) | x[1])
@@ -126,12 +133,14 @@ class v6addr(long):
"""
Convert a v6addr object to string format.
"""
+
return socket.inet_ntop(socket.AF_INET6, self.to_bytes())
def parse(s):
"""
Parse a string as either an IPv4 or IPv6 address, and return object of appropriate class.
"""
+
if isinstance(s, unicode):
s = s.encode("ascii")
return v6addr(s) if ":" in s else v4addr(s)
diff --git a/rpki/irdb/models.py b/rpki/irdb/models.py
index 6fa48c59..c5cf7f15 100644
--- a/rpki/irdb/models.py
+++ b/rpki/irdb/models.py
@@ -101,6 +101,7 @@ class SundialField(django.db.models.DateTimeField):
"""
A field type for our customized datetime objects.
"""
+
__metaclass__ = django.db.models.SubfieldBase
description = "A datetime type using our customized datetime objects"
diff --git a/rpki/irdb/zookeeper.py b/rpki/irdb/zookeeper.py
index 49229463..28197de0 100644
--- a/rpki/irdb/zookeeper.py
+++ b/rpki/irdb/zookeeper.py
@@ -151,7 +151,6 @@ class etree_wrapper(object):
"""
Wrapper for ETree objects so we can return them as function results
without requiring the caller to understand much about them.
-
"""
def __init__(self, e, msg = None, debug = False):
diff --git a/rpki/left_right.py b/rpki/left_right.py
index d05d0221..e4b664b9 100644
--- a/rpki/left_right.py
+++ b/rpki/left_right.py
@@ -67,6 +67,7 @@ class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, left_right_name
"""
Fetch self object to which this object links.
"""
+
return self_elt.sql_fetch(self.gctx, self.self_id)
@property
@@ -75,12 +76,14 @@ class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, left_right_name
"""
Return BSC object to which this object links.
"""
+
return bsc_elt.sql_fetch(self.gctx, self.bsc_id)
def make_reply_clone_hook(self, r_pdu):
"""
Set handles when cloning, including _id -> _handle translation.
"""
+
if r_pdu.self_handle is None:
r_pdu.self_handle = self.self_handle
for tag, elt in self.handles:
@@ -97,6 +100,7 @@ class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, left_right_name
"""
Find an object based on its handle.
"""
+
return cls.sql_fetch_where1(gctx, cls.element_name + "_handle = %s AND self_id = %s", (handle, self_id))
def serve_fetch_one_maybe(self):
@@ -104,6 +108,7 @@ class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, left_right_name
Find the object on which a get, set, or destroy method should
operate, or which would conflict with a create method.
"""
+
where = "%s.%s_handle = %%s AND %s.self_id = self.self_id AND self.self_handle = %%s" % ((self.element_name,) * 3)
args = (getattr(self, self.element_name + "_handle"), self.self_handle)
return self.sql_fetch_where1(self.gctx, where, args, "self")
@@ -112,6 +117,7 @@ class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, left_right_name
"""
Find the objects on which a list method should operate.
"""
+
where = "%s.self_id = self.self_id and self.self_handle = %%s" % self.element_name
return self.sql_fetch_where(self.gctx, where, (self.self_handle,), "self")
@@ -124,6 +130,7 @@ class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, left_right_name
operations, self is the pre-existing object from SQL and q_pdu is
the set request received from the the IRBE.
"""
+
for tag, elt in self.handles:
id_name = tag + "_id"
if getattr(self, id_name, None) is None:
@@ -171,6 +178,7 @@ class self_elt(data_elt):
"""
Fetch all BSC objects that link to this self object.
"""
+
return bsc_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
@property
@@ -178,6 +186,7 @@ class self_elt(data_elt):
"""
Fetch all repository objects that link to this self object.
"""
+
return repository_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
@property
@@ -185,6 +194,7 @@ class self_elt(data_elt):
"""
Fetch all parent objects that link to this self object.
"""
+
return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
@property
@@ -192,6 +202,7 @@ class self_elt(data_elt):
"""
Fetch all child objects that link to this self object.
"""
+
return child_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
@property
@@ -199,6 +210,7 @@ class self_elt(data_elt):
"""
Fetch all ROA objects that link to this self object.
"""
+
return rpki.rpkid.roa_obj.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
@property
@@ -206,6 +218,7 @@ class self_elt(data_elt):
"""
Fetch all Ghostbuster record objects that link to this self object.
"""
+
return rpki.rpkid.ghostbuster_obj.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
@property
@@ -213,6 +226,7 @@ class self_elt(data_elt):
"""
Fetch all EE certificate objects that link to this self object.
"""
+
return rpki.rpkid.ee_cert_obj.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
@@ -220,6 +234,7 @@ class self_elt(data_elt):
"""
Extra server actions for self_elt.
"""
+
actions = []
if q_pdu.rekey:
actions.append(self.serve_rekey)
@@ -243,6 +258,7 @@ class self_elt(data_elt):
"""
Handle a left-right rekey action for this self.
"""
+
def loop(iterator, parent):
parent.serve_rekey(iterator, eb)
rpki.async.iterator(self.parents, loop, cb)
@@ -251,6 +267,7 @@ class self_elt(data_elt):
"""
Handle a left-right revoke action for this self.
"""
+
def loop(iterator, parent):
parent.serve_revoke(iterator, eb)
rpki.async.iterator(self.parents, loop, cb)
@@ -259,6 +276,7 @@ class self_elt(data_elt):
"""
Handle a left-right reissue action for this self.
"""
+
def loop(iterator, parent):
parent.serve_reissue(iterator, eb)
rpki.async.iterator(self.parents, loop, cb)
@@ -267,6 +285,7 @@ class self_elt(data_elt):
"""
Handle a left-right revoke_forgotten action for this self.
"""
+
def loop(iterator, parent):
parent.serve_revoke_forgotten(iterator, eb)
rpki.async.iterator(self.parents, loop, cb)
@@ -275,6 +294,7 @@ class self_elt(data_elt):
"""
Handle a left-right clear_replay_protection action for this self.
"""
+
def loop(iterator, obj):
obj.serve_clear_replay_protection(iterator, eb)
rpki.async.iterator(self.parents + self.children + self.repositories, loop, cb)
@@ -283,6 +303,7 @@ class self_elt(data_elt):
"""
Extra cleanup actions when destroying a self_elt.
"""
+
def loop(iterator, parent):
parent.delete(iterator)
rpki.async.iterator(self.parents, loop, cb)
@@ -332,6 +353,7 @@ class self_elt(data_elt):
"""
Handle a left-right run_now action for this self.
"""
+
logger.debug("Forced immediate run of periodic actions for self %s[%d]",
self.self_handle, self.self_id)
completion = rpki.rpkid_tasks.CompletionHandler(cb)
@@ -344,6 +366,7 @@ class self_elt(data_elt):
Find the self object upon which a get, set, or destroy action
should operate, or which would conflict with a create method.
"""
+
return self.serve_fetch_handle(self.gctx, None, self.self_handle)
@classmethod
@@ -351,6 +374,7 @@ class self_elt(data_elt):
"""
Find a self object based on its self_handle.
"""
+
return cls.sql_fetch_where1(gctx, "self_handle = %s", (self_handle,))
def serve_fetch_all(self):
@@ -359,6 +383,7 @@ class self_elt(data_elt):
This is different from the list action for all other objects,
where list only works within a given self_id context.
"""
+
return self.sql_fetch_all(self.gctx)
def schedule_cron_tasks(self, completion):
@@ -430,6 +455,7 @@ class bsc_elt(data_elt):
"""
Fetch all repository objects that link to this BSC object.
"""
+
return repository_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
@property
@@ -437,6 +463,7 @@ class bsc_elt(data_elt):
"""
Fetch all parent objects that link to this BSC object.
"""
+
return parent_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
@property
@@ -444,6 +471,7 @@ class bsc_elt(data_elt):
"""
Fetch all child objects that link to this BSC object.
"""
+
return child_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
def serve_pre_save_hook(self, q_pdu, r_pdu, cb, eb):
@@ -451,6 +479,7 @@ class bsc_elt(data_elt):
Extra server actions for bsc_elt -- handle key generation. For
now this only allows RSA with SHA-256.
"""
+
if q_pdu.generate_keypair:
assert q_pdu.key_type in (None, "rsa") and q_pdu.hash_alg in (None, "sha256")
self.private_key_id = rpki.x509.RSA.generate(keylength = q_pdu.key_length or 2048)
@@ -494,12 +523,14 @@ class repository_elt(data_elt):
"""
Fetch all parent objects that link to this repository object.
"""
+
return parent_elt.sql_fetch_where(self.gctx, "repository_id = %s", (self.repository_id,))
def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
"""
Extra server actions for repository_elt.
"""
+
actions = []
if q_pdu.clear_replay_protection:
actions.append(self.serve_clear_replay_protection)
@@ -511,6 +542,7 @@ class repository_elt(data_elt):
"""
Handle a left-right clear_replay_protection action for this repository.
"""
+
self.last_cms_timestamp = None
self.sql_mark_dirty()
cb()
@@ -520,6 +552,7 @@ class repository_elt(data_elt):
"""
Default handler for publication response PDUs.
"""
+
pdu.raise_if_error()
def call_pubd(self, callback, errback, q_msg, handlers = None):
@@ -626,6 +659,7 @@ class parent_elt(data_elt):
"""
Fetch repository object to which this parent object links.
"""
+
return repository_elt.sql_fetch(self.gctx, self.repository_id)
@property
@@ -633,12 +667,14 @@ class parent_elt(data_elt):
"""
Fetch all CA objects that link to this parent object.
"""
+
return rpki.rpkid.ca_obj.sql_fetch_where(self.gctx, "parent_id = %s", (self.parent_id,))
def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
"""
Extra server actions for parent_elt.
"""
+
actions = []
if q_pdu.rekey:
actions.append(self.serve_rekey)
@@ -658,6 +694,7 @@ class parent_elt(data_elt):
"""
Handle a left-right rekey action for this parent.
"""
+
def loop(iterator, ca):
ca.rekey(iterator, eb)
rpki.async.iterator(self.cas, loop, cb)
@@ -666,6 +703,7 @@ class parent_elt(data_elt):
"""
Handle a left-right revoke action for this parent.
"""
+
def loop(iterator, ca):
ca.revoke(cb = iterator, eb = eb)
rpki.async.iterator(self.cas, loop, cb)
@@ -674,6 +712,7 @@ class parent_elt(data_elt):
"""
Handle a left-right reissue action for this parent.
"""
+
def loop(iterator, ca):
ca.reissue(cb = iterator, eb = eb)
rpki.async.iterator(self.cas, loop, cb)
@@ -682,6 +721,7 @@ class parent_elt(data_elt):
"""
Handle a left-right clear_replay_protection action for this parent.
"""
+
self.last_cms_timestamp = None
self.sql_mark_dirty()
cb()
@@ -862,6 +902,7 @@ class child_elt(data_elt):
"""
Fetch all child_cert objects that link to this child object.
"""
+
return rpki.rpkid.child_cert_obj.fetch(self.gctx, self, ca_detail, ski, unique)
@property
@@ -869,6 +910,7 @@ class child_elt(data_elt):
"""
Fetch all child_cert objects that link to this child object.
"""
+
return self.fetch_child_certs()
@property
@@ -876,12 +918,14 @@ class child_elt(data_elt):
"""
Fetch all parent objects that link to self object to which this child object links.
"""
+
return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
"""
Extra server actions for child_elt.
"""
+
actions = []
if q_pdu.reissue:
actions.append(self.serve_reissue)
@@ -895,6 +939,7 @@ class child_elt(data_elt):
"""
Handle a left-right reissue action for this child.
"""
+
publisher = rpki.rpkid.publication_queue()
for child_cert in self.child_certs:
child_cert.reissue(child_cert.ca_detail, publisher, force = True)
@@ -904,6 +949,7 @@ class child_elt(data_elt):
"""
Handle a left-right clear_replay_protection action for this child.
"""
+
self.last_cms_timestamp = None
self.sql_mark_dirty()
cb()
@@ -912,6 +958,7 @@ class child_elt(data_elt):
"""
Fetch the CA corresponding to an up-down class_name.
"""
+
if not class_name.isdigit():
raise rpki.exceptions.BadClassNameSyntax("Bad class name %s" % class_name)
ca = rpki.rpkid.ca_obj.sql_fetch(self.gctx, long(class_name))
@@ -928,6 +975,7 @@ class child_elt(data_elt):
"""
Extra server actions when destroying a child_elt.
"""
+
publisher = rpki.rpkid.publication_queue()
for child_cert in self.child_certs:
child_cert.revoke(publisher = publisher,
@@ -991,6 +1039,7 @@ class list_resources_elt(rpki.xml_utils.base_elt, left_right_namespace):
Handle <list_resources/> element. This requires special handling
due to the data types of some of the attributes.
"""
+
assert name == "list_resources", "Unexpected name %s, stack %s" % (name, stack)
self.read_attrs(attrs)
if isinstance(self.valid_until, str):
@@ -1007,6 +1056,7 @@ class list_resources_elt(rpki.xml_utils.base_elt, left_right_namespace):
Generate <list_resources/> element. This requires special
handling due to the data types of some of the attributes.
"""
+
elt = self.make_elt()
if isinstance(self.valid_until, int):
elt.set("valid_until", self.valid_until.toXMLtime())
@@ -1025,6 +1075,7 @@ class list_roa_requests_elt(rpki.xml_utils.base_elt, left_right_namespace):
Handle <list_roa_requests/> element. This requires special handling
due to the data types of some of the attributes.
"""
+
assert name == "list_roa_requests", "Unexpected name %s, stack %s" % (name, stack)
self.read_attrs(attrs)
if self.ipv4 is not None:
@@ -1070,6 +1121,7 @@ class list_ee_certificate_requests_elt(rpki.xml_utils.base_elt, left_right_names
Handle <list_ee_certificate_requests/> element. This requires special
handling due to the data types of some of the attributes.
"""
+
if name not in self.elements:
assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
self.read_attrs(attrs)
@@ -1088,6 +1140,7 @@ class list_ee_certificate_requests_elt(rpki.xml_utils.base_elt, left_right_names
"""
Handle <pkcs10/> sub-element.
"""
+
assert len(self.elements) == 1
if name == self.elements[0]:
self.pkcs10 = rpki.x509.PKCS10(Base64 = text)
@@ -1100,6 +1153,7 @@ class list_ee_certificate_requests_elt(rpki.xml_utils.base_elt, left_right_names
Generate <list_ee_certificate_requests/> element. This requires special
handling due to the data types of some of the attributes.
"""
+
if isinstance(self.eku, (tuple, list)):
self.eku = ",".join(self.eku)
elt = self.make_elt()
@@ -1130,6 +1184,7 @@ class list_published_objects_elt(rpki.xml_utils.text_elt, left_right_namespace):
misnomer here, there's no action attribute and no dispatch, we
just dump every published object for the specified <self/> and return.
"""
+
for parent in self_elt.serve_fetch_handle(self.gctx, None, self.self_handle).parents:
for ca in parent.cas:
ca_detail = ca.active_ca_detail
@@ -1150,6 +1205,7 @@ class list_published_objects_elt(rpki.xml_utils.text_elt, left_right_namespace):
"""
Generate one reply PDU.
"""
+
r_pdu = self.make_pdu(tag = self.tag, self_handle = self.self_handle,
uri = uri, child_handle = child_handle)
r_pdu.obj = obj.get_Base64()
@@ -1174,6 +1230,7 @@ class list_received_resources_elt(rpki.xml_utils.base_elt, left_right_namespace)
just dump a bunch of data about every certificate issued to us by
one of our parents, then return.
"""
+
for parent in self_elt.serve_fetch_handle(self.gctx, None, self.self_handle).parents:
for ca in parent.cas:
ca_detail = ca.active_ca_detail
@@ -1185,6 +1242,7 @@ class list_received_resources_elt(rpki.xml_utils.base_elt, left_right_namespace)
"""
Generate one reply PDU.
"""
+
resources = cert.get_3779resources()
return self.make_pdu(
tag = self.tag,
@@ -1218,6 +1276,7 @@ class report_error_elt(rpki.xml_utils.text_elt, left_right_namespace):
"""
Generate a <report_error/> element from an exception.
"""
+
self = cls()
self.self_handle = self_handle
self.tag = tag
diff --git a/rpki/pubd.py b/rpki/pubd.py
index 42e18e10..46e431c4 100644
--- a/rpki/pubd.py
+++ b/rpki/pubd.py
@@ -107,6 +107,8 @@ class main(object):
self.publication_multimodule = self.cfg.getboolean("publication-multimodule", False)
+ self.session = session_obj.fetch(self)
+
rpki.http.server(
host = self.http_server_host,
port = self.http_server_port,
@@ -183,8 +185,12 @@ class session_obj(rpki.sql.sql_persistent):
sql_template = rpki.sql.template(
"session",
"session_id",
- "uuid",
- "serial")
+ "uuid")
+
+ ## @var expiration_interval
+ # How long to wait after retiring a snapshot before purging it from the database.
+
+ expiration_interval = rpki.sundial.timedelta(hours = 6)
def __repr__(self):
return rpki.log.log_repr(self, self.uuid, self.serial)
@@ -201,27 +207,118 @@ class session_obj(rpki.sql.sql_persistent):
self.gctx = gctx
self.session_id = 1
self.uuid = uuid.uuid4()
- self.serial = 1
self.sql_store()
return self
@property
- @rpki.sql.cache_reference
def objects(self):
- return object_obj.sql_fetch_where(self.gctx, "session_id = %s", (self.session_id))
+ return object_obj.sql_fetch_where(self.gctx, "session_id = %s", (self.session_id,))
- def next_serial_number(self):
+ @property
+ def snapshots(self):
+ return snapshot_obj.sql_fetch_where(self.gctx, "session_id = %s", (self.session_id,))
+
+ @property
+ def current_snapshot(self):
+ return snapshot_obj.sql_fetch_where1(self.gctx,
+ "session_id = %s AND activated IS NOT NULL AND expires IS NULL",
+ (self.session_id,))
+
+ def new_snapshot(self):
+ return snapshot_obj.create(self)
+
+ def add_snapshot(self, new_snapshot):
+ now = rpki.sundial.now()
+ old_snapshot = self.current_snapshot
+ if old_snapshot is not None:
+ old_snapshot.expires = now + self.expiration_interval
+ old_snapshot.sql_store()
+ new_snapshot.activated = now
+ new_snapshot.sql_store()
+
+ def expire_snapshots(self):
+ for snapshot in snapshot_obj.sql_fetch_where(self.gctx,
+ "session_id = %s AND expires IS NOT NULL AND expires < %s",
+ (self.session_id, rpki.sundial.now())):
+ snapshot.sql_delete()
+
+
+class snapshot_obj(rpki.sql.sql_persistent):
+ """
+ An RRDP session snapshot.
+ """
+
+ sql_template = rpki.sql.template(
+ "snapshot",
+ "snapshot_id",
+ ("activated", rpki.sundial.datetime),
+ ("expires", rpki.sundial.datetime),
+ "session_id")
+
+ @property
+ @rpki.sql.cache_reference
+ def session(self):
+ return session_obj.sql_fetch(self.gctx, self.session_id)
+
+ @classmethod
+ def create(cls, session):
+ self = cls()
+ self.gctx = session.gctx
+ self.session_id = session.session_id
+ self.activated = None
+ self.expires = None
+ self.sql_store()
+ return self
+
+ @property
+ def serial(self):
"""
- Bump serial number
+ I know that using an SQL ID for any other purpose is usually a bad
+ idea, but in this case it has exactly the right properties, and we
+ really do want both the autoincrement behavior and the foreign key
+ behavior to tie to the snapshot serial numbers. So risk it.
+
+ Well, OK, only almost the right properties. auto-increment
+ probably does not back up if we ROLLBACK, which could leave gaps
+ in the sequence. So may need to rework this. Ignore for now.
"""
- self.serial += 1
- self.sql_mark_dirty()
- return self.serial
+ return self.snapshot_id
- # More methods when I know what they look like
+ def publish(self, client, obj, uri):
+ # Still a bit confused as to what we should do here. The
+ # overwrite <publish/> with another <publish/> model doens't
+ # really match the IXFR model. Current proposal is an attribute
+ # on <publish/> to say that this is an overwrite, haven't
+ # implemented that yet. Would need to push knowledge of when
+ # we're overwriting all the way from rpkid code that decides to
+ # write each kind of object. In most cases it looks like we
+ # already know, a priori, might be a few corner cases.
+ # Temporary kludge
+ if True:
+ try:
+ self.withdraw(client, uri)
+ except rpki.exceptions.NoObjectAtURI:
+ logger.debug("Withdrew %s", uri)
+ else:
+ logger.debug("No prior %s", uri)
+
+ logger.debug("Publishing %s", uri)
+ return object_obj.create(client, self, obj, uri)
+
+ def withdraw(self, client, uri):
+ obj = object_obj.sql_fetch_where1(self.gctx,
+ "session_id = %s AND client_id = %s AND withdrawn_snapshot_id IS NULL AND uri = %s",
+ (self.session_id, client.client_id, uri))
+ if obj is None:
+ raise rpki.exceptions.NoObjectAtURI("No object published at %s" % uri)
+ logger.debug("Withdrawing %s", uri)
+ obj.delete(self)
+
+
+
class object_obj(rpki.sql.sql_persistent):
"""
A published object.
@@ -233,15 +330,13 @@ class object_obj(rpki.sql.sql_persistent):
"uri",
"hash",
"payload",
- "published",
- "withdrawn")
-
- uri = None
- published = None
- withdrawn = None
+ "published_snapshot_id",
+ "withdrawn_snapshot_id",
+ "client_id",
+ "session_id")
def __repr__(self):
- return rpki.log.log_repr(self, self.uri, self.published, self.withdrawn)
+ return rpki.log.log_repr(self, self.uri, self.published_snapshot_id, self.withdrawn_snapshot_id)
@property
@rpki.sql.cache_reference
@@ -252,3 +347,23 @@ class object_obj(rpki.sql.sql_persistent):
@rpki.sql.cache_reference
def client(self):
return rpki.publication_control.client_elt.sql_fetch(self.gctx, self.client_id)
+
+ @classmethod
+ def create(cls, client, snapshot, obj, uri):
+ self = cls()
+ self.gctx = snapshot.gctx
+ self.uri = uri
+ self.payload = obj
+ self.hash = rpki.x509.sha256(obj.get_Base64())
+ logger.debug("Computed hash %s of %r", self.hash.encode("hex"), obj)
+ self.published_snapshot_id = snapshot.snapshot_id
+ self.withdrawn_snapshot_id = None
+ self.session_id = snapshot.session_id
+ self.client_id = client.client_id
+ self.sql_mark_dirty()
+ return self
+
+ def delete(self, snapshot):
+ self.withdrawn_snapshot_id = snapshot.snapshot_id
+ #self.sql_mark_dirty()
+ self.sql_store()
diff --git a/rpki/publication.py b/rpki/publication.py
index 87a097c9..19ab2107 100644
--- a/rpki/publication.py
+++ b/rpki/publication.py
@@ -55,14 +55,14 @@ class base_publication_elt(rpki.xml_utils.base_elt, publication_namespace):
def __repr__(self):
return rpki.log.log_repr(self, self.uri, self.payload)
- def serve_dispatch(self, r_msg, cb, eb):
+ def serve_dispatch(self, r_msg, snapshot, cb, eb):
"""
Action dispatch handler.
"""
try:
self.client.check_allowed_uri(self.uri)
- self.serve_action()
+ self.serve_action(snapshot)
r_pdu = self.__class__()
r_pdu.tag = self.tag
r_pdu.uri = self.uri
@@ -94,6 +94,7 @@ class base_publication_elt(rpki.xml_utils.base_elt, publication_namespace):
"""
No-op, since this is not a <report_error/> PDU.
"""
+
pass
@@ -121,20 +122,20 @@ class publish_elt(base_publication_elt):
elt.text = self.payload.get_Base64()
return elt
- def serve_action(self):
+ def serve_action(self, snapshot):
"""
Publish an object.
"""
logger.info("Publishing %s", self.payload.tracking_data(self.uri))
+ snapshot.publish(self.client, self.payload, self.uri)
filename = self.uri_to_filename()
filename_tmp = filename + ".tmp"
dirname = os.path.dirname(filename)
if not os.path.isdir(dirname):
os.makedirs(dirname)
- f = open(filename_tmp, "wb")
- f.write(self.payload.get_DER())
- f.close()
+ with open(filename_tmp, "wb") as f:
+ f.write(self.payload.get_DER())
os.rename(filename_tmp, filename)
@classmethod
@@ -151,12 +152,13 @@ class withdraw_elt(base_publication_elt):
element_name = "withdraw"
- def serve_action(self):
+ def serve_action(self, snapshot):
"""
Withdraw an object, then recursively delete empty directories.
"""
logger.info("Withdrawing %s", self.uri)
+ snapshot.withdraw(self.client, self.uri)
filename = self.uri_to_filename()
try:
os.remove(filename)
@@ -194,16 +196,18 @@ class report_error_elt(rpki.xml_utils.text_elt, publication_namespace):
attributes = ("tag", "error_code")
text_attribute = "error_text"
+ error_code = None
error_text = None
def __repr__(self):
- return rpki.log.log_repr(self)
+ return rpki.log.log_repr(self, self.error_code, self.error_text)
@classmethod
def from_exception(cls, e, tag = None):
"""
Generate a <report_error/> element from an exception.
"""
+
self = cls()
self.tag = tag
self.error_code = e.__class__.__name__
@@ -223,6 +227,7 @@ class report_error_elt(rpki.xml_utils.text_elt, publication_namespace):
"""
Raise exception associated with this <report_error/> PDU.
"""
+
t = rpki.exceptions.__dict__.get(self.error_code)
if isinstance(t, type) and issubclass(t, rpki.exceptions.RPKI_Exception):
raise t(getattr(self, "text", None))
@@ -247,9 +252,11 @@ class msg(rpki.xml_utils.msg, publication_namespace):
"""
Serve one msg PDU.
"""
+
if not self.is_query():
raise rpki.exceptions.BadQuery("Message type is not query")
r_msg = self.__class__.reply()
+ snapshot = gctx.session.new_snapshot() if len(self) > 0 else None
def loop(iterator, q_pdu):
@@ -257,18 +264,20 @@ class msg(rpki.xml_utils.msg, publication_namespace):
if not isinstance(e, rpki.exceptions.NotFound):
logger.exception("Exception processing PDU %r", q_pdu)
r_msg.append(report_error_elt.from_exception(e, q_pdu.tag))
+ snapshot.sql_delete()
cb(r_msg)
try:
q_pdu.gctx = gctx
q_pdu.client = client
- q_pdu.serve_dispatch(r_msg, iterator, fail)
+ q_pdu.serve_dispatch(r_msg, snapshot, iterator, fail)
except (rpki.async.ExitNow, SystemExit):
raise
except Exception, e:
fail(e)
def done():
+ gctx.session.add_snapshot(snapshot)
cb(r_msg)
rpki.async.iterator(self, loop, done)
diff --git a/rpki/publication_control.py b/rpki/publication_control.py
index 19c7f010..f65fa15d 100644
--- a/rpki/publication_control.py
+++ b/rpki/publication_control.py
@@ -87,7 +87,6 @@ class client_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, publication_c
return rpki.log.log_repr(self, self.client_handle, self.base_uri)
@property
- @rpki.sql.cache_reference
def objects(self):
return rpki.pubd.object_obj.sql_fetch_where(self.gctx, "client_id = %s", (self.client_id,))
@@ -95,6 +94,7 @@ class client_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, publication_c
"""
Extra server actions for client_elt.
"""
+
actions = []
if q_pdu.clear_replay_protection:
actions.append(self.serve_clear_replay_protection)
@@ -106,6 +106,7 @@ class client_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, publication_c
"""
Handle a clear_replay_protection action for this client.
"""
+
self.last_cms_timestamp = None
self.sql_mark_dirty()
cb()
@@ -115,18 +116,21 @@ class client_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, publication_c
Find the client object on which a get, set, or destroy method
should operate, or which would conflict with a create method.
"""
+
return self.sql_fetch_where1(self.gctx, "client_handle = %s", (self.client_handle,))
def serve_fetch_all(self):
"""
Find client objects on which a list method should operate.
"""
+
return self.sql_fetch_all(self.gctx)
def check_allowed_uri(self, uri):
"""
Make sure that a target URI is within this client's allowed URI space.
"""
+
if not uri.startswith(self.base_uri):
raise rpki.exceptions.ForbiddenURI
@@ -147,6 +151,7 @@ class report_error_elt(rpki.xml_utils.text_elt, publication_control_namespace):
"""
Generate a <report_error/> element from an exception.
"""
+
self = cls()
self.tag = tag
self.error_code = e.__class__.__name__
@@ -166,6 +171,7 @@ class report_error_elt(rpki.xml_utils.text_elt, publication_control_namespace):
"""
Raise exception associated with this <report_error/> PDU.
"""
+
t = rpki.exceptions.__dict__.get(self.error_code)
if isinstance(t, type) and issubclass(t, rpki.exceptions.RPKI_Exception):
raise t(getattr(self, "text", None))
@@ -190,6 +196,7 @@ class msg(rpki.xml_utils.msg, publication_control_namespace):
"""
Serve one msg PDU.
"""
+
if not self.is_query():
raise rpki.exceptions.BadQuery("Message type is not query")
r_msg = self.__class__.reply()
diff --git a/rpki/rcynic.py b/rpki/rcynic.py
index 10ad7516..a36e4a4e 100644
--- a/rpki/rcynic.py
+++ b/rpki/rcynic.py
@@ -53,6 +53,7 @@ class rcynic_object(object):
Print a bunch of object attributes, quietly ignoring any that
might be missing.
"""
+
for a in attrs:
try:
print "%s: %s" % (a.capitalize(), getattr(self, a))
@@ -63,6 +64,7 @@ class rcynic_object(object):
"""
Print common object attributes.
"""
+
self.show_attrs("filename", "uri", "status", "timestamp")
class rcynic_certificate(rcynic_object):
@@ -91,6 +93,7 @@ class rcynic_certificate(rcynic_object):
"""
Print certificate attributes.
"""
+
rcynic_object.show(self)
self.show_attrs("notBefore", "notAfter", "aia_uri", "sia_directory_uri", "resources")
@@ -128,6 +131,7 @@ class rcynic_roa(rcynic_object):
"""
Print ROA attributes.
"""
+
rcynic_object.show(self)
self.show_attrs("notBefore", "notAfter", "aia_uri", "resources", "asID")
if self.prefix_sets:
diff --git a/rpki/resource_set.py b/rpki/resource_set.py
index fea6ad2d..130bf4e7 100644
--- a/rpki/resource_set.py
+++ b/rpki/resource_set.py
@@ -86,6 +86,7 @@ class resource_range_as(resource_range):
"""
Convert a resource_range_as to string format.
"""
+
if self.min == self.max:
return str(self.min)
else:
@@ -96,6 +97,7 @@ class resource_range_as(resource_range):
"""
Parse ASN resource range from text (eg, XML attributes).
"""
+
r = re_asn_range.match(x)
if r:
return cls(long(r.group(1)), long(r.group(2)))
@@ -107,6 +109,7 @@ class resource_range_as(resource_range):
"""
Construct ASN range from strings.
"""
+
if b is None:
b = a
return cls(long(a), long(b))
@@ -133,6 +136,7 @@ class resource_range_ip(resource_range):
prefix. Returns prefix length if it can, otherwise raises
MustBePrefix exception.
"""
+
mask = self.min ^ self.max
if self.min & mask != 0:
raise rpki.exceptions.MustBePrefix
@@ -154,6 +158,7 @@ class resource_range_ip(resource_range):
the logic in one place. This property is useful primarily in
context where catching an exception isn't practical.
"""
+
try:
self.prefixlen()
return True
@@ -164,6 +169,7 @@ class resource_range_ip(resource_range):
"""
Convert a resource_range_ip to string format.
"""
+
try:
return str(self.min) + "/" + str(self.prefixlen())
except rpki.exceptions.MustBePrefix:
@@ -174,6 +180,7 @@ class resource_range_ip(resource_range):
"""
Parse IP address range or prefix from text (eg, XML attributes).
"""
+
r = re_address_range.match(x)
if r:
return cls.from_strings(r.group(1), r.group(2))
@@ -192,6 +199,7 @@ class resource_range_ip(resource_range):
"""
Construct a resource range corresponding to a prefix.
"""
+
assert isinstance(prefix, rpki.POW.IPAddress) and isinstance(prefixlen, (int, long))
assert prefixlen >= 0 and prefixlen <= prefix.bits, "Nonsensical prefix length: %s" % prefixlen
mask = (1 << (prefix.bits - prefixlen)) - 1
@@ -203,6 +211,7 @@ class resource_range_ip(resource_range):
Chop up a resource_range_ip into ranges that can be represented as
prefixes.
"""
+
try:
self.prefixlen()
result.append(self)
@@ -226,6 +235,7 @@ class resource_range_ip(resource_range):
"""
Construct IP address range from strings.
"""
+
if b is None:
b = a
a = rpki.POW.IPAddress(a)
@@ -300,6 +310,7 @@ class resource_set(list):
"""
Initialize a resource_set.
"""
+
list.__init__(self)
if isinstance(ini, (int, long)):
ini = str(ini)
@@ -317,6 +328,7 @@ class resource_set(list):
"""
Whack this resource_set into canonical form.
"""
+
assert not self.inherit or len(self) == 0
if not self.canonical:
self.sort()
@@ -339,6 +351,7 @@ class resource_set(list):
"""
Wrapper around list.append() (q.v.) to reset canonical flag.
"""
+
list.append(self, item)
self.canonical = False
@@ -346,6 +359,7 @@ class resource_set(list):
"""
Wrapper around list.extend() (q.v.) to reset canonical flag.
"""
+
list.extend(self, item)
self.canonical = False
@@ -353,6 +367,7 @@ class resource_set(list):
"""
Convert a resource_set to string format.
"""
+
if self.inherit:
return inherit_token
else:
@@ -428,6 +443,7 @@ class resource_set(list):
"""
Set intersection for resource sets.
"""
+
return self._comm(other)[2]
__and__ = intersection
@@ -436,6 +452,7 @@ class resource_set(list):
"""
Set difference for resource sets.
"""
+
return self._comm(other)[0]
__sub__ = difference
@@ -444,6 +461,7 @@ class resource_set(list):
"""
Set symmetric difference (XOR) for resource sets.
"""
+
com = self._comm(other)
return com[0] | com[1]
@@ -453,6 +471,7 @@ class resource_set(list):
"""
Set membership test for resource sets.
"""
+
assert not self.inherit
self.canonize()
if not self:
@@ -479,6 +498,7 @@ class resource_set(list):
"""
Test whether self is a subset (possibly improper) of other.
"""
+
for i in self:
if not other.contains(i):
return False
@@ -490,6 +510,7 @@ class resource_set(list):
"""
Test whether self is a superset (possibly improper) of other.
"""
+
return other.issubset(self)
__ge__ = issuperset
@@ -506,6 +527,7 @@ class resource_set(list):
we can't know the answer here. This is also consistent with __nonzero__
which returns True for inherit sets, and False for empty sets.
"""
+
return self.inherit or other.inherit or list.__ne__(self, other)
def __eq__(self, other):
@@ -516,6 +538,7 @@ class resource_set(list):
Tests whether or not this set is empty. Note that sets with the inherit
bit set are considered non-empty, despite having zero length.
"""
+
return self.inherit or len(self)
@classmethod
@@ -553,6 +576,7 @@ class resource_set(list):
a backwards compatability wrapper, real functionality is now part
of the range classes.
"""
+
return cls.range_type.parse_str(s)
class resource_set_as(resource_set):
@@ -577,6 +601,7 @@ class resource_set_ip(resource_set):
"""
Convert from a resource set to a ROA prefix set.
"""
+
prefix_ranges = []
for r in self:
r.chop_into_prefixes(prefix_ranges)
@@ -632,6 +657,7 @@ class resource_bag(object):
"""
True iff self is oversized with respect to other.
"""
+
return not self.asn.issubset(other.asn) or \
not self.v4.issubset(other.v4) or \
not self.v6.issubset(other.v6)
@@ -640,6 +666,7 @@ class resource_bag(object):
"""
True iff self is undersized with respect to other.
"""
+
return not other.asn.issubset(self.asn) or \
not other.v4.issubset(self.v4) or \
not other.v6.issubset(self.v6)
@@ -650,6 +677,7 @@ class resource_bag(object):
Build a resource bag that just inherits everything from its
parent.
"""
+
self = cls()
self.asn = resource_set_as()
self.v4 = resource_set_ipv4()
@@ -665,6 +693,7 @@ class resource_bag(object):
Parse a comma-separated text string into a resource_bag. Not
particularly efficient, fix that if and when it becomes an issue.
"""
+
asns = []
v4s = []
v6s = []
@@ -689,6 +718,7 @@ class resource_bag(object):
temporary: in the long run, we should be using rpki.POW.IPAddress
rather than long here.
"""
+
asn = inherit_token if resources[0] == "inherit" else [resource_range_as( r[0], r[1]) for r in resources[0] or ()]
v4 = inherit_token if resources[1] == "inherit" else [resource_range_ipv4(r[0], r[1]) for r in resources[1] or ()]
v6 = inherit_token if resources[2] == "inherit" else [resource_range_ipv6(r[0], r[1]) for r in resources[2] or ()]
@@ -700,6 +730,7 @@ class resource_bag(object):
"""
True iff all resource sets in this bag are empty.
"""
+
return not self.asn and not self.v4 and not self.v6
def __nonzero__(self):
@@ -719,6 +750,7 @@ class resource_bag(object):
Compute intersection with another resource_bag. valid_until
attribute (if any) inherits from self.
"""
+
return self.__class__(self.asn & other.asn,
self.v4 & other.v4,
self.v6 & other.v6,
@@ -731,6 +763,7 @@ class resource_bag(object):
Compute union with another resource_bag. valid_until attribute
(if any) inherits from self.
"""
+
return self.__class__(self.asn | other.asn,
self.v4 | other.v4,
self.v6 | other.v6,
@@ -743,6 +776,7 @@ class resource_bag(object):
Compute difference against another resource_bag. valid_until
attribute (if any) inherits from self
"""
+
return self.__class__(self.asn - other.asn,
self.v4 - other.v4,
self.v6 - other.v6,
@@ -755,6 +789,7 @@ class resource_bag(object):
Compute symmetric difference against another resource_bag.
valid_until attribute (if any) inherits from self
"""
+
return self.__class__(self.asn ^ other.asn,
self.v4 ^ other.v4,
self.v6 ^ other.v6,
@@ -816,6 +851,7 @@ class roa_prefix(object):
Initialize a ROA prefix. max_prefixlen is optional and defaults
to prefixlen. max_prefixlen must not be smaller than prefixlen.
"""
+
if max_prefixlen is None:
max_prefixlen = prefixlen
assert max_prefixlen >= prefixlen, "Bad max_prefixlen: %d must not be shorter than %d" % (max_prefixlen, prefixlen)
@@ -828,6 +864,7 @@ class roa_prefix(object):
Compare two ROA prefix objects. Comparision is based on prefix,
prefixlen, and max_prefixlen, in that order.
"""
+
assert self.__class__ is other.__class__
return (cmp(self.prefix, other.prefix) or
cmp(self.prefixlen, other.prefixlen) or
@@ -837,6 +874,7 @@ class roa_prefix(object):
"""
Convert a ROA prefix to string format.
"""
+
if self.prefixlen == self.max_prefixlen:
return str(self.prefix) + "/" + str(self.prefixlen)
else:
@@ -848,24 +886,28 @@ class roa_prefix(object):
object. This is an irreversable transformation because it loses
the max_prefixlen attribute, nothing we can do about that.
"""
+
return self.range_type.make_prefix(self.prefix, self.prefixlen)
def min(self):
"""
Return lowest address covered by prefix.
"""
+
return self.prefix
def max(self):
"""
Return highest address covered by prefix.
"""
+
return self.prefix | ((1 << (self.prefix.bits - self.prefixlen)) - 1)
def to_POW_roa_tuple(self):
"""
Convert a resource_range_ip to rpki.POW.ROA.setPrefixes() format.
"""
+
return self.prefix, self.prefixlen, self.max_prefixlen
@classmethod
@@ -873,6 +915,7 @@ class roa_prefix(object):
"""
Parse ROA prefix from text (eg, an XML attribute).
"""
+
r = re_prefix_with_maxlen.match(x)
if r:
return cls(rpki.POW.IPAddress(r.group(1)), int(r.group(2)), int(r.group(3)))
@@ -910,6 +953,7 @@ class roa_prefix_set(list):
"""
Initialize a ROA prefix set.
"""
+
list.__init__(self)
if isinstance(ini, str) and len(ini):
self.extend(self.parse_str(s) for s in ini.split(","))
@@ -923,6 +967,7 @@ class roa_prefix_set(list):
"""
Convert a ROA prefix set to string format.
"""
+
return ",".join(str(x) for x in self)
@classmethod
@@ -931,6 +976,7 @@ class roa_prefix_set(list):
Parse ROA prefix from text (eg, an XML attribute).
This method is a backwards compatability shim.
"""
+
return cls.prefix_type.parse_str(s)
def to_resource_set(self):
@@ -942,6 +988,7 @@ class roa_prefix_set(list):
a more efficient way to do this, but start by getting the output
right before worrying about making it fast or pretty.
"""
+
r = self.resource_set_type()
s = self.resource_set_type()
s.append(None)
@@ -982,6 +1029,7 @@ class roa_prefix_set(list):
"""
Convert ROA prefix set to form used by rpki.POW.ROA.setPrefixes().
"""
+
if self:
return tuple(a.to_POW_roa_tuple() for a in self)
else:
diff --git a/rpki/rootd.py b/rpki/rootd.py
index fb445213..579909fd 100644
--- a/rpki/rootd.py
+++ b/rpki/rootd.py
@@ -101,6 +101,7 @@ class message_pdu(rpki.up_down.message_pdu):
"""
Log query we're handling.
"""
+
logger.info("Serving %s query", self.type)
class sax_handler(rpki.up_down.sax_handler):
diff --git a/rpki/rpkid.py b/rpki/rpkid.py
index cb792572..ce16d832 100644
--- a/rpki/rpkid.py
+++ b/rpki/rpkid.py
@@ -318,6 +318,7 @@ class main(object):
Record that we were still alive when we got here, by resetting
keepalive timer.
"""
+
if force or self.cron_timeout is not None:
self.cron_timeout = rpki.sundial.now() + self.cron_keepalive
@@ -325,6 +326,7 @@ class main(object):
"""
Add a task to the scheduler task queue, unless it's already queued.
"""
+
if task not in self.task_queue:
logger.debug("Adding %r to task queue", task)
self.task_queue.append(task)
@@ -339,6 +341,7 @@ class main(object):
queue (we don't want to run it directly, as that could eventually
blow out our call stack).
"""
+
try:
self.task_current = self.task_queue.pop(0)
except IndexError:
@@ -350,6 +353,7 @@ class main(object):
"""
Run first task on the task queue, unless one is running already.
"""
+
if self.task_current is None:
self.task_next()
@@ -446,6 +450,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Fetch parent object to which this CA object links.
"""
+
return rpki.left_right.parent_elt.sql_fetch(self.gctx, self.parent_id)
@property
@@ -453,6 +458,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Fetch all ca_detail objects that link to this CA object.
"""
+
return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s", (self.ca_id,))
@property
@@ -460,6 +466,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Fetch the pending ca_details for this CA, if any.
"""
+
return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'pending'", (self.ca_id,))
@property
@@ -467,6 +474,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Fetch the active ca_detail for this CA, if any.
"""
+
return ca_detail_obj.sql_fetch_where1(self.gctx, "ca_id = %s AND state = 'active'", (self.ca_id,))
@property
@@ -474,6 +482,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Fetch deprecated ca_details for this CA, if any.
"""
+
return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'deprecated'", (self.ca_id,))
@property
@@ -481,6 +490,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Fetch active and deprecated ca_details for this CA, if any.
"""
+
return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND (state = 'active' OR state = 'deprecated')", (self.ca_id,))
@property
@@ -488,6 +498,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Fetch revoked ca_details for this CA, if any.
"""
+
return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'revoked'", (self.ca_id,))
@property
@@ -496,7 +507,7 @@ class ca_obj(rpki.sql.sql_persistent):
Fetch ca_details which are candidates for consideration when
processing an up-down issue_response PDU.
"""
- #return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND latest_ca_cert IS NOT NULL AND state != 'revoked'", (self.ca_id,))
+
return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state != 'revoked'", (self.ca_id,))
def construct_sia_uri(self, parent, rc):
@@ -542,7 +553,8 @@ class ca_obj(rpki.sql.sql_persistent):
if rc_cert is None:
- logger.warning("SKI %s in resource class %s is in database but missing from list_response to %s from %s, maybe parent certificate went away?",
+ logger.warning("SKI %s in resource class %s is in database but missing from list_response to %s from %s, "
+ "maybe parent certificate went away?",
ca_detail.public_key.gSKI(), rc.class_name, parent.self.self_handle, parent.parent_handle)
publisher = publication_queue()
ca_detail.delete(ca = ca_detail.ca, publisher = publisher)
@@ -677,6 +689,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Allocate a certificate serial number.
"""
+
self.last_issued_sn += 1
self.sql_mark_dirty()
return self.last_issued_sn
@@ -685,6 +698,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Allocate a manifest serial number.
"""
+
self.last_manifest_sn += 1
self.sql_mark_dirty()
return self.last_manifest_sn
@@ -693,6 +707,7 @@ class ca_obj(rpki.sql.sql_persistent):
"""
Allocate a CRL serial number.
"""
+
self.last_crl_sn += 1
self.sql_mark_dirty()
return self.last_crl_sn
@@ -783,6 +798,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Extra assertions for SQL decode of a ca_detail_obj.
"""
+
rpki.sql.sql_persistent.sql_decode(self, vals)
assert self.public_key is None or self.private_key_id is None or self.public_key.get_DER() == self.private_key_id.get_public_DER()
assert self.manifest_public_key is None or self.manifest_private_key_id is None or self.manifest_public_key.get_DER() == self.manifest_private_key_id.get_public_DER()
@@ -793,12 +809,14 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Fetch CA object to which this ca_detail links.
"""
+
return ca_obj.sql_fetch(self.gctx, self.ca_id)
def fetch_child_certs(self, child = None, ski = None, unique = False, unpublished = None):
"""
Fetch all child_cert objects that link to this ca_detail.
"""
+
return rpki.rpkid.child_cert_obj.fetch(self.gctx, child, self, ski, unique, unpublished)
@property
@@ -806,6 +824,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Fetch all child_cert objects that link to this ca_detail.
"""
+
return self.fetch_child_certs()
def unpublished_child_certs(self, when):
@@ -813,6 +832,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
Fetch all unpublished child_cert objects linked to this ca_detail
with attempted publication dates older than when.
"""
+
return self.fetch_child_certs(unpublished = when)
@property
@@ -820,6 +840,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Fetch all revoked_cert objects that link to this ca_detail.
"""
+
return revoked_cert_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,))
@property
@@ -827,6 +848,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Fetch all ROA objects that link to this ca_detail.
"""
+
return rpki.rpkid.roa_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,))
def unpublished_roas(self, when):
@@ -834,6 +856,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
Fetch all unpublished ROA objects linked to this ca_detail with
attempted publication dates older than when.
"""
+
return rpki.rpkid.roa_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s AND published IS NOT NULL and published < %s", (self.ca_detail_id, when))
@property
@@ -841,6 +864,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Fetch all Ghostbuster objects that link to this ca_detail.
"""
+
return rpki.rpkid.ghostbuster_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,))
@property
@@ -848,6 +872,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Fetch all EE certificate objects that link to this ca_detail.
"""
+
return rpki.rpkid.ee_cert_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,))
def unpublished_ghostbusters(self, when):
@@ -855,6 +880,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
Fetch all unpublished Ghostbusters objects linked to this
ca_detail with attempted publication dates older than when.
"""
+
return rpki.rpkid.ghostbuster_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s AND published IS NOT NULL and published < %s", (self.ca_detail_id, when))
@property
@@ -862,6 +888,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Return publication URI for this ca_detail's CRL.
"""
+
return self.ca.sia_uri + self.crl_uri_tail
@property
@@ -869,6 +896,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Return tail (filename portion) of publication URI for this ca_detail's CRL.
"""
+
return self.public_key.gSKI() + ".crl"
@property
@@ -876,12 +904,14 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Return publication URI for this ca_detail's manifest.
"""
+
return self.ca.sia_uri + self.public_key.gSKI() + ".mft"
def has_expired(self):
"""
Return whether this ca_detail's certificate has expired.
"""
+
return self.latest_ca_cert.getNotAfter() <= rpki.sundial.now()
def covers(self, target):
@@ -1237,6 +1267,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Check result of CRL publication.
"""
+
pdu.raise_if_error()
self.crl_published = None
self.sql_mark_dirty()
@@ -1294,6 +1325,7 @@ class ca_detail_obj(rpki.sql.sql_persistent):
"""
Check result of manifest publication.
"""
+
pdu.raise_if_error()
self.manifest_published = None
self.sql_mark_dirty()
@@ -1425,6 +1457,7 @@ class child_cert_obj(rpki.sql.sql_persistent):
"""
Initialize a child_cert_obj.
"""
+
rpki.sql.sql_persistent.__init__(self)
self.gctx = gctx
self.child_id = child_id
@@ -1440,6 +1473,7 @@ class child_cert_obj(rpki.sql.sql_persistent):
"""
Fetch child object to which this child_cert object links.
"""
+
return rpki.left_right.child_elt.sql_fetch(self.gctx, self.child_id)
@property
@@ -1448,6 +1482,7 @@ class child_cert_obj(rpki.sql.sql_persistent):
"""
Fetch ca_detail object to which this child_cert object links.
"""
+
return ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
@ca_detail.deleter
@@ -1462,6 +1497,7 @@ class child_cert_obj(rpki.sql.sql_persistent):
"""
Return the tail (filename) portion of the URI for this child_cert.
"""
+
return self.cert.gSKI() + ".cer"
@property
@@ -1469,6 +1505,7 @@ class child_cert_obj(rpki.sql.sql_persistent):
"""
Return the publication URI for this child_cert.
"""
+
return self.ca_detail.ca.sia_uri + self.uri_tail
def revoke(self, publisher, generate_crl_and_manifest = True):
@@ -1613,6 +1650,7 @@ class child_cert_obj(rpki.sql.sql_persistent):
"""
Publication callback: check result and mark published.
"""
+
pdu.raise_if_error()
self.published = None
self.sql_mark_dirty()
@@ -1637,6 +1675,7 @@ class revoked_cert_obj(rpki.sql.sql_persistent):
"""
Initialize a revoked_cert_obj.
"""
+
rpki.sql.sql_persistent.__init__(self)
self.gctx = gctx
self.serial = serial
@@ -1652,6 +1691,7 @@ class revoked_cert_obj(rpki.sql.sql_persistent):
"""
Fetch ca_detail object to which this revoked_cert_obj links.
"""
+
return ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
@classmethod
@@ -1659,6 +1699,7 @@ class revoked_cert_obj(rpki.sql.sql_persistent):
"""
Revoke a certificate.
"""
+
return cls(
serial = cert.getSerial(),
expires = cert.getNotAfter(),
@@ -1700,6 +1741,7 @@ class roa_obj(rpki.sql.sql_persistent):
"""
Fetch ca_detail object to which this roa_obj links.
"""
+
return rpki.rpkid.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
@ca_detail.deleter
@@ -1713,6 +1755,7 @@ class roa_obj(rpki.sql.sql_persistent):
"""
Extra SQL fetch actions for roa_obj -- handle prefix lists.
"""
+
for version, datatype, attribute in ((4, rpki.resource_set.roa_prefix_set_ipv4, "ipv4"),
(6, rpki.resource_set.roa_prefix_set_ipv6, "ipv6")):
setattr(self, attribute, datatype.from_sql(
@@ -1727,6 +1770,7 @@ class roa_obj(rpki.sql.sql_persistent):
"""
Extra SQL insert actions for roa_obj -- handle prefix lists.
"""
+
for version, prefix_set in ((4, self.ipv4), (6, self.ipv6)):
if prefix_set:
self.gctx.sql.executemany(
@@ -1741,6 +1785,7 @@ class roa_obj(rpki.sql.sql_persistent):
"""
Extra SQL delete actions for roa_obj -- handle prefix lists.
"""
+
self.gctx.sql.execute("DELETE FROM roa_prefix WHERE roa_id = %s", (self.roa_id,))
def __repr__(self):
@@ -1946,6 +1991,7 @@ class roa_obj(rpki.sql.sql_persistent):
"""
Reissue ROA associated with this roa_obj.
"""
+
if self.ca_detail is None:
self.generate(publisher = publisher, fast = fast)
else:
@@ -1955,6 +2001,7 @@ class roa_obj(rpki.sql.sql_persistent):
"""
Return publication URI for a public key.
"""
+
return self.ca_detail.ca.sia_uri + key.gSKI() + ".roa"
@property
@@ -1962,6 +2009,7 @@ class roa_obj(rpki.sql.sql_persistent):
"""
Return the publication URI for this roa_obj's ROA.
"""
+
return self.ca_detail.ca.sia_uri + self.uri_tail
@property
@@ -1970,6 +2018,7 @@ class roa_obj(rpki.sql.sql_persistent):
Return the tail (filename portion) of the publication URI for this
roa_obj's ROA.
"""
+
return self.cert.gSKI() + ".roa"
@@ -2012,6 +2061,7 @@ class ghostbuster_obj(rpki.sql.sql_persistent):
"""
Fetch self object to which this ghostbuster_obj links.
"""
+
return rpki.left_right.self_elt.sql_fetch(self.gctx, self.self_id)
@property
@@ -2020,6 +2070,7 @@ class ghostbuster_obj(rpki.sql.sql_persistent):
"""
Fetch ca_detail object to which this ghostbuster_obj links.
"""
+
return rpki.rpkid.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
def __init__(self, gctx = None, self_id = None, ca_detail_id = None, vcard = None):
@@ -2097,6 +2148,7 @@ class ghostbuster_obj(rpki.sql.sql_persistent):
"""
Check publication result.
"""
+
pdu.raise_if_error()
self.published = None
self.sql_mark_dirty()
@@ -2148,6 +2200,7 @@ class ghostbuster_obj(rpki.sql.sql_persistent):
"""
Reissue Ghostbuster associated with this ghostbuster_obj.
"""
+
if self.ghostbuster is None:
self.generate(publisher = publisher, fast = fast)
else:
@@ -2157,6 +2210,7 @@ class ghostbuster_obj(rpki.sql.sql_persistent):
"""
Return publication URI for a public key.
"""
+
return self.ca_detail.ca.sia_uri + key.gSKI() + ".gbr"
@property
@@ -2164,6 +2218,7 @@ class ghostbuster_obj(rpki.sql.sql_persistent):
"""
Return the publication URI for this ghostbuster_obj's ghostbuster.
"""
+
return self.ca_detail.ca.sia_uri + self.uri_tail
@property
@@ -2172,6 +2227,7 @@ class ghostbuster_obj(rpki.sql.sql_persistent):
Return the tail (filename portion) of the publication URI for this
ghostbuster_obj's ghostbuster.
"""
+
return self.cert.gSKI() + ".gbr"
@@ -2209,6 +2265,7 @@ class ee_cert_obj(rpki.sql.sql_persistent):
"""
Fetch self object to which this ee_cert_obj links.
"""
+
return rpki.left_right.self_elt.sql_fetch(self.gctx, self.self_id)
@property
@@ -2217,6 +2274,7 @@ class ee_cert_obj(rpki.sql.sql_persistent):
"""
Fetch ca_detail object to which this ee_cert_obj links.
"""
+
return rpki.rpkid.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
@ca_detail.deleter
@@ -2234,6 +2292,7 @@ class ee_cert_obj(rpki.sql.sql_persistent):
Although, really, one has to ask why we don't just store g(SKI)
in rpkid.sql instead of ski....
"""
+
return base64.urlsafe_b64encode(self.ski).rstrip("=")
@gski.setter
@@ -2245,6 +2304,7 @@ class ee_cert_obj(rpki.sql.sql_persistent):
"""
Return the publication URI for this ee_cert_obj.
"""
+
return self.ca_detail.ca.sia_uri + self.uri_tail
@property
@@ -2253,6 +2313,7 @@ class ee_cert_obj(rpki.sql.sql_persistent):
Return the tail (filename portion) of the publication URI for this
ee_cert_obj.
"""
+
return self.cert.gSKI() + ".cer"
@classmethod
@@ -2408,6 +2469,7 @@ class ee_cert_obj(rpki.sql.sql_persistent):
"""
Publication callback: check result and mark published.
"""
+
pdu.raise_if_error()
self.published = None
self.sql_mark_dirty()
diff --git a/rpki/sql.py b/rpki/sql.py
index 31ed40ee..9e805ad1 100644
--- a/rpki/sql.py
+++ b/rpki/sql.py
@@ -118,6 +118,7 @@ class session(object):
Clear the SQL object cache. Shouldn't be necessary now that the
cache uses weak references, but should be harmless.
"""
+
logger.debug("Clearing SQL cache")
self.assert_pristine()
self.cache.clear()
@@ -126,12 +127,14 @@ class session(object):
"""
Assert that there are no dirty objects in the cache.
"""
+
assert not self.dirty, "Dirty objects in SQL cache: %s" % self.dirty
def sweep(self):
"""
Write any dirty objects out to SQL.
"""
+
for s in self.dirty.copy():
#if s.sql_cache_debug:
logger.debug("Sweeping (%s) %r", "deleting" if s.sql_deleted else "storing", s)
@@ -150,6 +153,7 @@ class template(object):
"""
Build a SQL template.
"""
+
type_map = dict((x[0], x[1]) for x in data_columns if isinstance(x, tuple))
data_columns = tuple(isinstance(x, tuple) and x[0] or x for x in data_columns)
columns = (index_column,) + data_columns
@@ -220,6 +224,7 @@ class sql_persistent(object):
"""
Fetch one object from SQL, based on an arbitrary SQL WHERE expression.
"""
+
results = cls.sql_fetch_where(gctx, where, args, also_from)
if len(results) == 0:
return None
@@ -235,6 +240,7 @@ class sql_persistent(object):
"""
Fetch all objects of this type from SQL.
"""
+
return cls.sql_fetch_where(gctx, None)
@classmethod
@@ -242,6 +248,7 @@ class sql_persistent(object):
"""
Fetch objects of this type matching an arbitrary SQL WHERE expression.
"""
+
if where is None:
assert args is None and also_from is None
if cls.sql_debug:
@@ -269,6 +276,7 @@ class sql_persistent(object):
"""
Initialize one Python object from the result of a SQL query.
"""
+
self = cls()
self.gctx = gctx
self.sql_decode(dict(zip(cls.sql_template.columns, row)))
@@ -281,6 +289,7 @@ class sql_persistent(object):
"""
Mark this object as needing to be written back to SQL.
"""
+
if self.sql_cache_debug and not self.sql_is_dirty:
logger.debug("Marking %r SQL dirty", self)
self.gctx.sql.dirty.add(self)
@@ -289,6 +298,7 @@ class sql_persistent(object):
"""
Mark this object as not needing to be written back to SQL.
"""
+
if self.sql_cache_debug and self.sql_is_dirty:
logger.debug("Marking %r SQL clean", self)
self.gctx.sql.dirty.discard(self)
@@ -298,12 +308,14 @@ class sql_persistent(object):
"""
Query whether this object needs to be written back to SQL.
"""
+
return self in self.gctx.sql.dirty
def sql_mark_deleted(self):
"""
Mark this object as needing to be deleted in SQL.
"""
+
self.sql_deleted = True
self.sql_mark_dirty()
@@ -311,6 +323,7 @@ class sql_persistent(object):
"""
Store this object to SQL.
"""
+
args = self.sql_encode()
if not self.sql_in_db:
if self.sql_debug:
@@ -333,6 +346,7 @@ class sql_persistent(object):
"""
Delete this object from SQL.
"""
+
if self.sql_in_db:
id = getattr(self, self.sql_template.index) # pylint: disable=W0622
if self.sql_debug:
@@ -352,6 +366,7 @@ class sql_persistent(object):
mapping between column names in SQL and attribute names in Python.
If you need something fancier, override this.
"""
+
d = dict((a, getattr(self, a, None)) for a in self.sql_template.columns)
for i in self.sql_template.map:
if d.get(i) is not None:
@@ -365,6 +380,7 @@ class sql_persistent(object):
between column names in SQL and attribute names in Python. If you
need something fancier, override this.
"""
+
for a in self.sql_template.columns:
if vals.get(a) is not None and a in self.sql_template.map:
setattr(self, a, self.sql_template.map[a].from_sql(vals[a]))
@@ -375,18 +391,21 @@ class sql_persistent(object):
"""
Customization hook.
"""
+
pass
def sql_insert_hook(self):
"""
Customization hook.
"""
+
pass
def sql_update_hook(self):
"""
Customization hook.
"""
+
self.sql_delete_hook()
self.sql_insert_hook()
@@ -394,6 +413,7 @@ class sql_persistent(object):
"""
Customization hook.
"""
+
pass
diff --git a/rpki/sql_schemas.py b/rpki/sql_schemas.py
index 1b9f91be..e3b74b52 100644
--- a/rpki/sql_schemas.py
+++ b/rpki/sql_schemas.py
@@ -245,7 +245,7 @@ CREATE TABLE ee_cert (
## @var pubd
## SQL schema pubd
-pubd = '''-- $Id: pubd.sql 5883 2014-07-03 19:21:31Z sra $
+pubd = '''-- $Id: pubd.sql 5884 2014-07-04 00:37:08Z sra $
-- Copyright (C) 2012--2014 Dragon Research Labs ("DRL")
-- Portions copyright (C) 2009--2010 Internet Systems Consortium ("ISC")
@@ -270,8 +270,9 @@ pubd = '''-- $Id: pubd.sql 5883 2014-07-03 19:21:31Z sra $
-- to satisfy FOREIGN KEY constraints.
DROP TABLE IF EXISTS object;
-DROP TABLE IF EXISTS client;
+DROP TABLE IF EXISTS snapshot;
DROP TABLE IF EXISTS session;
+DROP TABLE IF EXISTS client;
-- An old table that should just be flushed if present at all.
@@ -291,27 +292,39 @@ CREATE TABLE client (
CREATE TABLE session (
session_id SERIAL NOT NULL,
uuid VARCHAR(36) NOT NULL,
- serial BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (session_id),
UNIQUE (uuid)
) ENGINE=InnoDB;
+CREATE TABLE snapshot (
+ snapshot_id SERIAL NOT NULL,
+ activated DATETIME,
+ expires DATETIME,
+ session_id BIGINT UNSIGNED NOT NULL,
+ PRIMARY KEY (snapshot_id),
+ CONSTRAINT snapshot_session_id
+ FOREIGN KEY (session_id) REFERENCES session (session_id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
CREATE TABLE object (
object_id SERIAL NOT NULL,
uri VARCHAR(255) NOT NULL,
- hash CHAR(32) BINARY NOT NULL,
+ hash BINARY(32) NOT NULL,
payload LONGBLOB NOT NULL,
- published BIGINT UNSIGNED NOT NULL,
- withdrawn BIGINT UNSIGNED,
+ published_snapshot_id BIGINT UNSIGNED,
+ withdrawn_snapshot_id BIGINT UNSIGNED,
client_id BIGINT UNSIGNED NOT NULL,
session_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (object_id),
+ CONSTRAINT object_published_snapshot_id
+ FOREIGN KEY (published_snapshot_id) REFERENCES snapshot (snapshot_id) ON DELETE SET NULL,
+ CONSTRAINT object_withdrawn_snapshot_id
+ FOREIGN KEY (withdrawn_snapshot_id) REFERENCES snapshot (snapshot_id) ON DELETE CASCADE,
CONSTRAINT object_client_id
FOREIGN KEY (client_id) REFERENCES client (client_id) ON DELETE CASCADE,
CONSTRAINT object_session_id
FOREIGN KEY (session_id) REFERENCES session (session_id) ON DELETE CASCADE,
- UNIQUE (uri),
- UNIQUE (hash)
+ UNIQUE (session_id, hash)
) ENGINE=InnoDB;
-- Local Variables:
diff --git a/rpki/sundial.py b/rpki/sundial.py
index 7be122c8..60037277 100644
--- a/rpki/sundial.py
+++ b/rpki/sundial.py
@@ -51,6 +51,7 @@ def now():
"""
Get current timestamp.
"""
+
return datetime.utcnow()
class ParseFailure(Exception):
@@ -69,6 +70,7 @@ class datetime(pydatetime.datetime):
Convert to seconds from epoch (like time.time()). Conversion
method is a bit silly, but avoids time module timezone whackiness.
"""
+
return int(self.strftime("%s"))
@classmethod
@@ -76,6 +78,7 @@ class datetime(pydatetime.datetime):
"""
Convert from XML time representation.
"""
+
if x is None:
return None
else:
@@ -85,6 +88,7 @@ class datetime(pydatetime.datetime):
"""
Convert to XML time representation.
"""
+
return self.strftime("%Y-%m-%dT%H:%M:%SZ")
def __str__(self):
@@ -96,6 +100,7 @@ class datetime(pydatetime.datetime):
Convert a datetime.datetime object into this subclass. This is
whacky due to the weird constructors for datetime.
"""
+
return cls.combine(x.date(), x.time())
def to_datetime(self):
@@ -104,6 +109,7 @@ class datetime(pydatetime.datetime):
shouldn't be necessary, but convincing SQL interfaces to use
subclasses of datetime can be hard.
"""
+
return pydatetime.datetime(year = self.year, month = self.month, day = self.day,
hour = self.hour, minute = self.minute, second = self.second,
microsecond = 0, tzinfo = None)
@@ -115,6 +121,7 @@ class datetime(pydatetime.datetime):
Convert from the format OpenSSL's command line tool uses into this
subclass. May require rewriting if we run into locale problems.
"""
+
if x.startswith("notBefore=") or x.startswith("notAfter="):
x = x.partition("=")[2]
return cls.strptime(x, "%b %d %H:%M:%S %Y GMT")
@@ -124,24 +131,28 @@ class datetime(pydatetime.datetime):
"""
Convert from SQL storage format.
"""
+
return cls.from_datetime(x)
def to_sql(self):
"""
Convert to SQL storage format.
"""
+
return self.to_datetime()
def later(self, other):
"""
Return the later of two timestamps.
"""
+
return other if other > self else self
def earlier(self, other):
"""
Return the earlier of two timestamps.
"""
+
return other if other < self else self
def __add__(self, y): return _cast(pydatetime.datetime.__add__(self, y))
@@ -216,6 +227,7 @@ class timedelta(pydatetime.timedelta):
"""
Parse text into a timedelta object.
"""
+
if not isinstance(arg, str):
return cls(seconds = arg)
elif arg.isdigit():
@@ -237,6 +249,7 @@ class timedelta(pydatetime.timedelta):
"""
Convert a timedelta interval to seconds.
"""
+
return self.days * 24 * 60 * 60 + self.seconds
@classmethod
@@ -244,6 +257,7 @@ class timedelta(pydatetime.timedelta):
"""
Convert a datetime.timedelta object into this subclass.
"""
+
return cls(days = x.days, seconds = x.seconds, microseconds = x.microseconds)
def __abs__(self): return _cast(pydatetime.timedelta.__abs__(self))
@@ -264,6 +278,7 @@ def _cast(x):
"""
Cast result of arithmetic operations back into correct subtype.
"""
+
if isinstance(x, pydatetime.datetime):
return datetime.from_datetime(x)
if isinstance(x, pydatetime.timedelta):
diff --git a/rpki/up_down.py b/rpki/up_down.py
index 4c2604bf..df45c8c2 100644
--- a/rpki/up_down.py
+++ b/rpki/up_down.py
@@ -51,6 +51,7 @@ class base_elt(object):
Some elements have no attributes and we only care about their
text content.
"""
+
pass
def endElement(self, stack, name, text):
@@ -59,12 +60,14 @@ class base_elt(object):
If we don't need to do anything else, just pop the stack.
"""
+
stack.pop()
def make_elt(self, name, *attrs):
"""
Construct a element, copying over a set of attributes.
"""
+
elt = lxml.etree.Element("{%s}%s" % (xmlns, name), nsmap=nsmap)
for key in attrs:
val = getattr(self, key, None)
@@ -76,6 +79,7 @@ class base_elt(object):
"""
Construct a sub-element with Base64 text content.
"""
+
if value is not None and not value.empty():
lxml.etree.SubElement(elt, "{%s}%s" % (xmlns, name), nsmap=nsmap).text = value.get_Base64()
@@ -83,12 +87,14 @@ class base_elt(object):
"""
Default PDU handler to catch unexpected types.
"""
+
raise rpki.exceptions.BadQuery("Unexpected query type %s" % q_msg.type)
def check_response(self):
"""
Placeholder for response checking.
"""
+
pass
class multi_uri(list):
@@ -100,6 +106,7 @@ class multi_uri(list):
"""
Initialize a set of URIs, which includes basic some syntax checking.
"""
+
list.__init__(self)
if isinstance(ini, (list, tuple)):
self[:] = ini
@@ -115,12 +122,14 @@ class multi_uri(list):
"""
Convert a multi_uri back to a string representation.
"""
+
return ",".join(self)
def rsync(self):
"""
Find first rsync://... URI in self.
"""
+
for s in self:
if s.startswith("rsync://"):
return s
@@ -135,6 +144,7 @@ class certificate_elt(base_elt):
"""
Handle attributes of <certificate/> element.
"""
+
assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack)
self.cert_url = multi_uri(attrs["cert_url"])
self.req_resource_set_as = rpki.resource_set.resource_set_as(attrs.get("req_resource_set_as"))
@@ -145,6 +155,7 @@ class certificate_elt(base_elt):
"""
Handle text content of a <certificate/> element.
"""
+
assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack)
self.cert = rpki.x509.X509(Base64 = text)
stack.pop()
@@ -153,6 +164,7 @@ class certificate_elt(base_elt):
"""
Generate a <certificate/> element.
"""
+
elt = self.make_elt("certificate", "cert_url",
"req_resource_set_as", "req_resource_set_ipv4", "req_resource_set_ipv6")
elt.text = self.cert.get_Base64()
@@ -169,6 +181,7 @@ class class_elt(base_elt):
"""
Initialize class_elt.
"""
+
base_elt.__init__(self)
self.certs = []
@@ -176,6 +189,7 @@ class class_elt(base_elt):
"""
Handle <class/> elements and their children.
"""
+
if name == "certificate":
cert = certificate_elt()
self.certs.append(cert)
@@ -195,6 +209,7 @@ class class_elt(base_elt):
"""
Handle <class/> elements and their children.
"""
+
if name == "issuer":
self.issuer = rpki.x509.X509(Base64 = text)
else:
@@ -205,6 +220,7 @@ class class_elt(base_elt):
"""
Generate a <class/> element.
"""
+
elt = self.make_elt("class", "class_name", "cert_url", "resource_set_as",
"resource_set_ipv4", "resource_set_ipv6",
"resource_set_notafter", "suggested_sia_head")
@@ -216,6 +232,7 @@ class class_elt(base_elt):
"""
Build a resource_bag from from this <class/> element.
"""
+
return rpki.resource_set.resource_bag(self.resource_set_as,
self.resource_set_ipv4,
self.resource_set_ipv6,
@@ -225,6 +242,7 @@ class class_elt(base_elt):
"""
Set resources of this class element from a resource_bag.
"""
+
self.resource_set_as = bag.asn
self.resource_set_ipv4 = bag.v4
self.resource_set_ipv6 = bag.v6
@@ -236,7 +254,10 @@ class list_pdu(base_elt):
"""
def toXML(self):
- """Generate (empty) payload of "list" PDU."""
+ """
+ Generate (empty) payload of "list" PDU.
+ """
+
return []
def serve_pdu(self, q_msg, r_msg, child, callback, errback):
@@ -283,6 +304,7 @@ class list_pdu(base_elt):
"""
Send a "list" query to parent.
"""
+
try:
logger.info('Sending "list" request to parent %s', parent.parent_handle)
parent.query_up_down(cls(), cb, eb)
@@ -300,6 +322,7 @@ class class_response_syntax(base_elt):
"""
Initialize class_response_syntax.
"""
+
base_elt.__init__(self)
self.classes = []
@@ -307,6 +330,7 @@ class class_response_syntax(base_elt):
"""
Handle "list_response" and "issue_response" PDUs.
"""
+
assert name == "class", "Unexpected name %s, stack %s" % (name, stack)
c = class_elt()
self.classes.append(c)
@@ -314,13 +338,17 @@ class class_response_syntax(base_elt):
c.startElement(stack, name, attrs)
def toXML(self):
- """Generate payload of "list_response" and "issue_response" PDUs."""
+ """
+ Generate payload of "list_response" and "issue_response" PDUs.
+ """
+
return [c.toXML() for c in self.classes]
class list_response_pdu(class_response_syntax):
"""
Up-Down protocol "list_response" PDU.
"""
+
pass
class issue_pdu(base_elt):
@@ -332,6 +360,7 @@ class issue_pdu(base_elt):
"""
Handle "issue" PDU.
"""
+
assert name == "request", "Unexpected name %s, stack %s" % (name, stack)
self.class_name = attrs["class_name"]
self.req_resource_set_as = rpki.resource_set.resource_set_as(attrs.get("req_resource_set_as"))
@@ -342,6 +371,7 @@ class issue_pdu(base_elt):
"""
Handle "issue" PDU.
"""
+
assert name == "request", "Unexpected name %s, stack %s" % (name, stack)
self.pkcs10 = rpki.x509.PKCS10(Base64 = text)
stack.pop()
@@ -350,6 +380,7 @@ class issue_pdu(base_elt):
"""
Generate payload of "issue" PDU.
"""
+
elt = self.make_elt("request", "class_name", "req_resource_set_as",
"req_resource_set_ipv4", "req_resource_set_ipv6")
elt.text = self.pkcs10.get_Base64()
@@ -433,6 +464,7 @@ class issue_pdu(base_elt):
"""
Send an "issue" request to parent associated with ca.
"""
+
assert ca_detail is not None and ca_detail.state in ("pending", "active")
self = cls()
self.class_name = ca.parent_resource_class
@@ -454,6 +486,7 @@ class issue_response_pdu(class_response_syntax):
Check whether this looks like a reasonable issue_response PDU.
XML schema should be tighter for this response.
"""
+
if len(self.classes) != 1 or len(self.classes[0].certs) != 1:
raise rpki.exceptions.BadIssueResponse
@@ -463,12 +496,18 @@ class revoke_syntax(base_elt):
"""
def startElement(self, stack, name, attrs):
- """Handle "revoke" PDU."""
+ """
+ Handle "revoke" PDU.
+ """
+
self.class_name = attrs["class_name"]
self.ski = attrs["ski"]
def toXML(self):
- """Generate payload of "revoke" PDU."""
+ """
+ Generate payload of "revoke" PDU.
+ """
+
return [self.make_elt("key", "class_name", "ski")]
class revoke_pdu(revoke_syntax):
@@ -480,6 +519,7 @@ class revoke_pdu(revoke_syntax):
"""
Convert g(SKI) encoding from PDU back to raw SKI.
"""
+
return base64.urlsafe_b64decode(self.ski + "=")
def serve_pdu(self, q_msg, r_msg, child, cb, eb):
@@ -506,6 +546,7 @@ class revoke_pdu(revoke_syntax):
"""
Send a "revoke" request for certificate(s) named by gski to parent associated with ca.
"""
+
parent = ca.parent
self = cls()
self.class_name = ca.parent_resource_class
@@ -546,6 +587,7 @@ class error_response_pdu(base_elt):
"""
Initialize an error_response PDU from an exception object.
"""
+
base_elt.__init__(self)
if exception is not None:
logger.debug("Constructing up-down error response from exception %s", exception)
@@ -571,6 +613,7 @@ class error_response_pdu(base_elt):
"""
Handle "error_response" PDU.
"""
+
if name == "status":
code = int(text)
if code not in self.codes:
@@ -587,6 +630,7 @@ class error_response_pdu(base_elt):
"""
Generate payload of "error_response" PDU.
"""
+
assert self.status in self.codes
elt = self.make_elt("status")
elt.text = str(self.status)
@@ -603,6 +647,7 @@ class error_response_pdu(base_elt):
Handle an error response. For now, just raise an exception,
perhaps figure out something more clever to do later.
"""
+
raise rpki.exceptions.UpstreamError(self.codes[self.status])
class message_pdu(base_elt):
@@ -629,6 +674,7 @@ class message_pdu(base_elt):
"""
Generate payload of message PDU.
"""
+
elt = self.make_elt("message", "version", "sender", "recipient", "type")
elt.extend(self.payload.toXML())
return elt
@@ -641,6 +687,7 @@ class message_pdu(base_elt):
attribute, so after some basic checks we have to instantiate the
right class object to handle whatever kind of PDU this is.
"""
+
assert name == "message", "Unexpected name %s, stack %s" % (name, stack)
assert self.version == int(attrs["version"])
self.sender = attrs["sender"]
@@ -653,6 +700,7 @@ class message_pdu(base_elt):
"""
Convert a message PDU to a string.
"""
+
return lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "UTF-8")
def serve_top_level(self, child, callback):
@@ -684,12 +732,14 @@ class message_pdu(base_elt):
"""
Log query we're handling. Separate method so rootd can override.
"""
+
logger.info("Serving %s query from child %s [sender %s, recipient %s]", self.type, child.child_handle, self.sender, self.recipient)
def serve_error(self, exception):
"""
Generate an error_response message PDU.
"""
+
r_msg = message_pdu()
r_msg.sender = self.recipient
r_msg.recipient = self.sender
@@ -702,6 +752,7 @@ class message_pdu(base_elt):
"""
Construct one message PDU.
"""
+
assert not cls.type2name[type(payload)].endswith("_response")
if sender is None:
sender = "tweedledee"
diff --git a/rpki/x509.py b/rpki/x509.py
index a7e4d17a..bf0d33f0 100644
--- a/rpki/x509.py
+++ b/rpki/x509.py
@@ -57,6 +57,7 @@ def base64_with_linebreaks(der):
Encode DER (really, anything) as Base64 text, with linebreaks to
keep the result (sort of) readable.
"""
+
b = base64.b64encode(der)
n = len(b)
return "\n" + "\n".join(b[i : min(i + 64, n)] for i in xrange(0, n, 64)) + "\n"
@@ -81,6 +82,27 @@ def first_rsync_uri(xia):
return uri
return None
+def sha1(bytes):
+ """
+ Calculate SHA-1 digest of some data.
+ Convenience wrapper around rpki.POW.Digest class.
+ """
+
+ d = rpki.POW.Digest(rpki.POW.SHA1_DIGEST)
+ d.update(bytes)
+ return d.digest()
+
+def sha256(bytes):
+ """
+ Calculate SHA-256 digest of some data.
+ Convenience wrapper around rpki.POW.Digest class.
+ """
+
+ d = rpki.POW.Digest(rpki.POW.SHA256_DIGEST)
+ d.update(bytes)
+ return d.digest()
+
+
class X501DN(object):
"""
Class to hold an X.501 Distinguished Name.
@@ -207,12 +229,14 @@ class DER_object(object):
"""
Test whether this object is empty.
"""
+
return all(getattr(self, a, None) is None for a in self.formats)
def clear(self):
"""
Make this object empty.
"""
+
for a in self.formats + self.other_clear:
setattr(self, a, None)
self.filename = None
@@ -223,6 +247,7 @@ class DER_object(object):
"""
Initialize a DER_object.
"""
+
self.clear()
if len(kw):
self.set(**kw)
@@ -271,6 +296,7 @@ class DER_object(object):
"""
Check for updates to a DER object that auto-updates from a file.
"""
+
if self.filename is None:
return
try:
@@ -301,6 +327,7 @@ class DER_object(object):
"""
Perform basic checks on a DER object.
"""
+
self.check_auto_update()
assert not self.empty()
@@ -309,6 +336,7 @@ class DER_object(object):
Set the POW value of this object based on a PEM input value.
Subclasses may need to override this.
"""
+
assert self.empty()
self.POW = self.POW_class.pemRead(pem)
@@ -317,6 +345,7 @@ class DER_object(object):
Get the DER value of this object.
Subclasses may need to override this method.
"""
+
self.check()
if self.DER:
return self.DER
@@ -330,6 +359,7 @@ class DER_object(object):
Get the rpki.POW value of this object.
Subclasses may need to override this method.
"""
+
self.check()
if not self.POW: # pylint: disable=E0203
self.POW = self.POW_class.derRead(self.get_DER())
@@ -339,18 +369,21 @@ class DER_object(object):
"""
Get the Base64 encoding of the DER value of this object.
"""
+
return base64_with_linebreaks(self.get_DER())
def get_PEM(self):
"""
Get the PEM representation of this object.
"""
+
return self.get_POW().pemWrite()
def __cmp__(self, other):
"""
Compare two DER-encoded objects.
"""
+
if self is None and other is None:
return 0
elif self is None:
@@ -367,6 +400,7 @@ class DER_object(object):
Return hexadecimal string representation of SKI for this object.
Only work for subclasses that implement get_SKI().
"""
+
ski = self.get_SKI()
return ":".join(("%02X" % ord(i) for i in ski)) if ski else ""
@@ -375,6 +409,7 @@ class DER_object(object):
Calculate g(SKI) for this object. Only work for subclasses
that implement get_SKI().
"""
+
return base64.urlsafe_b64encode(self.get_SKI()).rstrip("=")
def hAKI(self):
@@ -382,6 +417,7 @@ class DER_object(object):
Return hexadecimal string representation of AKI for this
object. Only work for subclasses that implement get_AKI().
"""
+
aki = self.get_AKI()
return ":".join(("%02X" % ord(i) for i in aki)) if aki else ""
@@ -390,24 +426,28 @@ class DER_object(object):
Calculate g(AKI) for this object. Only work for subclasses
that implement get_AKI().
"""
+
return base64.urlsafe_b64encode(self.get_AKI()).rstrip("=")
def get_AKI(self):
"""
Get the AKI extension from this object, if supported.
"""
+
return self.get_POW().getAKI()
def get_SKI(self):
"""
Get the SKI extension from this object, if supported.
"""
+
return self.get_POW().getSKI()
def get_EKU(self):
"""
Get the Extended Key Usage extension from this object, if supported.
"""
+
return self.get_POW().getEKU()
def get_SIA(self):
@@ -415,6 +455,7 @@ class DER_object(object):
Get the SIA extension from this object. Only works for subclasses
that support getSIA().
"""
+
return self.get_POW().getSIA()
def get_sia_directory_uri(self):
@@ -422,6 +463,7 @@ class DER_object(object):
Get SIA directory (id-ad-caRepository) URI from this object.
Only works for subclasses that support getSIA().
"""
+
sia = self.get_POW().getSIA()
return None if sia is None else first_rsync_uri(sia[0])
@@ -430,6 +472,7 @@ class DER_object(object):
Get SIA manifest (id-ad-rpkiManifest) URI from this object.
Only works for subclasses that support getSIA().
"""
+
sia = self.get_POW().getSIA()
return None if sia is None else first_rsync_uri(sia[1])
@@ -438,6 +481,7 @@ class DER_object(object):
Get SIA object (id-ad-signedObject) URI from this object.
Only works for subclasses that support getSIA().
"""
+
sia = self.get_POW().getSIA()
return None if sia is None else first_rsync_uri(sia[2])
@@ -446,6 +490,7 @@ class DER_object(object):
Get the SIA extension from this object. Only works for subclasses
that support getAIA().
"""
+
return self.get_POW().getAIA()
def get_aia_uri(self):
@@ -453,6 +498,7 @@ class DER_object(object):
Get AIA (id-ad-caIssuers) URI from this object.
Only works for subclasses that support getAIA().
"""
+
return first_rsync_uri(self.get_POW().getAIA())
def get_basicConstraints(self):
@@ -460,6 +506,7 @@ class DER_object(object):
Get the basicConstraints extension from this object. Only works
for subclasses that support getExtension().
"""
+
return self.get_POW().getBasicConstraints()
def is_CA(self):
@@ -467,6 +514,7 @@ class DER_object(object):
Return True if and only if object has the basicConstraints
extension and its cA value is true.
"""
+
basicConstraints = self.get_basicConstraints()
return basicConstraints is not None and basicConstraints[0]
@@ -474,6 +522,7 @@ class DER_object(object):
"""
Get RFC 3779 resources as rpki.resource_set objects.
"""
+
resources = rpki.resource_set.resource_bag.from_POW_rfc3779(self.get_POW().getRFC3779())
try:
resources.valid_until = self.getNotAfter()
@@ -486,12 +535,14 @@ class DER_object(object):
"""
Convert from SQL storage format.
"""
+
return cls(DER = x)
def to_sql(self):
"""
Convert to SQL storage format.
"""
+
return self.get_DER()
def dumpasn1(self):
@@ -522,11 +573,11 @@ class DER_object(object):
provide more information, but should make sure to include at least
this information at the start of the tracking line.
"""
+
try:
- d = rpki.POW.Digest(rpki.POW.SHA1_DIGEST)
- d.update(self.get_DER())
- return "%s %s %s" % (uri, self.creation_timestamp,
- "".join(("%02X" % ord(b) for b in d.digest())))
+ return "%s %s %s" % (uri,
+ self.creation_timestamp,
+ "".join(("%02X" % ord(b) for b in sha1(self.get_DER()))))
except: # pylint: disable=W0702
return uri
@@ -534,12 +585,14 @@ class DER_object(object):
"""
Pickling protocol -- pickle the DER encoding.
"""
+
return self.get_DER()
def __setstate__(self, state):
"""
Pickling protocol -- unpickle the DER encoding.
"""
+
self.set(DER = state)
class X509(DER_object):
@@ -559,48 +612,56 @@ class X509(DER_object):
"""
Get the issuer of this certificate.
"""
+
return X501DN.from_POW(self.get_POW().getIssuer())
def getSubject(self):
"""
Get the subject of this certificate.
"""
+
return X501DN.from_POW(self.get_POW().getSubject())
def getNotBefore(self):
"""
Get the inception time of this certificate.
"""
+
return self.get_POW().getNotBefore()
def getNotAfter(self):
"""
Get the expiration time of this certificate.
"""
+
return self.get_POW().getNotAfter()
def getSerial(self):
"""
Get the serial number of this certificate.
"""
+
return self.get_POW().getSerial()
def getPublicKey(self):
"""
Extract the public key from this certificate.
"""
+
return PublicKey(POW = self.get_POW().getPublicKey())
def get_SKI(self):
"""
Get the SKI extension from this object.
"""
+
return self.get_POW().getSKI()
def expired(self):
"""
Test whether this certificate has expired.
"""
+
return self.getNotAfter() <= rpki.sundial.now()
def issue(self, keypair, subject_key, serial, sia, aia, crldp, notAfter,
@@ -743,6 +804,7 @@ class X509(DER_object):
"""
Issue a BPKI certificate with values taking from an existing certificate.
"""
+
return self.bpki_certify(
keypair = keypair,
subject_name = source_cert.getSubject(),
@@ -759,6 +821,7 @@ class X509(DER_object):
"""
Issue a self-signed BPKI CA certificate.
"""
+
return cls._bpki_certify(
keypair = keypair,
issuer_name = subject_name,
@@ -775,6 +838,7 @@ class X509(DER_object):
"""
Issue a normal BPKI certificate.
"""
+
assert keypair.get_public() == self.getPublicKey()
return self._bpki_certify(
keypair = keypair,
@@ -833,6 +897,7 @@ class X509(DER_object):
allowed cases. So this method allows X509, None, lists, and
tuples, and returns a tuple of X509 objects.
"""
+
if isinstance(chain, cls):
chain = (chain,)
return tuple(x for x in chain if x is not None)
@@ -842,6 +907,7 @@ class X509(DER_object):
"""
Time at which this object was created.
"""
+
return self.getNotBefore()
class PKCS10(DER_object):
@@ -869,6 +935,7 @@ class PKCS10(DER_object):
"""
Get the DER value of this certification request.
"""
+
self.check()
if self.DER:
return self.DER
@@ -881,6 +948,7 @@ class PKCS10(DER_object):
"""
Get the rpki.POW value of this certification request.
"""
+
self.check()
if not self.POW: # pylint: disable=E0203
self.POW = rpki.POW.PKCS10.derRead(self.get_DER())
@@ -890,18 +958,21 @@ class PKCS10(DER_object):
"""
Extract the subject name from this certification request.
"""
+
return X501DN.from_POW(self.get_POW().getSubject())
def getPublicKey(self):
"""
Extract the public key from this certification request.
"""
+
return PublicKey(POW = self.get_POW().getPublicKey())
def get_SKI(self):
"""
Compute SKI for public key from this certification request.
"""
+
return self.getPublicKey().get_SKI()
@@ -1150,6 +1221,7 @@ class PrivateKey(DER_object):
"""
Get the DER value of this keypair.
"""
+
self.check()
if self.DER:
return self.DER
@@ -1162,6 +1234,7 @@ class PrivateKey(DER_object):
"""
Get the rpki.POW value of this keypair.
"""
+
self.check()
if not self.POW: # pylint: disable=E0203
self.POW = rpki.POW.Asymmetric.derReadPrivate(self.get_DER())
@@ -1171,12 +1244,14 @@ class PrivateKey(DER_object):
"""
Get the PEM representation of this keypair.
"""
+
return self.get_POW().pemWritePrivate()
def _set_PEM(self, pem):
"""
Set the POW value of this keypair from a PEM string.
"""
+
assert self.empty()
self.POW = self.POW_class.pemReadPrivate(pem)
@@ -1184,18 +1259,21 @@ class PrivateKey(DER_object):
"""
Get the DER encoding of the public key from this keypair.
"""
+
return self.get_POW().derWritePublic()
def get_SKI(self):
"""
Calculate the SKI of this keypair.
"""
+
return self.get_POW().calculateSKI()
def get_public(self):
"""
Convert the public key of this keypair into a PublicKey object.
"""
+
return PublicKey(DER = self.get_public_DER())
class PublicKey(DER_object):
@@ -1209,6 +1287,7 @@ class PublicKey(DER_object):
"""
Get the DER value of this public key.
"""
+
self.check()
if self.DER:
return self.DER
@@ -1221,6 +1300,7 @@ class PublicKey(DER_object):
"""
Get the rpki.POW value of this public key.
"""
+
self.check()
if not self.POW: # pylint: disable=E0203
self.POW = rpki.POW.Asymmetric.derReadPublic(self.get_DER())
@@ -1230,12 +1310,14 @@ class PublicKey(DER_object):
"""
Get the PEM representation of this public key.
"""
+
return self.get_POW().pemWritePublic()
def _set_PEM(self, pem):
"""
Set the POW value of this public key from a PEM string.
"""
+
assert self.empty()
self.POW = self.POW_class.pemReadPublic(pem)
@@ -1243,6 +1325,7 @@ class PublicKey(DER_object):
"""
Calculate the SKI of this public key.
"""
+
return self.get_POW().calculateSKI()
class KeyParams(DER_object):
@@ -1266,6 +1349,7 @@ class RSA(PrivateKey):
"""
Generate a new keypair.
"""
+
if not quiet:
logger.debug("Generating new %d-bit RSA key", keylength)
if generate_insecure_debug_only_rsa_key is not None:
@@ -1348,6 +1432,7 @@ class CMS_object(DER_object):
"""
Get the DER value of this CMS_object.
"""
+
self.check()
if self.DER:
return self.DER
@@ -1360,6 +1445,7 @@ class CMS_object(DER_object):
"""
Get the rpki.POW value of this CMS_object.
"""
+
self.check()
if not self.POW: # pylint: disable=E0203
self.POW = self.POW_class.derRead(self.get_DER())
@@ -1369,6 +1455,7 @@ class CMS_object(DER_object):
"""
Extract signingTime from CMS signed attributes.
"""
+
return self.get_POW().signingTime()
def verify(self, ta):
@@ -1540,6 +1627,7 @@ class CMS_object(DER_object):
"""
Time at which this object was created.
"""
+
return self.get_signingTime()
@@ -1561,6 +1649,7 @@ class Wrapped_CMS_object(CMS_object):
"""
Get the inner content of this Wrapped_CMS_object.
"""
+
if self.content is None:
raise rpki.exceptions.CMSContentNotSet("Inner content of CMS object %r is not set" % self)
return self.content
@@ -1569,6 +1658,7 @@ class Wrapped_CMS_object(CMS_object):
"""
Set the (inner) content of this Wrapped_CMS_object, clearing the wrapper.
"""
+
self.clear()
self.content = content
@@ -1651,12 +1741,14 @@ class SignedManifest(DER_CMS_object):
"""
Get thisUpdate value from this manifest.
"""
+
return self.get_POW().getThisUpdate()
def getNextUpdate(self):
"""
Get nextUpdate value from this manifest.
"""
+
return self.get_POW().getNextUpdate()
@classmethod
@@ -1667,9 +1759,7 @@ class SignedManifest(DER_CMS_object):
filelist = []
for name, obj in names_and_objs:
- d = rpki.POW.Digest(rpki.POW.SHA256_DIGEST)
- d.update(obj.get_DER())
- filelist.append((name.rpartition("/")[2], d.digest()))
+ filelist.append((name.rpartition("/")[2], sha256(obj.get_DER())))
filelist.sort(key = lambda x: x[0])
obj = cls.POW_class()
@@ -1697,6 +1787,7 @@ class ROA(DER_CMS_object):
"""
Build a ROA.
"""
+
ipv4 = ipv4.to_POW_roa_tuple() if ipv4 else None
ipv6 = ipv6.to_POW_roa_tuple() if ipv6 else None
obj = cls.POW_class()
@@ -1712,6 +1803,7 @@ class ROA(DER_CMS_object):
Return a string containing data we want to log when tracking how
objects move through the RPKI system.
"""
+
msg = DER_CMS_object.tracking_data(self, uri)
try:
self.extract_if_needed()
@@ -1794,6 +1886,7 @@ class XML_CMS_object(Wrapped_CMS_object):
"""
Encode inner content for signing.
"""
+
return lxml.etree.tostring(self.get_content(),
pretty_print = True,
encoding = self.encoding,
@@ -1803,12 +1896,14 @@ class XML_CMS_object(Wrapped_CMS_object):
"""
Decode XML and set inner content.
"""
+
self.content = lxml.etree.fromstring(xml)
def pretty_print_content(self):
"""
Pretty print XML content of this message.
"""
+
return lxml.etree.tostring(self.get_content(),
pretty_print = True,
encoding = self.encoding,
@@ -1818,6 +1913,7 @@ class XML_CMS_object(Wrapped_CMS_object):
"""
Handle XML RelaxNG schema check.
"""
+
try:
self.schema.assertValid(self.get_content())
except lxml.etree.DocumentInvalid:
@@ -1830,6 +1926,7 @@ class XML_CMS_object(Wrapped_CMS_object):
"""
Write DER of current message to disk, for debugging.
"""
+
f = open(prefix + rpki.sundial.now().isoformat() + "Z.cms", "wb")
f.write(self.get_DER())
f.close()
@@ -1838,6 +1935,7 @@ class XML_CMS_object(Wrapped_CMS_object):
"""
Wrap an XML PDU in CMS and return its DER encoding.
"""
+
if self.saxify is None:
self.set_content(msg)
else:
@@ -1853,6 +1951,7 @@ class XML_CMS_object(Wrapped_CMS_object):
"""
Unwrap a CMS-wrapped XML PDU and return Python objects.
"""
+
if self.dump_inbound_cms:
self.dump_inbound_cms.dump(self)
self.verify(ta)
@@ -1869,6 +1968,7 @@ class XML_CMS_object(Wrapped_CMS_object):
timestamp. Raises an exception if the recorded timestamp is more
recent, otherwise returns the new timestamp.
"""
+
new_timestamp = self.get_signingTime()
if timestamp is not None and timestamp > new_timestamp:
if context:
@@ -1884,6 +1984,7 @@ class XML_CMS_object(Wrapped_CMS_object):
"last_cms_timestamp" field of an SQL object and stores the new
timestamp back in that same field.
"""
+
obj.last_cms_timestamp = self.check_replay(obj.last_cms_timestamp, *context)
obj.sql_mark_dirty()
@@ -1913,6 +2014,7 @@ class Ghostbuster(Wrapped_CMS_object):
Encode inner content for signing. At the moment we're treating
the VCard as an opaque byte string, so no encoding needed here.
"""
+
return self.get_content()
def decode(self, vcard):
@@ -1920,6 +2022,7 @@ class Ghostbuster(Wrapped_CMS_object):
Decode XML and set inner content. At the moment we're treating
the VCard as an opaque byte string, so no encoding needed here.
"""
+
self.content = vcard
@classmethod
@@ -1927,6 +2030,7 @@ class Ghostbuster(Wrapped_CMS_object):
"""
Build a Ghostbuster record.
"""
+
self = cls()
self.set_content(vcard)
self.sign(keypair, certs)
@@ -1944,6 +2048,7 @@ class CRL(DER_object):
"""
Get the DER value of this CRL.
"""
+
self.check()
if self.DER:
return self.DER
@@ -1956,6 +2061,7 @@ class CRL(DER_object):
"""
Get the rpki.POW value of this CRL.
"""
+
self.check()
if not self.POW: # pylint: disable=E0203
self.POW = rpki.POW.CRL.derRead(self.get_DER())
@@ -1965,24 +2071,28 @@ class CRL(DER_object):
"""
Get thisUpdate value from this CRL.
"""
+
return self.get_POW().getThisUpdate()
def getNextUpdate(self):
"""
Get nextUpdate value from this CRL.
"""
+
return self.get_POW().getNextUpdate()
def getIssuer(self):
"""
Get issuer value of this CRL.
"""
+
return X501DN.from_POW(self.get_POW().getIssuer())
def getCRLNumber(self):
"""
Get CRL Number value for this CRL.
"""
+
return self.get_POW().getCRLNumber()
@classmethod
@@ -1990,6 +2100,7 @@ class CRL(DER_object):
"""
Generate a new CRL.
"""
+
crl = rpki.POW.CRL()
crl.setVersion(version)
crl.setIssuer(issuer.getSubject().get_POW())
@@ -2006,6 +2117,7 @@ class CRL(DER_object):
"""
Time at which this object was created.
"""
+
return self.getThisUpdate()
## @var uri_dispatch_map
@@ -2024,4 +2136,5 @@ def uri_dispatch(uri):
"""
Return the Python class object corresponding to a given URI.
"""
+
return uri_dispatch_map[os.path.splitext(uri)[1]]
diff --git a/rpki/xml_utils.py b/rpki/xml_utils.py
index 1574cd9e..11c2180e 100644
--- a/rpki/xml_utils.py
+++ b/rpki/xml_utils.py
@@ -56,6 +56,7 @@ class sax_handler(xml.sax.handler.ContentHandler):
"""
Initialize SAX handler.
"""
+
xml.sax.handler.ContentHandler.__init__(self)
self.text = ""
self.stack = []
@@ -64,18 +65,21 @@ class sax_handler(xml.sax.handler.ContentHandler):
"""
Redirect startElementNS() events to startElement().
"""
+
return self.startElement(name[1], attrs)
def endElementNS(self, name, qname):
"""
Redirect endElementNS() events to endElement().
"""
+
return self.endElement(name[1])
def characters(self, content):
"""
Accumulate a chuck of element content (text).
"""
+
self.text += content
def startElement(self, name, attrs):
@@ -111,6 +115,7 @@ class sax_handler(xml.sax.handler.ContentHandler):
Handle endElement() events. Mostly this means handling any
accumulated element text.
"""
+
text = self.text.encode("ascii").strip()
self.text = ""
self.stack[-1].endElement(self.stack, name, text)
@@ -120,6 +125,7 @@ class sax_handler(xml.sax.handler.ContentHandler):
"""
Create a one-off SAX parser, parse an ETree, return the result.
"""
+
self = cls()
lxml.sax.saxify(elt, self)
return self.result
@@ -128,6 +134,7 @@ class sax_handler(xml.sax.handler.ContentHandler):
"""
Handle top-level PDU for this protocol.
"""
+
assert name == self.name and attrs["version"] == self.version
return self.pdu()
@@ -154,6 +161,7 @@ class base_elt(object):
"""
Default startElement() handler: just process attributes.
"""
+
if name not in self.elements:
assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
self.read_attrs(attrs)
@@ -162,6 +170,7 @@ class base_elt(object):
"""
Default endElement() handler: just pop the stack.
"""
+
assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
stack.pop()
@@ -169,12 +178,14 @@ class base_elt(object):
"""
Default toXML() element generator.
"""
+
return self.make_elt()
def read_attrs(self, attrs):
"""
Template-driven attribute reader.
"""
+
for key in self.attributes:
val = attrs.get(key, None)
if isinstance(val, str) and val.isdigit() and not key.endswith("_handle"):
@@ -187,6 +198,7 @@ class base_elt(object):
"""
XML element constructor.
"""
+
elt = lxml.etree.Element("{%s}%s" % (self.xmlns, self.element_name), nsmap = self.nsmap)
for key in self.attributes:
val = getattr(self, key, None)
@@ -201,6 +213,7 @@ class base_elt(object):
"""
Constructor for Base64-encoded subelement.
"""
+
if value is not None and not value.empty():
lxml.etree.SubElement(elt, "{%s}%s" % (self.xmlns, name), nsmap = self.nsmap).text = value.get_Base64()
@@ -208,6 +221,7 @@ class base_elt(object):
"""
Convert a base_elt object to string format.
"""
+
return lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "us-ascii")
@classmethod
@@ -215,6 +229,7 @@ class base_elt(object):
"""
Generic PDU constructor.
"""
+
self = cls()
for k, v in kargs.items():
if isinstance(v, bool):
@@ -235,6 +250,7 @@ class text_elt(base_elt):
"""
Extract text from parsed XML.
"""
+
base_elt.endElement(self, stack, name, text)
setattr(self, self.text_attribute, text)
@@ -242,6 +258,7 @@ class text_elt(base_elt):
"""
Insert text into generated XML.
"""
+
elt = self.make_elt()
elt.text = getattr(self, self.text_attribute) or None
return elt
@@ -258,6 +275,7 @@ class data_elt(base_elt):
that sub-elements are Base64-encoded using the sql_template
mechanism.
"""
+
if name in self.elements:
elt_type = self.sql_template.map.get(name)
assert elt_type is not None, "Couldn't find element type for %s, stack %s" % (name, stack)
@@ -271,6 +289,7 @@ class data_elt(base_elt):
Default element generator for SQL-based objects. This assumes
that sub-elements are Base64-encoded DER objects.
"""
+
elt = self.make_elt()
for i in self.elements:
self.make_b64elt(elt, i, getattr(self, i, None))
@@ -280,6 +299,7 @@ class data_elt(base_elt):
"""
Construct a reply PDU.
"""
+
if r_pdu is None:
r_pdu = self.__class__()
self.make_reply_clone_hook(r_pdu)
@@ -297,6 +317,7 @@ class data_elt(base_elt):
"""
Overridable hook.
"""
+
pass
def serve_fetch_one(self):
@@ -304,6 +325,7 @@ class data_elt(base_elt):
Find the object on which a get, set, or destroy method should
operate.
"""
+
r = self.serve_fetch_one_maybe()
if r is None:
raise rpki.exceptions.NotFound
@@ -313,12 +335,14 @@ class data_elt(base_elt):
"""
Overridable hook.
"""
+
cb()
def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
"""
Overridable hook.
"""
+
cb()
def serve_create(self, r_msg, cb, eb):
@@ -371,6 +395,7 @@ class data_elt(base_elt):
"""
Handle a get action.
"""
+
r_pdu = self.serve_fetch_one()
self.make_reply(r_pdu)
r_msg.append(r_pdu)
@@ -380,6 +405,7 @@ class data_elt(base_elt):
"""
Handle a list action for non-self objects.
"""
+
for r_pdu in self.serve_fetch_all():
self.make_reply(r_pdu)
r_msg.append(r_pdu)
@@ -389,12 +415,14 @@ class data_elt(base_elt):
"""
Overridable hook.
"""
+
cb()
def serve_destroy(self, r_msg, cb, eb):
"""
Handle a destroy action.
"""
+
def done():
db_pdu.sql_delete()
r_msg.append(self.make_reply())
@@ -406,6 +434,7 @@ class data_elt(base_elt):
"""
Action dispatch handler.
"""
+
dispatch = { "create" : self.serve_create,
"set" : self.serve_set,
"get" : self.serve_get,
@@ -419,6 +448,7 @@ class data_elt(base_elt):
"""
Uniform handling for unimplemented control operations.
"""
+
unimplemented = [x for x in controls if getattr(self, x, False)]
if unimplemented:
raise rpki.exceptions.NotImplementedYet("Unimplemented control %s" % ", ".join(unimplemented))
@@ -432,6 +462,7 @@ class msg(list):
"""
Handle top-level PDU.
"""
+
if name == "msg":
assert self.version == int(attrs["version"])
self.type = attrs["type"]
@@ -445,6 +476,7 @@ class msg(list):
"""
Handle top-level PDU.
"""
+
assert name == "msg", "Unexpected name %s, stack %s" % (name, stack)
assert len(stack) == 1
stack.pop()
@@ -453,12 +485,14 @@ class msg(list):
"""
Convert msg object to string.
"""
+
return lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "us-ascii")
def toXML(self):
"""
Generate top-level PDU.
"""
+
elt = lxml.etree.Element("{%s}msg" % (self.xmlns), nsmap = self.nsmap, version = str(self.version), type = self.type)
elt.extend(i.toXML() for i in self)
return elt
@@ -468,6 +502,7 @@ class msg(list):
"""
Create a query PDU.
"""
+
self = cls(args)
self.type = "query"
return self
@@ -477,6 +512,7 @@ class msg(list):
"""
Create a reply PDU.
"""
+
self = cls(args)
self.type = "reply"
return self
@@ -485,10 +521,12 @@ class msg(list):
"""
Is this msg a query?
"""
+
return self.type == "query"
def is_reply(self):
"""
Is this msg a reply?
"""
+
return self.type == "reply"
diff --git a/schemas/sql/pubd.sql b/schemas/sql/pubd.sql
index 3b9bb844..34778491 100644
--- a/schemas/sql/pubd.sql
+++ b/schemas/sql/pubd.sql
@@ -23,8 +23,9 @@
-- to satisfy FOREIGN KEY constraints.
DROP TABLE IF EXISTS object;
-DROP TABLE IF EXISTS client;
+DROP TABLE IF EXISTS snapshot;
DROP TABLE IF EXISTS session;
+DROP TABLE IF EXISTS client;
-- An old table that should just be flushed if present at all.
@@ -44,27 +45,39 @@ CREATE TABLE client (
CREATE TABLE session (
session_id SERIAL NOT NULL,
uuid VARCHAR(36) NOT NULL,
- serial BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (session_id),
UNIQUE (uuid)
) ENGINE=InnoDB;
+CREATE TABLE snapshot (
+ snapshot_id SERIAL NOT NULL,
+ activated DATETIME,
+ expires DATETIME,
+ session_id BIGINT UNSIGNED NOT NULL,
+ PRIMARY KEY (snapshot_id),
+ CONSTRAINT snapshot_session_id
+ FOREIGN KEY (session_id) REFERENCES session (session_id) ON DELETE CASCADE
+) ENGINE=InnoDB;
+
CREATE TABLE object (
object_id SERIAL NOT NULL,
uri VARCHAR(255) NOT NULL,
- hash CHAR(32) BINARY NOT NULL,
+ hash BINARY(32) NOT NULL,
payload LONGBLOB NOT NULL,
- published BIGINT UNSIGNED NOT NULL,
- withdrawn BIGINT UNSIGNED,
+ published_snapshot_id BIGINT UNSIGNED,
+ withdrawn_snapshot_id BIGINT UNSIGNED,
client_id BIGINT UNSIGNED NOT NULL,
session_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (object_id),
+ CONSTRAINT object_published_snapshot_id
+ FOREIGN KEY (published_snapshot_id) REFERENCES snapshot (snapshot_id) ON DELETE SET NULL,
+ CONSTRAINT object_withdrawn_snapshot_id
+ FOREIGN KEY (withdrawn_snapshot_id) REFERENCES snapshot (snapshot_id) ON DELETE CASCADE,
CONSTRAINT object_client_id
FOREIGN KEY (client_id) REFERENCES client (client_id) ON DELETE CASCADE,
CONSTRAINT object_session_id
FOREIGN KEY (session_id) REFERENCES session (session_id) ON DELETE CASCADE,
- UNIQUE (uri),
- UNIQUE (hash)
+ UNIQUE (session_id, hash)
) ENGINE=InnoDB;
-- Local Variables: