aboutsummaryrefslogtreecommitdiff
path: root/rpkid/rpki
diff options
context:
space:
mode:
authorRob Austein <sra@hactrn.net>2012-04-15 04:42:40 +0000
committerRob Austein <sra@hactrn.net>2012-04-15 04:42:40 +0000
commitfd695c2371824c1952510bab9fbe0e05b52b9e9d (patch)
tree60b9836b9d24055d900be3335856ec4e0091cec2 /rpkid/rpki
parentb5eb637d68bd8387cfff7cb06945f6654d1192db (diff)
parentf4d381b2ead3a3fab4b7b0c73cdc8d3a6b4cb12d (diff)
Merge branches/tk161 to trunk.
svn path=/trunk/; revision=4415
Diffstat (limited to 'rpkid/rpki')
-rw-r--r--rpkid/rpki/csv_utils.py100
-rw-r--r--rpkid/rpki/gui/app/AllocationTree.py151
-rw-r--r--rpkid/rpki/gui/app/admin.py62
-rw-r--r--rpkid/rpki/gui/app/asnset.py40
-rw-r--r--rpkid/rpki/gui/app/forms.py493
-rw-r--r--rpkid/rpki/gui/app/glue.py506
-rw-r--r--rpkid/rpki/gui/app/misc.py47
-rw-r--r--rpkid/rpki/gui/app/models.py376
-rwxr-xr-xrpkid/rpki/gui/app/range_list.py244
-rw-r--r--rpkid/rpki/gui/app/settings.py.in14
-rw-r--r--rpkid/rpki/gui/app/templates/app/app_base.html31
-rw-r--r--rpkid/rpki/gui/app/templates/app/bootstrap_form.html33
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_add_resource_form.html16
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_delete_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/child_delete_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_detail.html53
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_form.html17
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_import_form.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_list.html7
-rw-r--r--rpkid/rpki/gui/app/templates/app/client_detail.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/client_import_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/import_child_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/client_list.html1
-rw-r--r--rpkid/rpki/gui/app/templates/app/conf_empty.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/conf_empty.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/conf_list.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/conf_list.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/dashboard.html85
-rw-r--r--rpkid/rpki/gui/app/templates/app/destroy_handle_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/destroy_handle_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/generic_result.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/generic_result.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/ghostbuster_form.html21
-rw-r--r--rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html53
-rw-r--r--rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html13
-rw-r--r--rpkid/rpki/gui/app/templates/app/initialize_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/initialize_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/object_detail.html33
-rw-r--r--rpkid/rpki/gui/app/templates/app/object_list.html36
-rw-r--r--rpkid/rpki/gui/app/templates/app/object_table.html41
-rw-r--r--rpkid/rpki/gui/app/templates/app/parent_detail.html62
-rw-r--r--rpkid/rpki/gui/app/templates/app/parent_import_form.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/parent_list.html5
-rw-r--r--rpkid/rpki/gui/app/templates/app/pubclient_list.html9
-rw-r--r--rpkid/rpki/gui/app/templates/app/repository_detail.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/repository_import_form.html18
-rw-r--r--rpkid/rpki/gui/app/templates/app/repository_list.html7
-rw-r--r--rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html54
-rw-r--r--rpkid/rpki/gui/app/templates/app/roa_request_list.html14
-rw-r--r--rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html58
-rw-r--r--rpkid/rpki/gui/app/templates/app/roarequest_form.html16
-rw-r--r--rpkid/rpki/gui/app/templates/app/route_roa_list.html19
-rw-r--r--rpkid/rpki/gui/app/templates/app/routes_view.html41
-rw-r--r--rpkid/rpki/gui/app/templates/app/update_bpki_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/update_bpki_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/user_confirm_delete.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/user_create_form.html16
-rw-r--r--rpkid/rpki/gui/app/templates/app/user_edit_form.html16
-rw-r--r--rpkid/rpki/gui/app/templates/app/user_list.html29
-rw-r--r--rpkid/rpki/gui/app/templates/base.html72
-rw-r--r--rpkid/rpki/gui/app/templates/registration/login.html41
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/asn_view.html93
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/child_form.html20
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/child_view.html60
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html13
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/dashboard.html193
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html14
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html69
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html18
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html23
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html13
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html13
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html13
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/parent_form.html11
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/parent_view.html38
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html96
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html24
-rw-r--r--rpkid/rpki/gui/app/templatetags/__init__.py0
-rw-r--r--rpkid/rpki/gui/app/templatetags/app_extras.py13
-rw-r--r--rpkid/rpki/gui/app/timestamp.py25
-rw-r--r--rpkid/rpki/gui/app/urls.py101
-rw-r--r--rpkid/rpki/gui/app/views.py1428
-rw-r--r--rpkid/rpki/gui/cacheview/admin.py59
-rw-r--r--rpkid/rpki/gui/cacheview/models.py203
-rw-r--r--rpkid/rpki/gui/models.py132
-rw-r--r--rpkid/rpki/gui/routeview/__init__.py0
-rw-r--r--rpkid/rpki/gui/routeview/models.py46
-rw-r--r--rpkid/rpki/gui/urls.py47
-rw-r--r--rpkid/rpki/http.py2
-rw-r--r--rpkid/rpki/ipaddrs.py4
-rw-r--r--rpkid/rpki/irdb/__init__.py23
-rw-r--r--rpkid/rpki/irdb/models.py585
-rw-r--r--rpkid/rpki/irdb/zookeeper.py1264
-rw-r--r--rpkid/rpki/irdbd.py243
-rw-r--r--rpkid/rpki/left_right.py2
-rw-r--r--rpkid/rpki/myrpki.py10
-rw-r--r--rpkid/rpki/oids.py1
-rw-r--r--rpkid/rpki/old_irdbd.py249
-rw-r--r--rpkid/rpki/pubd.py2
-rw-r--r--rpkid/rpki/rcynic.py11
-rw-r--r--rpkid/rpki/relaxng.py381
-rw-r--r--rpkid/rpki/resource_set.py48
-rw-r--r--rpkid/rpki/rootd.py4
-rw-r--r--rpkid/rpki/rpkic.py486
-rw-r--r--rpkid/rpki/rpkid.py2
-rw-r--r--rpkid/rpki/sql_schemas.py109
-rw-r--r--rpkid/rpki/x509.py258
100 files changed, 6462 insertions, 3057 deletions
diff --git a/rpkid/rpki/csv_utils.py b/rpkid/rpki/csv_utils.py
new file mode 100644
index 00000000..f7eed414
--- /dev/null
+++ b/rpkid/rpki/csv_utils.py
@@ -0,0 +1,100 @@
+"""
+CSV utilities, moved here from myrpki.py.
+
+$Id$
+
+Copyright (C) 2009--2011 Internet Systems Consortium ("ISC")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+"""
+
+import csv
+import os
+
+class BadCSVSyntax(Exception):
+ """
+ Bad CSV syntax.
+ """
+
+class csv_reader(object):
+ """
+ Reader for tab-delimited text that's (slightly) friendlier than the
+ stock Python csv module (which isn't intended for direct use by
+ humans anyway, and neither was this package originally, but that
+ seems to be the way that it has evolved...).
+
+ Columns parameter specifies how many columns users of the reader
+ expect to see; lines with fewer columns will be padded with None
+ values.
+
+ Original API design for this class courtesy of Warren Kumari, but
+ don't blame him if you don't like what I did with his ideas.
+ """
+
+ def __init__(self, filename, columns = None, min_columns = None, comment_characters = "#;"):
+ assert columns is None or isinstance(columns, int)
+ assert min_columns is None or isinstance(min_columns, int)
+ if columns is not None and min_columns is None:
+ min_columns = columns
+ self.filename = filename
+ self.columns = columns
+ self.min_columns = min_columns
+ self.comment_characters = comment_characters
+ self.file = open(filename, "r")
+
+ def __iter__(self):
+ line_number = 0
+ for line in self.file:
+ line_number += 1
+ line = line.strip()
+ if not line or line[0] in self.comment_characters:
+ continue
+ fields = line.split()
+ if self.min_columns is not None and len(fields) < self.min_columns:
+ raise BadCSVSyntax, "%s:%d: Not enough columns in line %r" % (self.filename, line_number, line)
+ if self.columns is not None and len(fields) > self.columns:
+ raise BadCSVSyntax, "%s:%d: Too many columns in line %r" % (self.filename, line_number, line)
+ if self.columns is not None and len(fields) < self.columns:
+ fields += tuple(None for i in xrange(self.columns - len(fields)))
+ yield fields
+
+class csv_writer(object):
+ """
+ Writer object for tab delimited text. We just use the stock CSV
+ module in excel-tab mode for this.
+
+ If "renmwo" is set (default), the file will be written to
+ a temporary name and renamed to the real filename after closing.
+ """
+
+ def __init__(self, filename, renmwo = True):
+ self.filename = filename
+ self.renmwo = "%s.~renmwo%d~" % (filename, os.getpid()) if renmwo else filename
+ self.file = open(self.renmwo, "w")
+ self.writer = csv.writer(self.file, dialect = csv.get_dialect("excel-tab"))
+
+ def close(self):
+ """
+ Close this writer.
+ """
+ if self.file is not None:
+ self.file.close()
+ self.file = None
+ if self.filename != self.renmwo:
+ os.rename(self.renmwo, self.filename)
+
+ def __getattr__(self, attr):
+ """
+ Fake inheritance from whatever object csv.writer deigns to give us.
+ """
+ return getattr(self.writer, attr)
diff --git a/rpkid/rpki/gui/app/AllocationTree.py b/rpkid/rpki/gui/app/AllocationTree.py
deleted file mode 100644
index f51ed430..00000000
--- a/rpkid/rpki/gui/app/AllocationTree.py
+++ /dev/null
@@ -1,151 +0,0 @@
-# $Id$
-"""
-Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
-
-from rpki.gui.app import misc, models
-from rpki import resource_set
-
-class AllocationTree(object):
- '''Virtual class representing a tree of unallocated resource ranges.
- Keeps track of which subsets of a resource range have been
- allocated.'''
-
- def __init__(self, resource):
- self.resource = resource
- self.range = resource.as_resource_range()
- self.need_calc = True
-
- def calculate(self):
- if self.need_calc:
- self.children = []
- self.alloc = self.__class__.set_type()
- self.unalloc = self.__class__.set_type()
-
- if self.is_allocated():
- self.alloc.append(self.range)
- else:
- for child in self.resource.children.all():
- c = self.__class__(child)
- if c.unallocated():
- self.children.append(c)
- self.alloc = self.alloc.union(c.alloc)
- total = self.__class__.set_type()
- total.append(self.range)
- self.unalloc = total.difference(self.alloc)
- self.need_calc=False
-
- def unallocated(self):
- self.calculate()
- return self.unalloc
-
- def as_ul(self):
- '''Returns a string of the tree as an unordered HTML list.'''
- s = []
- s.append('<a href="%s">%s</a>' % (self.resource.get_absolute_url(), self.resource))
-
- # when the unallocated range is a subset of the current range,
- # display the missing ranges
- u = self.unallocated()
- if len(u) != 1 or self.range != u[0]:
- s.append(' (missing: ')
- s.append(', '.join(str(x) for x in u))
- s.append(')')
-
- # quick access links
- if self.resource.parent:
- s.append(' | <a href="%s/delete">delete</a>' % (self.resource.get_absolute_url(),))
- s.append(' | <a href="%s/allocate">give</a>' % (self.resource.get_absolute_url(),))
- if self.range.min != self.range.max:
- s.append(' | <a href="%s/split">split</a>' % (self.resource.get_absolute_url(),))
- # add type-specific actions
- a = self.supported_actions()
- if a:
- s.extend(a)
-
- if self.children:
- s.append('\n<ul>\n')
- for c in self.children:
- s.append('<li>' + c.as_ul())
- s.append('\n</ul>')
-
- return ''.join(s)
-
- def supported_actions(self):
- '''Virtual method allowing subclasses to add actions to the HTML list.'''
- return None
-
- @classmethod
- def from_resource_range(cls, resource):
- if isinstance(resource, resource_set.resource_range_as):
- return AllocationTreeAS(resource)
- if isinstance(resource, resoute_set.resource_range_ip):
- return AllocationTreeIP(resource)
- raise ValueError, 'Unsupported resource range type'
-
-class AllocationTreeAS(AllocationTree):
- set_type = resource_set.resource_set_as
-
- def __init__(self, *args, **kwargs):
- AllocationTree.__init__(self, *args, **kwargs)
- self.conf = misc.top_parent(self.resource).from_cert.all()[0].parent.conf
-
- def is_allocated(self):
- '''Returns true if this AS has been allocated to a child or
- used in a ROA request.'''
- # FIXME: detect use in ROA requests
-
- if self.resource.allocated:
- return True
-
- # for individual ASNs
- if self.range.min == self.range.max:
- # is this ASN used in any roa?
- if self.conf.roas.filter(asn=self.range.min):
- return True
-
- return False
-
-class AllocationTreeIP(AllocationTree):
- '''virtual class representing a tree of IP address ranges.'''
-
- @classmethod
- def from_prefix(cls, prefix):
- r = prefix.as_resource_range()
- if isinstance(r, resource_set.resource_range_ipv4):
- return AllocationTreeIPv4(prefix)
- elif isinstance(r, resource_set.resource_range_ipv6):
- return AllocationTreeIPv6(prefix)
- raise ValueError, 'Unsupported IP range type'
-
- def supported_actions(self):
- '''add a link to issue a ROA for this IP range'''
- if self.resource.is_prefix():
- return [' | <a href="%s/roa">roa</a>' % self.resource.get_absolute_url()]
- else:
- return []
-
- def is_allocated(self):
- '''Return True if this IP range is allocated to a child or used
- in a ROA request.'''
- return self.resource.allocated or self.resource.roa_requests.count()
-
-class AllocationTreeIPv4(AllocationTreeIP):
- set_type = resource_set.resource_set_ipv4
-
-class AllocationTreeIPv6(AllocationTreeIP):
- set_type = resource_set.resource_set_ipv6
-
-# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/app/admin.py b/rpkid/rpki/gui/app/admin.py
deleted file mode 100644
index 52dc2c87..00000000
--- a/rpkid/rpki/gui/app/admin.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""
-$Id$
-
-Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
-
-from django import forms
-from django.contrib import admin
-from rpki.gui.app import models
-
-class ConfAdmin( admin.ModelAdmin ):
- pass
-
-class ChildAdmin( admin.ModelAdmin ):
- pass
-
-class AddressRangeAdmin( admin.ModelAdmin ):
- #list_display = ('__unicode__', 'lo', 'hi')
- pass
-
-class AsnAdmin( admin.ModelAdmin ):
- #list_display = ('__unicode__',)
- pass
-
-class ParentAdmin( admin.ModelAdmin ):
- pass
-
-class RoaAdmin( admin.ModelAdmin ):
- pass
-
-class ResourceCertAdmin(admin.ModelAdmin):
- pass
-
-class RoaRequestAdmin(admin.ModelAdmin):
- pass
-
-class GhostbusterAdmin(admin.ModelAdmin):
- pass
-
-admin.site.register(models.AddressRange, AddressRangeAdmin)
-admin.site.register(models.Child, ChildAdmin)
-admin.site.register(models.Conf, ConfAdmin)
-admin.site.register(models.Asn, AsnAdmin)
-admin.site.register(models.Ghostbuster, GhostbusterAdmin)
-admin.site.register(models.Parent, ParentAdmin)
-admin.site.register(models.ResourceCert, ResourceCertAdmin)
-admin.site.register(models.Roa, RoaAdmin)
-admin.site.register(models.RoaRequest, RoaRequestAdmin)
-
-# vim:sw=4 ts=8
diff --git a/rpkid/rpki/gui/app/asnset.py b/rpkid/rpki/gui/app/asnset.py
deleted file mode 100644
index beb3a8dc..00000000
--- a/rpkid/rpki/gui/app/asnset.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# $Id$
-"""
-Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
-
-class asnset(object):
- """A set-like object for containing sets of ASN values."""
- v = set()
-
- def __init__(self, init=None):
- """
- May be initialized from a comma separated list of positive integers.
- """
- if init:
- self.v = set(int(x) for x in init.split(',') if x.strip() != '')
- if [x for x in self.v if x <= 0]:
- raise ValueError, 'must be a positive integer'
-
- def __str__(self):
- return ','.join(str(x) for x in sorted(self.v))
-
- def __iter__(self):
- return iter(self.v)
-
- def add(self, n):
- assert isinstance(n, int)
- assert n > 0
- self.v.add(n)
diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py
index aad9185d..fb48fb08 100644
--- a/rpkid/rpki/gui/app/forms.py
+++ b/rpkid/rpki/gui/app/forms.py
@@ -1,26 +1,29 @@
-# $Id$
-"""
-Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
-
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+
+from django.contrib.auth.models import User
from django import forms
+from rpki.resource_set import (resource_range_as, resource_range_ipv4,
+ resource_range_ipv6)
+from rpki.gui.app import models
+from rpki.exceptions import BadIPResource
+from rpki.gui.app.glue import str_to_resource_range
-import rpki.ipaddrs
-
-from rpki.gui.app import models, misc
-from rpki.gui.app.asnset import asnset
class AddConfForm(forms.Form):
handle = forms.CharField(required=True,
@@ -44,212 +47,296 @@ class AddConfForm(forms.Form):
label='Pubd contact',
help_text='email address for the operator of your pubd instance')
-class ImportForm(forms.Form):
- '''Form used for uploading parent/child identity xml files'''
- handle = forms.CharField(max_length=30, help_text='your name for this entity')
- xml = forms.FileField(help_text='xml filename')
-def PrefixSplitForm(parent, *args, **kwargs):
- class _wrapper(forms.Form):
- prefix = forms.CharField(max_length=200, help_text='CIDR or range')
+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')
- def clean(self):
- p = self.cleaned_data.get('prefix')
- try:
- r = misc.parse_resource_range(p)
- except ValueError, err:
- print err
- raise forms.ValidationError, 'invalid prefix or range'
- # we get AssertionError is the range is misordered (hi before lo)
- except AssertionError, err:
- print err
- raise forms.ValidationError, 'invalid prefix or range'
- pr = parent.as_resource_range()
- if r.min < pr.min or r.max > pr.max:
- raise forms.ValidationError, \
- 'range is outside parent range'
- if r.min == pr.min and r.max == pr.max:
- raise forms.ValidationError, \
- 'range is equal to parent'
- if parent.allocated:
- raise forms.ValidationError, 'prefix is assigned to child'
- for p in parent.children.all():
- c = p.as_resource_range()
- if c.min <= r.min <= c.max or c.min <= r.max <= c.max:
- raise forms.ValidationError, \
- 'overlap with another child prefix: %s' % (c,)
-
- return self.cleaned_data
- return _wrapper(*args, **kwargs)
-
-def PrefixAllocateForm(iv, child_set, *args, **kwargs):
- class _wrapper(forms.Form):
- child = forms.ModelChoiceField(initial=iv, queryset=child_set,
- required=False, empty_label='(Unallocated)')
- return _wrapper(*args, **kwargs)
-
-def PrefixRoaForm(prefix, *args, **kwargs):
- prefix_range = prefix.as_resource_range()
-
- class _wrapper(forms.Form):
- asns = forms.CharField(max_length=200, required=False,
- help_text='Comma-separated list of ASNs')
- max_length = forms.IntegerField(min_value=prefix_range.prefixlen(),
- max_value=prefix_range.datum_type.bits,
- initial=prefix_range.prefixlen(),
- help_text='must be in range %d-%d' % (prefix_range.prefixlen(), prefix_range.datum_type.bits))
+ # override full_name. it is required in the db schema, but we allow the
+ # user to skip it and default from family+given name
+ full_name = forms.CharField(max_length=40, required=False,
+ help_text='automatically generated from family and given names if left blank')
- def clean_asns(self):
+ def __init__(self, issuer, *args, **kwargs):
+ super(GhostbusterRequestForm, self).__init__(*args, **kwargs)
+ self.fields['parent'].queryset = models.Parent.objects.filter(issuer=issuer)
+
+ class Meta:
+ model = models.GhostbusterRequest
+ exclude = ('issuer', 'vcard')
+
+ def clean(self):
+ family_name = self.cleaned_data.get('family_name')
+ given_name = self.cleaned_data.get('given_name')
+ if not all([family_name, given_name]):
+ raise forms.ValidationError, 'Family and Given names must be specified'
+
+ email = self.cleaned_data.get('email_address')
+ postal = self.cleaned_data.get('postal_address')
+ telephone = self.cleaned_data.get('telephone')
+ if not any([email, postal, telephone]):
+ raise forms.ValidationError, 'One of telephone, email or postal address must be specified'
+
+ # if the full name is not specified, default to given+family
+ fn = self.cleaned_data.get('full_name')
+ if not fn:
+ self.cleaned_data['full_name'] = '%s %s' % (given_name, family_name)
+
+ return self.cleaned_data
+
+
+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')
+ xml = forms.FileField(label='XML file',
+ widget=forms.FileInput(attrs={'class': 'input-file'}))
+
+
+class ImportRepositoryForm(forms.Form):
+ handle = forms.CharField(max_length=30, required=False,
+ label='Parent Handle',
+ help_text='Optional. Must be specified if you use a different name for this parent')
+ xml = forms.FileField(label='XML file',
+ widget=forms.FileInput(attrs={'class': 'input-file'}))
+
+
+class ImportClientForm(forms.Form):
+ """Form used for importing publication client requests."""
+ xml = forms.FileField(label='XML file',
+ widget=forms.FileInput(attrs={'class': 'input-file'}))
+
+
+class UserCreateForm(forms.Form):
+ handle = forms.CharField(max_length=30, help_text='handle for new child')
+ email = forms.CharField(max_length=30,
+ help_text='email address for new user')
+ password = forms.CharField(widget=forms.PasswordInput)
+ password2 = forms.CharField(widget=forms.PasswordInput,
+ label='Confirm Password')
+ parent = forms.ModelChoiceField(required=False,
+ queryset=models.Conf.objects.all(),
+ help_text='optionally make a child of')
+
+ def clean_handle(self):
+ handle = self.cleaned_data.get('handle')
+ if (handle and models.Conf.objects.filter(handle=handle).exists() or
+ User.objects.filter(username=handle).exists()):
+ raise forms.ValidationError('user already exists')
+ return handle
+
+ def clean(self):
+ p1 = self.cleaned_data.get('password')
+ p2 = self.cleaned_data.get('password2')
+ if p1 != p2:
+ raise forms.ValidationError('passwords do not match')
+ handle = self.cleaned_data.get('handle')
+ parent = self.cleaned_data.get('parent')
+ if handle and parent and parent.children.filter(handle=handle).exists():
+ raise forms.ValidationError('parent already has a child by that name')
+ return self.cleaned_data
+
+
+class UserEditForm(forms.Form):
+ """Form for editing a user."""
+ email = forms.CharField()
+ pw = forms.CharField(widget=forms.PasswordInput, label='Password',
+ required=False)
+ pw2 = forms.CharField(widget=forms.PasswordInput, label='Confirm password',
+ required=False)
+
+ def clean(self):
+ p1 = self.cleaned_data.get('pw')
+ p2 = self.cleaned_data.get('pw2')
+ if p1 != p2:
+ raise forms.ValidationError('Passwords do not match')
+ return self.cleaned_data
+
+
+class ROARequest(forms.Form):
+ """Form for entering a ROA request.
+
+ Handles both IPv4 and IPv6."""
+
+ asn = forms.IntegerField(label='AS')
+ prefix = forms.CharField(max_length=50)
+ max_prefixlen = forms.CharField(required=False,
+ label='Max Prefix Length')
+ confirmed = forms.BooleanField(widget=forms.HiddenInput, required=False)
+
+ def __init__(self, *args, **kwargs):
+ """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)
+ super(ROARequest, self).__init__(*args, **kwargs)
+ self.conf = conf
+
+ def _as_resource_range(self):
+ """Convert the prefix in the form to a
+ rpki.resource_set.resource_range_ip object.
+
+ """
+ prefix = self.cleaned_data.get('prefix')
+ return str_to_resource_range(prefix)
+
+ def clean_asn(self):
+ value = self.cleaned_data.get('asn')
+ if value < 0:
+ raise forms.ValidationError('AS must be a positive value or 0')
+ return value
+
+ def clean_prefix(self):
+ try:
+ r = self._as_resource_range()
+ except:
+ raise forms.ValidationError('invalid IP address')
+
+ manager = models.ResourceRangeAddressV4 if isinstance(r, resource_range_ipv4) else models.ResourceRangeAddressV6
+ if not manager.objects.filter(cert__parent__issuer=self.conf,
+ prefix_min__lte=r.min,
+ prefix_max__gte=r.max).exists():
+ raise forms.ValidationError('prefix is not allocated to you')
+ return str(r)
+
+ def clean_max_prefixlen(self):
+ v = self.cleaned_data.get('max_prefixlen')
+ if v:
+ if v[0] == '/':
+ v = v[1:] # allow user to specify /24
try:
- v = asnset(self.cleaned_data.get('asns'))
- return ','.join(str(x) for x in sorted(v))
+ if int(v) < 0:
+ raise forms.ValidationError('max prefix length must be positive or 0')
except ValueError:
+ raise forms.ValidationError('invalid integer value')
+ return v
+
+ def clean(self):
+ if 'prefix' in self.cleaned_data:
+ r = self._as_resource_range()
+ max_prefixlen = self.cleaned_data.get('max_prefixlen')
+ max_prefixlen = int(max_prefixlen) if max_prefixlen else r.prefixlen()
+ if max_prefixlen < r.prefixlen():
+ raise forms.ValidationError('max prefix length must be greater than or equal to the prefix length')
+ if max_prefixlen > r.datum_type.bits:
raise forms.ValidationError, \
- 'Must be a list of integers separated by commas.'
- return self.cleaned_data['asns']
+ 'max prefix length (%d) is out of range for IP version (%d)' % (max_prefixlen, r.datum_type.bits)
+ self.cleaned_data['max_prefixlen'] = str(max_prefixlen)
- def clean(self):
- if not prefix.is_prefix():
- raise forms.ValidationError, \
- '%s can not be represented as a prefix.' % (prefix,)
- if prefix.allocated:
- raise forms.ValidationError, \
- 'Prefix is allocated to a child.'
- return self.cleaned_data
+ return self.cleaned_data
- return _wrapper(*args, **kwargs)
-def PrefixDeleteForm(prefix, *args, **kwargs):
- class _wrapped(forms.Form):
+class ROARequestConfirm(forms.Form):
+ asn = forms.IntegerField(widget=forms.HiddenInput)
+ prefix = forms.CharField(widget=forms.HiddenInput)
+ max_prefixlen = forms.IntegerField(widget=forms.HiddenInput)
- def clean(self):
- if not prefix.parent:
- raise forms.ValidationError, \
- 'Can not delete prefix received from parent'
- if prefix.allocated:
- raise forms.ValidationError, 'Prefix is allocated to child'
- if prefix.roa_requests.all():
- raise forms.ValidationError, 'Prefix is used in your ROAs'
- if prefix.children.all():
- raise forms.ValidationError, 'Prefix has been split'
- return self.cleaned_data
-
- return _wrapped(*args, **kwargs)
-
-def GhostbusterForm(parent_qs, conf=None):
- """
- Generate a ModelForm with the subset of parents for the current
- resource handle.
+ def clean_asn(self):
+ value = self.cleaned_data.get('asn')
+ if value < 0:
+ raise forms.ValidationError('AS must be a positive value or 0')
+ return value
- The 'conf' argument is required when creating a new object, in
- order to specify the value of the 'conf' field in the new
- Ghostbuster object.
- """
- class wrapped(forms.ModelForm):
- # override parent
- parent = forms.ModelMultipleChoiceField(queryset=parent_qs, required=False,
- help_text='use this record for a specific parent, or leave blank for all parents')
- # override full_name. it is required in the db schema, but we allow the
- # user to skip it and default from family+given name
- full_name = forms.CharField(max_length=40, required=False,
- help_text='automatically generated from family and given names if left blank')
-
- class Meta:
- model = models.Ghostbuster
- exclude = [ 'conf' ]
-
- def clean(self):
- family_name = self.cleaned_data.get('family_name')
- given_name = self.cleaned_data.get('given_name')
- if not all([family_name, given_name]):
- raise forms.ValidationError, 'Family and Given names must be specified'
-
- email = self.cleaned_data.get('email_address')
- postal = self.cleaned_data.get('postal_address')
- telephone = self.cleaned_data.get('telephone')
- if not any([email, postal, telephone]):
- raise forms.ValidationError, 'One of telephone, email or postal address must be specified'
-
- # if the full name is not specified, default to given+family
- fn = self.cleaned_data.get('full_name')
- if not fn:
- self.cleaned_data['full_name'] = '%s %s' % (given_name, family_name)
-
- return self.cleaned_data
-
- def save(self, *args, **kwargs):
- if conf:
- # the generic create_object view doesn't allow us to set
- # the conf field, so wrap the save() method and set it
- # here
- kwargs['commit'] = False
- obj = super(wrapped, self).save(*args, **kwargs)
- obj.conf = conf
- obj.save()
- return obj
- else:
- return super(wrapped, self).save(*args, **kwargs)
-
- return wrapped
-
-class ChildForm(forms.ModelForm):
+ def clean_prefix(self):
+ try:
+ r = str_to_resource_range(self.cleaned_data.get('prefix'))
+ except BadIPResource:
+ raise forms.ValidationError('invalid prefix')
+ return str(r)
+
+ def clean(self):
+ try:
+ r =str_to_resource_range(self.cleaned_data.get('prefix'))
+ if r.prefixlen() > self.cleaned_data.get('max_prefixlen'):
+ raise forms.ValidationError('max length is smaller than mask')
+ except BadIPResource:
+ pass
+ return self.cleaned_data
+
+
+def AddASNForm(qs):
"""
- Subclass for editing rpki.gui.app.models.Child objects.
+ Generate a form class which only allows specification of ASNs contained
+ within the specified queryset. `qs` should be a QuerySet of
+ irdb.models.ChildASN.
+
"""
- class Meta:
- model = models.Child
- exclude = [ 'conf', 'handle' ]
+ class _wrapped(forms.Form):
+ asns = forms.CharField(label='ASNs', help_text='single ASN or range')
-def ImportChildForm(parent_conf, *args, **kwargs):
- class wrapped(forms.Form):
- handle = forms.CharField(max_length=30, help_text="Child's RPKI handle")
- xml = forms.FileField(help_text="Child's identity.xml file")
+ def clean_asns(self):
+ try:
+ r = resource_range_as.parse_str(self.cleaned_data.get('asns'))
+ except:
+ raise forms.ValidationError('invalid AS or range')
+ if not qs.filter(min__lte=r.min, max__gte=r.max).exists():
+ raise forms.ValidationError('AS or range is not delegated to you')
+ return str(r)
- def clean_handle(self):
- if parent_conf.children.filter(handle=self.cleaned_data['handle']):
- raise forms.ValidationError, "a child with that handle already exists"
- return self.cleaned_data['handle']
+ return _wrapped
- return wrapped(*args, **kwargs)
-def ImportParentForm(conf, *args, **kwargs):
- class wrapped(forms.Form):
- handle = forms.CharField(max_length=30, help_text="Parent's RPKI handle")
- xml = forms.FileField(help_text="XML response from parent", required=False)
+def AddNetForm(qsv4, qsv6):
+ """
+ Generate a form class which only allows specification of prefixes contained
+ within the specified queryset. `qs` should be a QuerySet of
+ irdb.models.ChildNet.
- def clean_handle(self):
- if conf.parents.filter(handle=self.cleaned_data['handle']):
- raise forms.ValidationError, "a parent with that handle already exists"
- return self.cleaned_data['handle']
+ """
- return wrapped(*args, **kwargs)
+ class _wrapped(forms.Form):
+ address_range = forms.CharField(help_text='CIDR or range')
-class ImportRepositoryForm(forms.Form):
- parent_handle = forms.CharField(max_length=30, required=False, help_text='(optional)')
- xml = forms.FileField(help_text='xml file from repository operator')
+ def clean_address_range(self):
+ address_range = self.cleaned_data.get('address_range')
+ try:
+ r = resource_range_ipv4.parse_str(address_range)
+ if not qsv4.filter(prefix_min__lte=r.min, prefix_max__gte=r.max).exists():
+ raise forms.ValidationError('IP address range is not delegated to you')
+ except BadIPResource:
+ try:
+ r = resource_range_ipv6.parse_str(address_range)
+ if not qsv6.filter(prefix_min__lte=r.min, prefix_max__gte=r.max).exists():
+ raise forms.ValidationError('IP address range is not delegated to you')
+ except BadIPResource:
+ raise forms.ValidationError('invalid IP address range')
+ return str(r)
+
+ return _wrapped
+
+
+def ChildForm(instance):
+ """
+ Form for editing a Child model.
-class ImportPubClientForm(forms.Form):
- xml = forms.FileField(help_text='xml file from publication client')
+ 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.
-def ChildWizardForm(parent, *args, **kwargs):
- class wrapped(forms.Form):
- handle = forms.CharField(max_length=30, help_text='handle for new child')
- #create_user = forms.BooleanField(help_text='create a new user account for this handle?')
- #password = forms.CharField(widget=forms.PasswordInput, help_text='password for new user', required=False)
- #password2 = forms.CharField(widget=forms.PasswordInput, help_text='repeat password', required=False)
+ """
- def clean_handle(self):
- if parent.children.filter(handle=self.cleaned_data['handle']):
- raise forms.ValidationError, 'a child with that handle already exists'
- return self.cleaned_data['handle']
+ class _wrapped(forms.Form):
+ valid_until = forms.DateTimeField(initial=instance.valid_until)
+ as_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildASN.objects.filter(child=instance),
+ required=False,
+ label='AS Ranges',
+ help_text='deselect to remove delegation')
+ address_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildNet.objects.filter(child=instance),
+ required=False,
+ help_text='deselect to remove delegation')
- return wrapped(*args, **kwargs)
+ return _wrapped
-class GenericConfirmationForm(forms.Form):
- """
- stub form used for doing confirmations.
- """
- pass
-# vim:sw=4 ts=8 expandtab
+class UserDeleteForm(forms.Form):
+ """Stub form for deleting users."""
+ pass
diff --git a/rpkid/rpki/gui/app/glue.py b/rpkid/rpki/gui/app/glue.py
index 687af268..7de1a9e5 100644
--- a/rpkid/rpki/gui/app/glue.py
+++ b/rpkid/rpki/gui/app/glue.py
@@ -1,125 +1,48 @@
-# $Id$
-"""
-Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
+"""
+This file contains code that interfaces between the django views implementing
+the portal gui and the rpki.* modules.
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
"""
from __future__ import with_statement
-import os, os.path, csv, shutil, stat, sys
-from datetime import datetime, timedelta
+__version__ = '$Id$'
-from django.db.models import F
+from datetime import datetime
-import rpki, rpki.async, rpki.http, rpki.x509, rpki.left_right, rpki.myrpki
-import rpki.publication
+from rpki.resource_set import (resource_set_as, resource_set_ipv4,
+ resource_set_ipv6, resource_range_ipv4,
+ resource_range_ipv6)
+from rpki.left_right import list_received_resources_elt
+from rpki.irdb.zookeeper import Zookeeper
from rpki.gui.app import models, settings
-def confpath(*handle):
- """
- Return the absolute pathname to the configuration directory for
- the given resource handle. If additional arguments are given, they
- are taken to mean files/subdirectories.
- """
- argv = [ settings.CONFDIR ]
- argv.extend(handle)
- return os.path.join(*argv)
-
-def read_file_from_handle(handle, fname):
- """read a filename relative to the directory for the given resource handle. returns
- a tuple of (content, mtime)"""
- with open(confpath(handle, fname), 'r') as fp:
- data = fp.read()
- mtime = os.fstat(fp.fileno())[stat.ST_MTIME]
- return data, mtime
-
-read_identity = lambda h: read_file_from_handle(h, 'entitydb/identity.xml')[0]
-
-def output_asns(path, handle):
- '''Write out csv file containing asns delegated to my children.'''
- qs = models.Asn.objects.filter(lo=F('hi'), allocated__in=handle.children.all())
- w = rpki.myrpki.csv_writer(path)
- w.writerows([asn.allocated.handle, asn.lo] for asn in qs)
- w.close()
-
-def output_prefixes(path, handle):
- '''Write out csv file containing prefixes delegated to my children.'''
- qs = models.AddressRange.objects.filter(allocated__in=handle.children.all())
- w = rpki.myrpki.csv_writer(path)
- w.writerows([p.allocated.handle, p.as_resource_range()] for p in qs)
- w.close()
-
-def output_roas(path, handle):
- '''Write out csv file containing my roas.'''
- qs = models.RoaRequest.objects.filter(roa__in=handle.roas.all())
- w = rpki.myrpki.csv_writer(path)
- w.writerows([req.as_roa_prefix(), req.roa.asn,
- '%s-group-%d' % (handle.handle, req.roa.pk)] for req in qs)
- w.close()
-
-def qualify_path(pfx, fname):
- """Ensure 'path' is an absolute filename."""
- return fname if fname.startswith('/') else os.path.join(pfx, fname)
-
-def build_rpkid_caller(cfg, verbose=False):
- """
- Returns a function suitable for calling rpkid using the
- configuration information specified in the rpki.config.parser
- object.
- """
- bpki_servers_dir = cfg.get("bpki_servers_directory")
- if not bpki_servers_dir.startswith('/'):
- bpki_servers_dir = confpath(cfg.get('handle'), bpki_servers_dir)
-
- bpki_servers = rpki.myrpki.CA(cfg.filename, bpki_servers_dir)
- rpkid_base = "http://%s:%s/" % (cfg.get("rpkid_server_host"), cfg.get("rpkid_server_port"))
-
- return rpki.async.sync_wrapper(rpki.http.caller(
- proto = rpki.left_right,
- client_key = rpki.x509.RSA(PEM_file = bpki_servers.dir + "/irbe.key"),
- client_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/irbe.cer"),
- server_ta = rpki.x509.X509(PEM_file = bpki_servers.cer),
- server_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/rpkid.cer"),
- url = rpkid_base + "left-right",
- debug = verbose))
-
-def build_pubd_caller(cfg):
- bpki_servers_dir = cfg.get("bpki_servers_directory")
- if not bpki_servers_dir.startswith('/'):
- bpki_servers_dir = confpath(cfg.get('handle'), bpki_servers_dir)
-
- bpki_servers = rpki.myrpki.CA(cfg.filename, bpki_servers_dir)
- pubd_base = "http://%s:%s/" % (cfg.get("pubd_server_host"), cfg.get("pubd_server_port"))
-
- return rpki.async.sync_wrapper(rpki.http.caller(
- proto = rpki.publication,
- client_key = rpki.x509.RSA( PEM_file = bpki_servers.dir + "/irbe.key"),
- client_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/irbe.cer"),
- server_ta = rpki.x509.X509(PEM_file = bpki_servers.cer),
- server_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/pubd.cer"),
- url = pubd_base + "control"))
def ghostbuster_to_vcard(gbr):
- """
- Convert a Ghostbuster object into a vCard object.
- """
+ """Convert a GhostbusterRequest object into a vCard object."""
import vobject
vcard = vobject.vCard()
- vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name, given=gbr.given_name)
+ vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name,
+ given=gbr.given_name)
- adr_fields = [ 'box', 'extended', 'street', 'city', 'region', 'code', 'country' ]
+ adr_fields = ['box', 'extended', 'street', 'city', 'region', 'code',
+ 'country']
adr_dict = dict((f, getattr(gbr, f, '')) for f in adr_fields)
if any(adr_dict.itervalues()):
vcard.add('ADR').value = vobject.vcard.Address(**adr_dict)
@@ -128,185 +51,64 @@ def ghostbuster_to_vcard(gbr):
# the ORG type is a sequence of organization unit names, so
# transform the org name into a tuple before stuffing into the
# vCard object
- attrs = [ ('FN', 'full_name', None),
- ('TEL', 'telephone', None),
- ('ORG', 'organization', lambda x: (x,)),
- ('EMAIL', 'email_address', None) ]
+ attrs = [('FN', 'full_name', None),
+ ('TEL', 'telephone', None),
+ ('ORG', 'organization', lambda x: (x,)),
+ ('EMAIL', 'email_address', None)]
for vtype, field, transform in attrs:
v = getattr(gbr, field)
if v:
vcard.add(vtype).value = transform(v) if transform else v
return vcard.serialize()
-def qualify_path(pfx, fname):
- """
- Ensure 'path' is an absolute filename.
- """
- return fname if fname.startswith('/') else os.path.join(pfx, fname)
-
-def configure_resources(log, handle):
- """
- This function should be called when resources for this resource
- holder have changed. It updates IRDB and notifies rpkid to
- immediately process the changes, rather than waiting for the cron
- job to run.
- For backwards compatability (and backups), it also writes the csv
- files for use with the myrpki.py command line script.
+def list_received_resources(log, conf):
"""
+ Query rpkid for this resource handle's received resources.
- path = confpath(handle.handle)
- cfg = rpki.config.parser(os.path.join(path, 'rpki.conf'), 'myrpki')
-
- output_asns(qualify_path(path, cfg.get('asn_csv')), handle)
- output_prefixes(qualify_path(path, cfg.get('prefix_csv')), handle)
- output_roas(qualify_path(path, cfg.get('roa_csv')), handle)
-
- roa_requests = []
- for roa in handle.roas.all():
- v4 = rpki.resource_set.roa_prefix_set_ipv4()
- v6 = rpki.resource_set.roa_prefix_set_ipv6()
- for req in roa.from_roa_request.all():
- pfx = req.as_roa_prefix()
- if isinstance(pfx, rpki.resource_set.roa_prefix_ipv4):
- v4.append(pfx)
- else:
- v6.append(pfx)
- roa_requests.append((roa.asn, v4, v6))
-
- children = []
- for child in handle.children.all():
- asns = rpki.resource_set.resource_set_as([a.as_resource_range() for a in child.asn.all()])
-
- v4 = rpki.resource_set.resource_set_ipv4()
- v6 = rpki.resource_set.resource_set_ipv6()
- for pfx in child.address_range.all():
- rng = pfx.as_resource_range()
- if isinstance(rng, rpki.resource_set.resource_range_ipv4):
- v4.append(rng)
- else:
- v6.append(rng)
-
- # convert from datetime.datetime to rpki.sundial.datetime
- valid_until = rpki.sundial.datetime.fromdatetime(child.valid_until)
- children.append((child.handle, asns, v4, v6, valid_until))
-
- ghostbusters = []
- for gbr in handle.ghostbusters.all():
- vcard = ghostbuster_to_vcard(gbr)
- parent_set = gbr.parent.all()
- if parent_set:
- for p in parent_set:
- ghostbusters.append((p, vcard))
- else:
- ghostbusters.append((None, vcard))
+ 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.
- # for hosted handles, get the config for the irdbd/rpkid host
- if handle.host:
- cfg = rpki.config.parser(confpath(handle.host.handle, 'rpki.conf'), 'myrpki')
-
- irdb = rpki.myrpki.IRDB(cfg)
- irdb.update(handle, roa_requests, children, ghostbusters)
- irdb.close()
+ """
- # contact rpkid to request immediate update
- call_rpkid = build_rpkid_caller(cfg)
- call_rpkid(rpki.left_right.self_elt.make_pdu(action='set', self_handle=handle.handle, run_now=True))
+ z = Zookeeper(handle=conf.handle)
+ pdus = z.call_rpkid(list_received_resources_elt.make_pdu(self_handle=conf.handle))
-def list_received_resources(log, conf):
- "Query rpkid for this resource handle's children and received resources."
-
- # if this handle is hosted, get the cfg for the host
- rpki_conf = conf.host if conf.host else conf
- cfg = rpki.config.parser(confpath(rpki_conf.handle, 'rpki.conf'), 'myrpki')
- call_rpkid = build_rpkid_caller(cfg)
- pdus = call_rpkid(rpki.left_right.list_received_resources_elt.make_pdu(self_handle=conf.handle),
- rpki.left_right.child_elt.make_pdu(action="list", self_handle=conf.handle),
- rpki.left_right.parent_elt.make_pdu(action="list", self_handle=conf.handle))
+ models.ResourceCert.objects.filter(parent__issuer=conf).delete()
for pdu in pdus:
- if isinstance(pdu, rpki.left_right.child_elt):
- # have we seen this child before?
- child_set = conf.children.filter(handle=pdu.child_handle)
- if not child_set:
- # default to 1 year. no easy way to query irdb for the
- # current value.
- valid_until = datetime.now() + timedelta(days=365)
- child = models.Child(conf=conf, handle=pdu.child_handle,
- valid_until=valid_until)
- child.save()
-
- elif isinstance(pdu, rpki.left_right.parent_elt):
- # have we seen this parent before?
- parent_set = conf.parents.filter(handle=pdu.parent_handle)
- if not parent_set:
- parent = models.Parent(conf=conf, handle=pdu.parent_handle)
- parent.save()
-
- elif isinstance(pdu, rpki.left_right.list_received_resources_elt):
-
- # have we seen this parent before?
- parent_set = conf.parents.filter(handle=pdu.parent_handle)
- if not parent_set:
- parent = models.Parent(conf=conf, handle=pdu.parent_handle)
- parent.save()
- else:
- parent = parent_set[0]
+ if isinstance(pdu, list_received_resources_elt):
+ parent = models.Parent.objects.get(issuer=conf,
+ handle=pdu.parent_handle)
not_before = datetime.strptime(pdu.notBefore, "%Y-%m-%dT%H:%M:%SZ")
not_after = datetime.strptime(pdu.notAfter, "%Y-%m-%dT%H:%M:%SZ")
- #print >>log, 'uri: %s, not before: %s, not after: %s' % (pdu.uri, not_before, not_after)
+ cert = models.ResourceCert.objects.create(parent=parent,
+ not_before=not_before, not_after=not_after,
+ uri=pdu.uri)
- # have we seen this resource cert before?
- cert_set = parent.resources.filter(uri=pdu.uri)
- if cert_set.count() == 0:
- cert = models.ResourceCert(uri=pdu.uri, parent=parent,
- not_before=not_before, not_after=not_after)
- else:
- cert = cert_set[0]
- # update timestamps since it could have been modified
- cert.not_before = not_before
- cert.not_after = not_after
- cert.save()
+ for asn in resource_set_as(pdu.asn):
+ cert.asn_ranges.create(min=asn.min, max=asn.max)
- for asn in rpki.resource_set.resource_set_as(pdu.asn):
- # see if this resource is already part of the cert
- if cert.asn.filter(lo=asn.min, hi=asn.max).count() == 0:
- # ensure this range wasn't seen from another of our parents
- for v in models.Asn.objects.filter(lo=asn.min, hi=asn.max):
- # determine if resource is delegated from another parent
- if v.from_cert.filter(parent__in=conf.parents.all()).count():
- cert.asn.add(v)
- break
- else:
- cert.asn.create(lo=asn.min, hi=asn.max)
- cert.save()
+ for rng in resource_set_ipv4(pdu.ipv4):
+ print >>log, 'adding v4 address range: %s' % rng
+ cert.address_ranges.create(prefix_min=rng.min,
+ prefix_max=rng.max)
- # IPv4/6 - not separated in the django db
- def add_missing_address(addr_set):
- for ip in addr_set:
- lo=str(ip.min)
- hi=str(ip.max)
- if cert.address_range.filter(lo=lo, hi=hi).count() == 0:
- # ensure that this range wasn't previously seen from another of our parents
- for v in models.AddressRange.objects.filter(lo=lo, hi=hi):
- # determine if this resource is delegated from another parent as well
- if v.from_cert.filter(parent__in=conf.parents.all()).count():
- cert.address_range.add(v)
- break
- else:
- cert.address_range.create(lo=lo, hi=hi)
- cert.save()
+ for rng in resource_set_ipv6(pdu.ipv6):
+ cert.address_ranges_v6.create(prefix_min=rng.min,
+ prefix_max=rng.max)
+ else:
+ print >>log, "error: unexpected pdu from rpkid type=%s" % type(pdu)
- add_missing_address(rpki.resource_set.resource_set_ipv4(pdu.ipv4))
- add_missing_address(rpki.resource_set.resource_set_ipv6(pdu.ipv6))
def config_from_template(dest, a):
"""
- Create a new rpki.conf file from a generic template. Go line by
- line through the template and substitute directives from the
- dictionary 'a'.
+ Create a new rpki.conf file from a generic template. Go line by line
+ through the template and substitute directives from the dictionary 'a'.
+
"""
with open(dest, 'w') as f:
for r in open(settings.RPKI_CONF_TEMPLATE):
@@ -320,181 +122,9 @@ def config_from_template(dest, a):
else:
print >>f, r,
-class Myrpki(rpki.myrpki.main):
- """
- wrapper around rpki.myrpki.main to force the config file to what i want,
- and avoid cli arg parsing.
- """
- def __init__(self, handle):
- self.cfg_file = confpath(handle, 'rpki.conf')
- self.read_config()
-
-def configure_daemons(log, conf, m):
- if conf.host:
- m.configure_resources_main()
-
- host = Myrpki(conf.host.handle)
- host.do_configure_daemons(m.cfg.get('xml_filename'))
- else:
- m.do_configure_daemons('')
-
-def initialize_handle(log, handle, host, owner=None, commit=True):
- """
- Create a new Conf object for this user.
- """
- print >>log, "initializing new resource handle %s" % handle
-
- qs = models.Conf.objects.filter(handle=handle)
- if not qs:
- conf = models.Conf(handle=handle, host=host)
- conf.save()
- if owner:
- conf.owner.add(owner)
- else:
- conf = qs[0]
-
- # create the config directory if it doesn't already exist
- top = confpath(conf.handle)
- if not os.path.exists(top):
- os.makedirs(top)
-
- cfg_file = confpath(conf.handle, 'rpki.conf')
-
- # create rpki.conf file if it doesn't exist
- if not os.path.exists(cfg_file):
- print >>log, "generating rpki.conf for %s" % conf.handle
- config_from_template(cfg_file, { 'handle': conf.handle,
- 'configuration_directory': top, 'run_rpkid': 'false'})
-
- # create stub csv files
- for f in ('asns', 'prefixes', 'roas'):
- p = confpath(conf.handle, f + '.csv')
- if not os.path.exists(p):
- f = open(p, 'w')
- f.close()
-
- # load configuration for self
- m = Myrpki(conf.handle)
- m.do_initialize('')
-
- if commit:
- # run twice the first time to get bsc cert issued
- configure_daemons(log, conf, m)
- configure_daemons(log, conf, m)
-
- return conf, m
-
-def import_child(log, conf, child_handle, xml_file):
- """
- Import a child's identity.xml.
- """
- m = Myrpki(conf.handle)
- m.do_configure_child(xml_file)
- configure_daemons(log, conf, m)
-
-def import_parent(log, conf, parent_handle, xml_file):
- m = Myrpki(conf.handle)
- m.do_configure_parent(xml_file)
- configure_daemons(log, conf, m)
-
-def import_pubclient(log, conf, xml_file):
- m = Myrpki(conf.handle)
- m.do_configure_publication_client(xml_file)
- configure_daemons(log, conf, m)
-
-def import_repository(log, conf, xml_file):
- m = Myrpki(conf.handle)
- m.do_configure_repository(xml_file)
- configure_daemons(log, conf, m)
-
-def create_child(log, parent_conf, child_handle):
- """
- implements the child create wizard to create a new locally hosted child
- """
- child_conf, child = initialize_handle(log, handle=child_handle, host=parent_conf, commit=False)
-
- parent_handle = parent_conf.handle
- parent = Myrpki(parent_handle)
-
- child_identity_xml = os.path.join(child.cfg.get("entitydb_dir"), 'identity.xml')
- parent_response_xml = os.path.join(parent.cfg.get("entitydb_dir"), 'children', child_handle + '.xml')
- repo_req_xml = os.path.join(child.cfg.get('entitydb_dir'), 'repositories', parent_handle + '.xml')
- # XXX for now we assume the child is hosted by parent's pubd
- repo_resp_xml = os.path.join(parent.cfg.get('entitydb_dir'), 'pubclients', '%s.%s.xml' % (parent_handle, child_handle))
-
- parent.do_configure_child(child_identity_xml)
-
- child.do_configure_parent(parent_response_xml)
-
- parent.do_configure_publication_client(repo_req_xml)
-
- child.do_configure_repository(repo_resp_xml)
-
- # run twice the first time to get bsc cert issued
- sys.stdout = sys.stderr
- configure_daemons(log, child_conf, child)
- configure_daemons(log, child_conf, child)
-
-def destroy_handle(log, handle):
- conf = models.Conf.objects.get(handle=handle)
-
- cfg = rpki.config.parser(confpath(conf.host.handle, 'rpki.conf'), 'myrpki')
- call_rpkid = build_rpkid_caller(cfg)
- call_pubd = build_pubd_caller(cfg)
-
- # destroy the <self/> object and the <child/> object from the host/parent.
- rpkid_reply = call_rpkid(
- rpki.left_right.self_elt.make_pdu(action="destroy", self_handle=handle),
- rpki.left_right.child_elt.make_pdu(action="destroy", self_handle=conf.host.handle, child_handle=handle))
- if isinstance(rpkid_reply[0], rpki.left_right.report_error_elt):
- print >>log, "Error while calling pubd to delete client %s:" % handle
- print >>log, rpkid_reply[0]
-
- pubd_reply = call_pubd(rpki.publication.client_elt.make_pdu(action="destroy", client_handle=handle))
- if isinstance(pubd_reply[0], rpki.publication.report_error_elt):
- print >>log, "Error while calling pubd to delete client %s:" % handle
- print >>log, pubd_reply[0]
-
- conf.delete()
-
- shutil.remove(confpath(handle))
-
-def read_child_response(log, conf, child_handle):
- m = Myrpki(conf.handle)
- bname = child_handle + '.xml'
- return open(os.path.join(m.cfg.get('entitydb_dir'), 'children', bname)).read()
-
-def read_child_repo_response(log, conf, child_handle):
- """
- Return the XML file for the configure_publication_client response to the
- child.
-
- Note: the current model assumes the publication client is a child of this
- handle.
- """
-
- m = Myrpki(conf.handle)
- return open(os.path.join(m.cfg.get('entitydb_dir'), 'pubclients', '%s.%s.xml' % (conf.handle, child_handle))).read()
-
-def update_bpki(log, conf):
- m = Myrpki(conf.handle)
-
- # automatically runs configure_daemons when self-hosted
- # otherwise runs configure_resources
- m.do_update_bpki('')
-
- # when hosted, ship off to rpkid host
- if conf.host:
- configure_daemons(log, conf, m)
-
-def delete_child(log, conf, child_handle):
- m = Myrpki(conf.handle)
- m.do_delete_child(child_handle)
- configure_daemons(log, conf, m)
-
-def delete_parent(log, conf, parent_handle):
- m = Myrpki(conf.handle)
- m.do_delete_parent(parent_handle)
- configure_daemons(log, conf, m)
-
-# vim:sw=4 ts=8 expandtab
+def str_to_resource_range(prefix):
+ try:
+ r = resource_range_ipv4.parse_str(prefix)
+ except BadIPResource:
+ r = resource_range_ipv6.parse_str(prefix)
+ return r
diff --git a/rpkid/rpki/gui/app/misc.py b/rpkid/rpki/gui/app/misc.py
deleted file mode 100644
index 5d3cba93..00000000
--- a/rpkid/rpki/gui/app/misc.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# $Id$
-"""
-Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
-
-import rpki.resource_set
-import rpki.ipaddrs
-
-def str_to_range(lo, hi):
- """Convert IP address strings to resource_range_ip."""
- x = rpki.ipaddrs.parse(lo)
- y = rpki.ipaddrs.parse(hi)
- assert type(x) == type(y)
- if isinstance(x, rpki.ipaddrs.v4addr):
- return rpki.resource_set.resource_range_ipv4(x, y)
- else:
- return rpki.resource_set.resource_range_ipv6(x, y)
-
-def parse_resource_range(s):
- '''Parse an IPv4/6 resource range.'''
- # resource_set functions only accept str
- if isinstance(s, unicode):
- s = s.encode()
- try:
- return rpki.resource_set.resource_range_ipv4.parse_str(s)
- except ValueError:
- return rpki.resource_set.resource_range_ipv6.parse_str(s)
-
-def top_parent(prefix):
- '''Returns the topmost resource from which the specified argument derives'''
- while prefix.parent:
- prefix = prefix.parent
- return prefix
-
-# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py
index b78736b5..b7393717 100644
--- a/rpkid/rpki/gui/app/models.py
+++ b/rpkid/rpki/gui/app/models.py
@@ -1,271 +1,273 @@
-# $Id$
-"""
-Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
-
-import socket
+# Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
from django.db import models
-from django.contrib.auth.models import User
-
-from rpki.gui.app.misc import str_to_range
import rpki.resource_set
import rpki.exceptions
+import rpki.irdb.models
+import rpki.gui.models
+import rpki.gui.routeview.models
-class HandleField(models.CharField):
- def __init__(self, **kwargs):
- models.CharField.__init__(self, max_length=255, **kwargs)
-
-class IPAddressField(models.CharField):
- def __init__( self, **kwargs ):
- models.CharField.__init__(self, max_length=40, **kwargs)
class TelephoneField(models.CharField):
- def __init__( self, **kwargs ):
+ def __init__(self, **kwargs):
models.CharField.__init__(self, max_length=40, **kwargs)
-class Conf(models.Model):
- '''This is the center of the universe, also known as a place to
- have a handle on a resource-holding entity. It's the <self>
- in the rpkid schema.'''
- handle = HandleField(unique=True, db_index=True)
- owner = models.ManyToManyField(User)
- # NULL if self-hosted, otherwise the conf that is hosting us
- host = models.ForeignKey('Conf', related_name='hosting', null=True, blank=True)
+class Parent(rpki.irdb.models.Parent):
+ """proxy model for irdb Parent"""
def __unicode__(self):
- return self.handle
+ return u"%s's parent %s" % (self.issuer.handle, self.handle)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.parent_detail', [str(self.pk)])
+
+ class Meta:
+ proxy = True
+ verbose_name = 'Parent'
+
-class Child(models.Model):
- conf = models.ForeignKey(Conf, related_name='children')
- handle = HandleField() # parent's name for child
- valid_until = models.DateTimeField(help_text='date and time when authorization to use delegated resources ends')
+class Child(rpki.irdb.models.Child):
+ """proxy model for irdb Child"""
def __unicode__(self):
- return u"%s's child %s" % (self.conf, self.handle)
+ return u"%s's child %s" % (self.issuer.handle, self.handle)
@models.permalink
def get_absolute_url(self):
- return ('rpki.gui.app.views.child_view', [self.handle])
+ return ('rpki.gui.app.views.child_view', [str(self.pk)])
class Meta:
- verbose_name_plural = "children"
- # children of a specific configuration should be unique
- unique_together = ('conf', 'handle')
-
-class AddressRange(models.Model):
- '''An address range/prefix.'''
- lo = IPAddressField(blank=False)
- hi = IPAddressField(blank=False)
- # parent address range
- parent = models.ForeignKey('AddressRange', related_name='children',
- blank=True, null=True)
- # child to which this resource is delegated
- allocated = models.ForeignKey('Child', related_name='address_range',
- blank=True, null=True)
+ proxy = True
+ verbose_name_plural = 'Children'
+
+
+class ChildASN(rpki.irdb.models.ChildASN):
+ """Proxy model for irdb ChildASN."""
class Meta:
- ordering = ['lo', 'hi']
+ proxy = True
def __unicode__(self):
- if self.lo == self.hi:
- return u"%s" % (self.lo,)
-
- try:
- # pretty print cidr
- return unicode(self.as_resource_range())
- except socket.error, err:
- print err
- # work around for bug when hi/lo get reversed
- except AssertionError, err:
- print err
- return u'%s - %s' % (self.lo, self.hi)
+ return u'AS%s' % self.as_resource_range()
- #__unicode__.admin_order_field = 'lo'
- @models.permalink
- def get_absolute_url(self):
- return ('rpki.gui.app.views.address_view', [str(self.pk)])
-
- def as_resource_range(self):
- '''Convert to rpki.resource_set.resource_range_ip.'''
- return str_to_range(self.lo, self.hi)
-
- def is_prefix(self):
- '''Returns True if this address range can be represented as a
- prefix.'''
- try:
- self.as_resource_range().prefixlen()
- except rpki.exceptions.MustBePrefix, err:
- return False
- return True
-
-class Asn(models.Model):
- '''An ASN or range thereof.'''
- lo = models.IntegerField(blank=False)
- hi = models.IntegerField(blank=False)
- # parent asn range
- parent = models.ForeignKey('Asn', related_name='children',
- blank=True, null=True)
- # child to which this resource is delegated
- allocated = models.ForeignKey(Child, related_name='asn',
- blank=True, null=True)
+class ChildNet(rpki.irdb.models.ChildNet):
+ """Proxy model for irdb ChildNet."""
class Meta:
- ordering = ['lo', 'hi']
+ proxy = True
def __unicode__(self):
- if self.lo == self.hi:
- return u"ASN %d" % (self.lo,)
- else:
- return u"ASNs %d - %d" % (self.lo, self.hi)
+ return u'%s' % self.as_resource_range()
- #__unicode__.admin_order_field = 'lo'
- @models.permalink
- def get_absolute_url(self):
- return ('rpki.gui.app.views.asn_view', [str(self.pk)])
+class Conf(rpki.irdb.models.ResourceHolderCA):
+ """This is the center of the universe, also known as a place to
+ have a handle on a resource-holding entity. It's the <self>
+ in the rpkid schema.
- def as_resource_range(self):
- # we force conversion to long() here because resource_range_as() wants
- # the type of both arguments to be identical, and models.IntegerField
- # will be a long when the value is large
- return rpki.resource_set.resource_range_as(long(self.lo), long(self.hi))
+ """
+ @property
+ def parents(self):
+ """Simulates irdb.models.Parent.objects, but returns app.models.Parent
+ proxy objects.
-class Parent(models.Model):
- conf = models.ForeignKey(Conf, related_name='parents')
- handle = HandleField() # my name for this parent
+ """
+ return Parent.objects.filter(issuer=self)
- def __unicode__(self):
- return u"%s's parent %s" % (self.conf, self.handle)
+ @property
+ def children(self):
+ """Simulates irdb.models.Child.objects, but returns app.models.Child
+ proxy objects.
+
+ """
+ return Child.objects.filter(issuer=self)
@models.permalink
def get_absolute_url(self):
- return ('rpki.gui.app.views.parent_view', [self.handle])
+ return ('rpki.gui.app.views.user_detail', [str(self.pk)])
class Meta:
- # parents of a specific configuration should be unique
- unique_together = ('conf', 'handle')
+ proxy = True
+
class ResourceCert(models.Model):
- parent = models.ForeignKey(Parent, related_name='resources')
+ """Represents a resource certificate.
- # resources granted from my parent
- asn = models.ManyToManyField(Asn, related_name='from_cert', blank=True,
- null=True)
- address_range = models.ManyToManyField(AddressRange,
- related_name='from_cert', blank=True, null=True)
+ This model is used to cache the output of <list_received_resources/>.
- # unique id for this resource certificate
- # FIXME: URLField(verify_exists=False) doesn't seem to work - the admin
- # editor won't accept a rsync:// scheme as valid
- uri = models.CharField(max_length=200)
+ """
+ # pointer to the parent object in the irdb
+ parent = models.ForeignKey(Parent, related_name='certs')
# certificate validity period
not_before = models.DateTimeField()
not_after = models.DateTimeField()
+ # Locator for this object. Used to look up the validation status, expiry
+ # of ancestor certs in cacheview
+ uri = models.CharField(max_length=255)
+
def __unicode__(self):
- return u"%s's resource cert from parent %s" % (self.parent.conf.handle,
- self.parent.handle)
+ return u"%s's cert from %s" % (self.parent.issuer.handle,
+ self.parent.handle)
+
-class Roa(models.Model):
- '''Maps an ASN to the set of prefixes it can originate routes for.
- This differs from a real ROA in that prefixes from multiple
- parents/resource certs can be selected. The glue module contains
- code to split the ROAs into groups by common resource certs.'''
+class ResourceRangeAddressV4(rpki.gui.models.PrefixV4):
+ cert = models.ForeignKey(ResourceCert, related_name='address_ranges')
- conf = models.ForeignKey(Conf, related_name='roas')
- asn = models.IntegerField()
- active = models.BooleanField()
- # the resource cert from which all prefixes for this roa are derived
- cert = models.ForeignKey(ResourceCert, related_name='roas')
+class ResourceRangeAddressV6(rpki.gui.models.PrefixV6):
+ cert = models.ForeignKey(ResourceCert, related_name='address_ranges_v6')
+
+
+class ResourceRangeAS(rpki.gui.models.ASN):
+ cert = models.ForeignKey(ResourceCert, related_name='asn_ranges')
+
+
+class ROARequest(rpki.irdb.models.ROARequest):
+ class Meta:
+ proxy = True
def __unicode__(self):
- return u"%s's ROA for %d" % (self.conf, self.asn)
+ return u"%s's ROA request for AS%d" % (self.issuer.handle, self.asn)
- @models.permalink
- def get_absolute_url(self):
- return ('rpki.gui.app.views.roa_view', [str(self.pk)])
-class RoaRequest(models.Model):
- roa = models.ForeignKey(Roa, related_name='from_roa_request')
- max_length = models.IntegerField()
- prefix = models.ForeignKey(AddressRange, related_name='roa_requests')
+class ROARequestPrefix(rpki.irdb.models.ROARequestPrefix):
+ class Meta:
+ proxy = True
+ verbose_name = 'ROA'
def __unicode__(self):
- return u'roa request for asn %d on %s-%d' % (self.roa.asn, self.prefix,
- self.max_length)
-
- def as_roa_prefix(self):
- '''Convert to a rpki.resouce_set.roa_prefix subclass.'''
- r = self.prefix.as_resource_range()
- if isinstance(r, rpki.resource_set.resource_range_ipv4):
- return rpki.resource_set.roa_prefix_ipv4(r.min, r.prefixlen(),
- self.max_length)
- else:
- return rpki.resource_set.roa_prefix_ipv6(r.min, r.prefixlen(),
- self.max_length)
+ return u'ROA request prefix %s for asn %d' % (str(self.as_roa_prefix()),
+ self.roa_request.asn)
@models.permalink
def get_absolute_url(self):
- return ('rpki.gui.app.views.roa_request_view', [str(self.pk)])
+ return ('rpki.gui.app.views.roa_detail', [str(self.pk)])
+
-class Ghostbuster(models.Model):
+class GhostbusterRequest(rpki.irdb.models.GhostbusterRequest):
"""
- Stores the information require to fill out a vCard entry to populate
- a ghostbusters record.
+ Stores the information require to fill out a vCard entry to
+ populate a ghostbusters record.
+
+ This model is inherited from the irdb GhostBusterRequest model so
+ that the broken out fields can be included for ease of editing.
"""
+
full_name = models.CharField(max_length=40)
# components of the vCard N type
- family_name = models.CharField(max_length=20)
- given_name = models.CharField(max_length=20)
- additional_name = models.CharField(max_length=20, blank=True, null=True)
+ family_name = models.CharField(max_length=20)
+ given_name = models.CharField(max_length=20)
+ additional_name = models.CharField(max_length=20, blank=True, null=True)
honorific_prefix = models.CharField(max_length=10, blank=True, null=True)
honorific_suffix = models.CharField(max_length=10, blank=True, null=True)
- email_address = models.EmailField(blank=True, null=True)
- organization = models.CharField(blank=True, null=True, max_length=255)
- telephone = TelephoneField(blank=True, null=True)
+ email_address = models.EmailField(blank=True, null=True)
+ organization = models.CharField(blank=True, null=True, max_length=255)
+ telephone = TelephoneField(blank=True, null=True)
# elements of the ADR type
- box = models.CharField(verbose_name='P.O. Box', blank=True, null=True, max_length=40)
+ box = models.CharField(verbose_name='P.O. Box', blank=True, null=True,
+ max_length=40)
extended = models.CharField(blank=True, null=True, max_length=255)
- street = models.CharField(blank=True, null=True, max_length=255)
- city = models.CharField(blank=True, null=True, max_length=40)
- region = models.CharField(blank=True, null=True, max_length=40, help_text='state or province')
- code = models.CharField(verbose_name='Postal Code', blank=True, null=True, max_length=40)
- country = models.CharField(blank=True, null=True, max_length=40)
-
- conf = models.ForeignKey(Conf, related_name='ghostbusters')
- # parent can be null when using the same record for all parents
- parent = models.ManyToManyField(Parent, related_name='ghostbusters',
- blank=True, null=True, help_text='use this record for a specific parent, or leave blank for all parents')
+ street = models.CharField(blank=True, null=True, max_length=255)
+ city = models.CharField(blank=True, null=True, max_length=40)
+ region = models.CharField(blank=True, null=True, max_length=40,
+ help_text='state or province')
+ code = models.CharField(verbose_name='Postal Code', blank=True, null=True,
+ max_length=40)
+ country = models.CharField(blank=True, null=True, max_length=40)
def __unicode__(self):
- return u"%s's GBR: %s" % (self.conf, self.full_name)
+ return u"%s's GBR: %s" % (self.issuer.handle, self.full_name)
@models.permalink
def get_absolute_url(self):
return ('rpki.gui.app.views.ghostbuster_view', [str(self.pk)])
class Meta:
- ordering = [ 'family_name', 'given_name' ]
+ ordering = ('family_name', 'given_name')
+ verbose_name = 'Ghostbuster'
+
+
+class Timestamp(models.Model):
+ """Model to hold metadata about the collection of external data.
+
+ This model is a hash table mapping a timestamp name to the
+ timestamp value. All timestamps values are in UTC.
+
+ The utility function rpki.gui.app.timestmap.update(name) should be used to
+ set timestamps rather than updating this model directly."""
+
+ name = models.CharField(max_length=30, primary_key=True)
+ ts = models.DateTimeField(null=False)
+
+ def __unicode__(self):
+ return '%s: %s' % (self.name, self.ts)
+
+
+class Repository(rpki.irdb.models.Repository):
+ class Meta:
+ proxy = True
+ verbose_name_plural = 'Repositories'
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.repository_detail', [str(self.pk)])
+
+ def __unicode__(self):
+ return "%s's repository %s" % (self.issuer.handle, self.handle)
+
+
+class Client(rpki.irdb.models.Client):
+ "Proxy model for pubd clients."
+
+ class Meta:
+ proxy = True
+ verbose_name = 'Client'
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.client_detail', [str(self.pk)])
+
+ def __unicode__(self):
+ return self.handle
-# vim:sw=4 ts=8 expandtab
+
+class RouteOrigin(rpki.gui.routeview.models.RouteOrigin):
+ class Meta:
+ proxy = True
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.route_detail', [str(self.pk)])
+
+
+class RouteOriginV6(rpki.gui.routeview.models.RouteOriginV6):
+ class Meta:
+ proxy = True
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.route_detail', [str(self.pk)])
diff --git a/rpkid/rpki/gui/app/range_list.py b/rpkid/rpki/gui/app/range_list.py
new file mode 100755
index 00000000..fcfcfc24
--- /dev/null
+++ b/rpkid/rpki/gui/app/range_list.py
@@ -0,0 +1,244 @@
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
+
+import bisect
+import unittest
+
+class RangeList(list):
+ """A sorted list of ranges, which automatically merges adjacent ranges.
+
+ Items in the list are expected to have ".min" and ".max" attributes."""
+
+ def __init__(self, ini=None):
+ list.__init__(self)
+ if ini:
+ self.extend(ini)
+
+ def append(self, v):
+ keys = [x.min for x in self]
+
+ # lower bound
+ i = bisect.bisect_left(keys, v.min)
+
+ # upper bound
+ j = bisect.bisect_right(keys, v.max, lo=i)
+
+ # if the max value for the previous item is greater than v.min, include the previous item in the range to replace
+ # and use its min value. also include the previous item if the max value is 1 less than the min value for the
+ # inserted item
+ if i > 0 and self[i-1].max >= v.min - 1:
+ i = i - 1
+ vmin = self[i].min
+ else:
+ vmin = v.min
+
+ # if the max value for the previous item is greater than the max value for the new item, use the previous item's max
+ if j > 0 and self[j-1].max > v.max:
+ vmax = self[j-1].max
+ else:
+ vmax = v.max
+
+ # if the max value for the new item is 1 less than the min value for the next item, combine into a single item
+ if j < len(self) and vmax+1 == self[j].min:
+ vmax = self[j].max
+ j = j+1
+
+ # replace the range with a new object covering the entire range
+ self[i:j] = [v.__class__(min=vmin, max=vmax)]
+
+ def extend(self, args):
+ for x in args:
+ self.append(x)
+
+ def difference(self, other):
+ """Return a RangeList object which contains ranges in this object which are not in "other"."""
+ it = iter(other)
+
+ try:
+ cur = it.next()
+ except StopIteration:
+ return self
+
+ r = RangeList()
+
+ for x in self:
+ xmin = x.min
+
+ def V(v):
+ """convert the integer value to the appropriate type for this
+ range"""
+ return x.__class__.datum_type(v)
+
+ try:
+ while xmin <= x.max:
+ if xmin < cur.min:
+ r.append(x.__class__(min=V(xmin),
+ max=V(min(x.max,cur.min-1))))
+ xmin = cur.max+1
+ elif xmin == cur.min:
+ xmin = cur.max+1
+ else: # xmin > cur.min
+ if xmin <= cur.max:
+ xmin = cur.max+1
+ else: # xmin > cur.max
+ cur = it.next()
+
+ except StopIteration:
+ r.append(x.__class__(min=V(xmin), max=x.max))
+
+ return r
+
+class TestRangeList(unittest.TestCase):
+ class MinMax(object):
+ def __init__(self, min, max):
+ self.min = min
+ self.max = max
+
+ def __str__(self):
+ return '(%d, %d)' % (self.min, self.max)
+
+ def __repr__(self):
+ return '<MinMax: (%d, %d)>' % (self.min, self.max)
+
+ def __eq__(self, other):
+ return self.min == other.min and self.max == other.max
+
+ def setUp(self):
+ self.v1 = TestRangeList.MinMax(1,2)
+ self.v2 = TestRangeList.MinMax(4,5)
+ self.v3 = TestRangeList.MinMax(7,8)
+ self.v4 = TestRangeList.MinMax(3,4)
+ self.v5 = TestRangeList.MinMax(2,3)
+ self.v6 = TestRangeList.MinMax(1,10)
+
+ def test_empty_append(self):
+ s = RangeList()
+ s.append(self.v1)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], self.v1)
+
+ def test_no_overlap(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v2)
+ self.assertTrue(len(s) == 2)
+ self.assertEqual(s[0], self.v1)
+ self.assertEqual(s[1], self.v2)
+
+ def test_no_overlap_prepend(self):
+ s = RangeList()
+ s.append(self.v2)
+ s.append(self.v1)
+ self.assertTrue(len(s) == 2)
+ self.assertEqual(s[0], self.v1)
+ self.assertEqual(s[1], self.v2)
+
+ def test_insert_middle(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v3)
+ s.append(self.v2)
+ self.assertTrue(len(s) == 3)
+ self.assertEqual(s[0], self.v1)
+ self.assertEqual(s[1], self.v2)
+ self.assertEqual(s[2], self.v3)
+
+ def test_append_overlap(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v5)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], TestRangeList.MinMax(1,3))
+
+ def test_combine_range(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v4)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], TestRangeList.MinMax(1,4))
+
+ def test_append_subset(self):
+ s = RangeList()
+ s.append(self.v6)
+ s.append(self.v3)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], self.v6)
+
+ def test_append_equal(self):
+ s = RangeList()
+ s.append(self.v6)
+ s.append(self.v6)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], self.v6)
+
+ def test_prepend_combine(self):
+ s = RangeList()
+ s.append(self.v4)
+ s.append(self.v1)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], TestRangeList.MinMax(1,4))
+
+ def test_append_aggregate(self):
+ s = RangeList()
+ s.append(self.v1)
+ s.append(self.v2)
+ s.append(self.v3)
+ s.append(self.v6)
+ self.assertTrue(len(s) == 1)
+ self.assertEqual(s[0], self.v6)
+
+ def test_diff_empty(self):
+ s = RangeList()
+ s.append(self.v1)
+ self.assertEqual(s, s.difference([]))
+
+ def test_diff_self(self):
+ s = RangeList()
+ s.append(self.v1)
+ self.assertEqual(s.difference(s), [])
+
+ def test_diff_middle(self):
+ s1 = RangeList([self.v6])
+ s2 = RangeList([self.v3])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,6), TestRangeList.MinMax(9, 10)]))
+
+ def test_diff_overlap(self):
+ s1 = RangeList([self.v2])
+ s2 = RangeList([self.v4])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(5,5)]))
+
+ def test_diff_overlap2(self):
+ s1 = RangeList([self.v2])
+ s2 = RangeList([self.v4])
+ self.assertEqual(s2.difference(s1), RangeList([TestRangeList.MinMax(3,3)]))
+
+ def test_diff_multi(self):
+ s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(4,5)])
+ s2 = RangeList([TestRangeList.MinMax(4,4)])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(5,5)]))
+
+ def test_diff_multi_overlap(self):
+ s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(3,4)])
+ s2 = RangeList([TestRangeList.MinMax(2,3)])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,1), TestRangeList.MinMax(4,4)]))
+
+ def test_diff_multi_overlap2(self):
+ s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(3,4), TestRangeList.MinMax(6,7)])
+ s2 = RangeList([TestRangeList.MinMax(2,3), TestRangeList.MinMax(6,6)])
+ self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,1), TestRangeList.MinMax(4,4), TestRangeList.MinMax(7,7)]))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/rpkid/rpki/gui/app/settings.py.in b/rpkid/rpki/gui/app/settings.py.in
index 28410f35..fcfe4678 100644
--- a/rpkid/rpki/gui/app/settings.py.in
+++ b/rpkid/rpki/gui/app/settings.py.in
@@ -7,16 +7,4 @@
from django.conf import settings
-# directory containing the resource handles served by the rpki portal gui
-CONFDIR = settings.MYRPKI if hasattr(settings, 'CONFDIR') else '%(AC_LOCALSTATEDIR)s/rpki/conf'
-
-# maildir-style mailbox where uploaded requests are saved
-INBOX = settings.MYRPKI if hasattr(settings, 'INBOX') else '%(AC_LOCALSTATEDIR)s/rpki/inbox'
-
-# maildir-style mailbox where responses to client requests are stored
-OUTBOX = settings.MYRPKI if hasattr(settings, 'OUTBOX') else '%(AC_LOCALSTATEDIR)s/rpki/outbox'
-
-# uid the web server runs as
-WEB_USER = settings.MYRPKI if hasattr(settings, 'WEB_USER') else '%(AC_WEBUSER)s'
-
-RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE if hasattr(settings, 'RPKI_CONF_TEMPLATE') else '%(AC_DATAROOTDIR)s/rpki/gui/rpki.conf.template'
+RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE if hasattr(settings, 'RPKI_CONF_TEMPLATE') else '%(AC_DATAROOTDIR)s/rpki/rpki.conf.template'
diff --git a/rpkid/rpki/gui/app/templates/app/app_base.html b/rpkid/rpki/gui/app/templates/app/app_base.html
new file mode 100644
index 00000000..c7901115
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/app_base.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+
+{# This template defines the common structure for the rpki.gui.app application. #}
+
+{% block sidebar %}
+
+<h2>{{ request.session.handle }}</h2>
+
+{# common navigation #}
+
+<ul class='unstyled'>
+ <li><a href="{% url rpki.gui.app.views.dashboard %}">dashboard</a></li>
+ <li><a href="{% url rpki.gui.app.views.route_view %}">routes</a></li>
+ <li><a href="{% url rpki.gui.app.views.parent_list %}">parents</a></li>
+ <li><a href="{% url rpki.gui.app.views.child_list %}">children</a></li>
+ <li><a href="{% url rpki.gui.app.views.roa_list %}">roas</a></li>
+ <li><a href="{% url rpki.gui.app.views.ghostbuster_list %}">ghostbusters</a></li>
+ <li><a href="{% url rpki.gui.app.views.repository_list %}">repositories</a></li>
+</ul>
+
+{% if request.user.is_superuser %}
+<ul class='unstyled'>
+ <li><a href="{% url rpki.gui.app.views.client_list %}">pubclients</a></li>
+ <li><a href="{% url rpki.gui.app.views.conf_list %}" title="select a different resource handle to manage">select identity</a></li>
+ <li><a href="{% url rpki.gui.app.views.user_list %}" title="manage users">users</a></li>
+</ul>
+{% endif %}
+
+{% block sidebar_extra %}{% endblock %}
+
+{% endblock sidebar %}
diff --git a/rpkid/rpki/gui/app/templates/app/bootstrap_form.html b/rpkid/rpki/gui/app/templates/app/bootstrap_form.html
new file mode 100644
index 00000000..bf8b3553
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/bootstrap_form.html
@@ -0,0 +1,33 @@
+{# vim:set ft=htmldjango #}
+
+{% if form.non_field_errors %}
+<div class='alert-message error'>
+ <p>{{ form.non_field_errors }}
+</div>
+{% endif %}
+
+{% for field in form %}
+
+{% if field.is_hidden %}
+ {{ field }}
+{% else %}
+ <div class='clearfix {% if field.errors %}error{% endif %}'>
+ {{ field.label_tag }}
+ {% if field.required %}*{% endif %}
+ <div class='input'>
+ {{ field }}
+ {% if field.help_text %}
+ <span class='help-inline'>{{ field.help_text }}</span>
+ {% endif %}
+ {% if field.errors %}
+ <ul>
+ {% for error in field.errors %}
+ <li class='help-inline'>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ </div><!-- input -->
+ </div><!-- clearfix -->
+{% endif %}
+
+{% endfor %}
diff --git a/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html b/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html
new file mode 100644
index 00000000..98789191
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html
@@ -0,0 +1,16 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>Add Resource: {{ object.handle }}</h1>
+</div>
+
+<form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='actions'>
+ <input class='btn primary' type='submit' value='Save'>
+ <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a>
+ </div>
+</form>
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_delete_form.html b/rpkid/rpki/gui/app/templates/app/child_delete_form.html
index 22c40a60..22c40a60 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/child_delete_form.html
+++ b/rpkid/rpki/gui/app/templates/app/child_delete_form.html
diff --git a/rpkid/rpki/gui/app/templates/app/child_detail.html b/rpkid/rpki/gui/app/templates/app/child_detail.html
new file mode 100644
index 00000000..b180633d
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/child_detail.html
@@ -0,0 +1,53 @@
+{% extends "app/object_detail.html" %}
+
+{% block object_detail %}
+<div class='row'>
+ <div class='span2'>
+ <p><strong>Child Handle</strong>
+ </div>
+ <div class='span2'>
+ <p>{{ object.handle }}
+ </div>
+</div>
+<div class='row'>
+ <div class='span2'>
+ <p><strong>Valid until</strong>
+ </div>
+ <div class='span4'>
+ <p>{{ object.valid_until }}
+ </div>
+</div>
+
+<div class='row'>
+ <div class='span4'>
+ <strong>Addresses</strong>
+ {% if object.address_ranges.all %}
+ <ul class='unstyled'>
+ {% for a in object.address_ranges.all %}
+ <li>{{ a.as_resource_range }}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p style='font-style:italic'>none</p>
+ {% endif %}
+ </div>
+ <div class='span4'>
+ <strong>ASNs</strong>
+ {% if object.asns.all %}
+ <ul class='unstyled'>
+ {% for a in object.asns.all %}
+ <li>{{ a.as_resource_range }}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p style='font-style:italic'>none</p>
+ {% endif %}
+ </div>
+</div>
+{% endblock object_detail %}
+
+{% block actions %}
+<a class='btn' href="{{ object.get_absolute_url }}/add_asn" title='Delegate an ASN to this child'>+AS</a>
+<a class='btn' href="{{ object.get_absolute_url }}/add_address" title='Delegate a prefix to this child'>+Prefix</a>
+<a class='btn' href="{{ object.get_absolute_url }}/export" title='Download XML file to send to child'>Export</a>
+{% endblock actions %}
diff --git a/rpkid/rpki/gui/app/templates/app/child_form.html b/rpkid/rpki/gui/app/templates/app/child_form.html
new file mode 100644
index 00000000..cd9b2a8c
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/child_form.html
@@ -0,0 +1,17 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>Edit Child: {{ object.handle }}</h1>
+</div>
+
+<form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='actions'>
+ <input class='btn primary' type='submit' value='Save'>
+ <a class='btn' href="{{ object.get_absolute_url }}">Cancel</a>
+ </div>
+</form>
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/app/child_import_form.html b/rpkid/rpki/gui/app/templates/app/child_import_form.html
new file mode 100644
index 00000000..4b0cf9d2
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/child_import_form.html
@@ -0,0 +1,20 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>Import Child</h1>
+</div>
+
+<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}">
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='actions'>
+ <input class='btn primary' type="submit" value="Import">
+ <a class='btn' href="{% url rpki.gui.app.views.child_list %}">Cancel</a>
+ </div>
+</form>
+
+{% endblock %}
+
+<!-- vim: set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/child_list.html b/rpkid/rpki/gui/app/templates/app/child_list.html
new file mode 100644
index 00000000..9ba31ffd
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/child_list.html
@@ -0,0 +1,7 @@
+{% extends "app/object_list.html" %}
+
+{% block object_detail %}
+<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li>
+{% endblock object_detail %}
+
+<!-- vim: set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/client_detail.html b/rpkid/rpki/gui/app/templates/app/client_detail.html
new file mode 100644
index 00000000..4755fbca
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/client_detail.html
@@ -0,0 +1,20 @@
+{% extends "app/object_detail.html" %}
+
+{% block object_detail %}
+<div class='row'>
+ <div class='span2'>
+ <p><strong>Name</strong>
+ </div>
+ <div class='span6'>
+ <p>{{ object.handle }}
+ </div>
+</div>
+<div class='row'>
+ <div class='span2'>
+ <p><strong>SIA</strong>
+ </div>
+ <div class='span6'>
+ <p>{{ object.sia_base }}
+ </div>
+</div>
+{% endblock object_detail %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_child_form.html b/rpkid/rpki/gui/app/templates/app/client_import_form.html
index acd6bf61..acd6bf61 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/import_child_form.html
+++ b/rpkid/rpki/gui/app/templates/app/client_import_form.html
diff --git a/rpkid/rpki/gui/app/templates/app/client_list.html b/rpkid/rpki/gui/app/templates/app/client_list.html
new file mode 100644
index 00000000..a2a0a5a2
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/client_list.html
@@ -0,0 +1 @@
+{% extends "app/object_list.html" %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/conf_empty.html b/rpkid/rpki/gui/app/templates/app/conf_empty.html
index 0ef9366c..0ef9366c 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/conf_empty.html
+++ b/rpkid/rpki/gui/app/templates/app/conf_empty.html
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/conf_list.html b/rpkid/rpki/gui/app/templates/app/conf_list.html
index 4bb18114..4bb18114 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/conf_list.html
+++ b/rpkid/rpki/gui/app/templates/app/conf_list.html
diff --git a/rpkid/rpki/gui/app/templates/app/dashboard.html b/rpkid/rpki/gui/app/templates/app/dashboard.html
new file mode 100644
index 00000000..f74dad09
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/dashboard.html
@@ -0,0 +1,85 @@
+{% extends "app/app_base.html" %}
+
+{% block sidebar_extra %}
+<ul class='unstyled'>
+ <li><a href="{% url rpki.gui.app.views.conf_export %}" title="download XML identity to send to parent">export identity</a></li>
+</ul>
+
+<ul class='unstyled'>
+ <li><a href="{% url rpki.gui.app.views.refresh %}">refresh</a></li>
+</ul>
+{% endblock sidebar_extra %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>Dashboard</h1>
+</div>
+
+<div class='row'>
+ <div class='span10'>
+ <h2>Resources</h2>
+ <table class='condensed-table zebra-striped'>
+ <tr>
+ <th>Resource</th>
+ <th>Valid Until</th>
+ <th>Parent</th>
+ </tr>
+
+ {% for object in asns %}
+ <tr>
+ <td>{{ object }}</td>
+ <td>{{ object.cert.not_after }}</td>
+ <td>{{ object.cert.parent.handle }}</td>
+ </tr>
+ {% endfor %}
+
+ {% for object in prefixes %}
+ <tr>
+ <td>{{ object.as_resource_range }}</td>
+ <td>{{ object.cert.not_after }}</td>
+ <td>{{ object.cert.parent.handle }}</td>
+ </tr>
+ {% endfor %}
+
+ {% if prefixes_v6 %}
+ {% for object in prefixes_v6 %}
+ <tr>
+ <td>{{ object.as_resource_range }}</td>
+ <td>{{ object.cert.not_after }}</td>
+ <td>{{ object.cert.parent.handle }}</td>
+ </tr>
+ {% endfor %}
+ {% endif %}
+ </table>
+ </div>
+ <div class='span6'>
+ <h2>Unallocated Resources</h2>
+ <p>The following resources have not been allocated to a child, nor appear in a ROA.
+
+ {% if unused_asns %}
+ <ul>
+ {% for asn in unused_asns %}
+ <li>AS{{ asn }}
+ {% endfor %} <!-- ASNs -->
+ </ul>
+ {% endif %}
+
+ {% if unused_prefixes %}
+ <ul>
+ {% for addr in unused_prefixes %}
+ <li>{{ addr }}
+ {% endfor %} <!-- addrs -->
+ </ul>
+ {% endif %}
+
+ {% if unused_prefixes_v6 %}
+ <ul>
+ {% for addr in unused_prefixes_v6 %}
+ <li>{{ addr }}
+ {% endfor %} <!-- addrs -->
+ </ul>
+ {% endif %}
+
+ </div><!-- /span -->
+</div><!-- /row -->
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/destroy_handle_form.html b/rpkid/rpki/gui/app/templates/app/destroy_handle_form.html
index e1e6711f..e1e6711f 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/destroy_handle_form.html
+++ b/rpkid/rpki/gui/app/templates/app/destroy_handle_form.html
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/generic_result.html b/rpkid/rpki/gui/app/templates/app/generic_result.html
index 65d4e42e..65d4e42e 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/generic_result.html
+++ b/rpkid/rpki/gui/app/templates/app/generic_result.html
diff --git a/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html
new file mode 100644
index 00000000..76b1d25a
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html
@@ -0,0 +1,20 @@
+{% extends "app/ghostbuster_detail.html" %}
+
+{% block extra %}
+
+<div class='alert-message block-message warning'>
+ <p>
+ <strong>Please confirm</strong> that you really want to delete by clicking Delete.
+
+ <div class='alert-actions'>
+ <form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ <input class='btn danger' type='submit' value='Delete' />
+ <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a>
+ </form>
+ </div>
+</div>
+
+{% endblock %}
+
+<!-- vim:set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html b/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html
new file mode 100644
index 00000000..b6f28815
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html
@@ -0,0 +1,21 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>Edit Ghostbuster Request</h1>
+</div>
+
+<form action='{{ request.get_full_path }}' method='POST'>
+ {% csrf_token %}
+
+ {# include code to render form using Twitter Bootstrap CSS Framework #}
+ {% include "app/bootstrap_form.html" %}
+
+ <div class='actions'>
+ <input class='btn primary' type='submit' value='Save'>
+ <a class='btn' href="{{ object.get_absolute_url }}">Cancel</a>
+ </div>
+
+</form>
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html
new file mode 100644
index 00000000..fa8915e4
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html
@@ -0,0 +1,53 @@
+{% extends "app/object_detail.html" %}
+
+{% block object_detail %}
+<table class='zebra-striped condensed-table'>
+ <tr><td >Full Name</td><td>{{ object.full_name }}</td></tr>
+
+ {% if object.honorific_prefix %}
+ <tr><td >Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr>
+ {% endif %}
+
+ {% if object.organization %}
+ <tr><td >Organization</td><td>{{ object.organization }}</td></tr>
+ {% endif %}
+
+ {% if object.telephone %}
+ <tr><td >Telephone</td><td>{{ object.telephone }}</td></tr>
+ {% endif %}
+
+ {% if object.email_address %}
+ <tr><td >Email</td><td>{{ object.email_address }}</td></tr>
+ {% endif %}
+
+ {% if object.box %}
+ <tr><td >P.O. Box</td><td>{{ object.box }}</td></tr>
+ {% endif %}
+
+ {% if object.extended %}
+ <tr><td >Extended Address</td><td>{{ object.extended }}</td></tr>
+ {% endif %}
+
+ {% if object.street %}
+ <tr><td >Street Address</td><td>{{ object.street }}</td></tr>
+ {% endif %}
+
+ {% if object.city %}
+ <tr><td >City</td><td>{{ object.city }}</td></tr>
+ {% endif %}
+
+ {% if object.region %}
+ <tr><td >Region</td><td>{{ object.region }}</td></tr>
+ {% endif %}
+
+ {% if object.code %}
+ <tr><td >Postal Code</td><td>{{ object.code }}</td></tr>
+ {% endif %}
+
+ {% if object.country %}
+ <tr><td >Country</td><td>{{ object.country }}</td></tr>
+ {% endif %}
+
+</table>
+{% endblock object_detail %}
+<!-- vim: set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html
new file mode 100644
index 00000000..327b79b1
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html
@@ -0,0 +1,13 @@
+{% extends "app/object_list.html" %}
+
+{% block object_detail %}
+<li><a href="{{ object.get_absolute_url }}">{{ object.full_name }}</a></li>
+{% endblock object_detail %}
+
+{% block actions %}
+<div class='actions'>
+ <a class='btn' href='{% url rpki.gui.app.views.ghostbuster_create %}' title='Create a new Ghostbuster Request'>Create</a>
+</div>
+{% endblock actions %}
+
+<!-- vim: set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/initialize_form.html b/rpkid/rpki/gui/app/templates/app/initialize_form.html
index 372316ee..372316ee 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/initialize_form.html
+++ b/rpkid/rpki/gui/app/templates/app/initialize_form.html
diff --git a/rpkid/rpki/gui/app/templates/app/object_detail.html b/rpkid/rpki/gui/app/templates/app/object_detail.html
new file mode 100644
index 00000000..6a93f644
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/object_detail.html
@@ -0,0 +1,33 @@
+{% extends "app/app_base.html" %}
+{% load app_extras %}
+
+{% block content %}
+<div class='page-header'>
+ <h1>{% verbose_name object %}</h1>
+</div>
+
+{% block object_detail %}
+{{ object }}
+{% endblock object_detail %}
+
+{% if confirm_delete %}
+<div class='alert-message block-message warning'>
+ <p><strong>Please confirm</strong> that you would like to delete this object.
+ <div class='alert-actions'>
+ <form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ <input class='btn danger' type='submit' value='Delete'/>
+ <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a>
+ </form>
+ </div>
+</div>
+{% else %}
+<div class='actions'>
+ {% if can_edit %}
+ <a class='btn' href='{{ object.get_absolute_url }}/edit'>Edit</a>
+ {% endif %}
+ <a class='btn danger' href='{{ object.get_absolute_url }}/delete' title='Permanently delete this object'>Delete</a>
+ {% block actions %}{% endblock actions %}
+</div>
+{% endif %}
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/object_list.html b/rpkid/rpki/gui/app/templates/app/object_list.html
new file mode 100644
index 00000000..e78eab98
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/object_list.html
@@ -0,0 +1,36 @@
+{% extends "app/app_base.html" %}
+{% load app_extras %}
+
+{# generic object list #}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>{% verbose_name_plural object_list %}</h1>
+</div>
+
+{% if object_list %}
+<ul>
+ {% for object in object_list %}
+ {% block object_detail %}
+ <li><a href="{{ object.get_absolute_url }}">{{ object }}</a></li>
+ {% endblock %}
+ {% endfor %}
+</ul>
+{% else %}
+<div class='alert-message warning'>
+ <p>There are <strong>no items</strong> in this list.
+</div>
+{% endif %}
+
+{% block actions %}
+{% if create_url %}
+<div class='actions'>
+ <a class='btn' href='{{ create_url }}'>{{ create_label|default:"Create" }}</a>
+</div>
+{% endif %}
+{% endblock %}
+
+{% endblock %}
+
+<!-- vim: set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/object_table.html b/rpkid/rpki/gui/app/templates/app/object_table.html
new file mode 100644
index 00000000..4d154490
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/object_table.html
@@ -0,0 +1,41 @@
+{% extends "app/app_base.html" %}
+{% load app_extras %}
+
+{# Generic object list displayed as a table. #}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>{% verbose_name_plural object_list %}</h1>
+</div>
+
+{% if object_list %}
+<table style='zebra-striped condensed-table'>
+ <tr>
+ {% block table_header %}{% endblock %}
+ </tr>
+ {% for object in object_list %}
+ <tr>
+ {% block object_detail %}
+ <td><a href="{{ object.get_absolute_url }}">{{ object }}</a></td>
+ {% endblock %}
+ </tr>
+ {% endfor %}
+</table>
+{% else %}
+<div class='alert-message warning'>
+ <p>There are <strong>no items</strong> in this list.
+</div>
+{% endif %}
+
+{% block actions %}
+{% if create_url %}
+<div class='actions'>
+ <a class='btn' href='{{ create_url }}'>{{ create_label|default:"Create" }}</a>
+</div>
+{% endif %}
+{% endblock %}
+
+{% endblock %}
+
+<!-- vim: set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/parent_detail.html b/rpkid/rpki/gui/app/templates/app/parent_detail.html
new file mode 100644
index 00000000..e5703074
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/parent_detail.html
@@ -0,0 +1,62 @@
+{% extends "app/object_detail.html" %}
+
+{% block object_detail %}
+<h2>{{ object.handle }}</h2>
+
+<table>
+ <tr>
+ <td>service_uri</td>
+ <td>{{ object.service_uri }}</td>
+ </tr>
+ <tr>
+ <td>parent_handle</td>
+ <td>{{ object.parent_handle }}</td>
+ </tr>
+ <tr>
+ <td>child_handle</td>
+ <td>{{ object.child_handle }}</td>
+ </tr>
+ <tr>
+ <td>repository_type</td>
+ <td>{{ object.repository_type }}</td>
+ </tr>
+ <tr>
+ <td>referrer</td>
+ <td>{{ object.referrer }}</td>
+ </tr>
+ <tr>
+ <td>ta validity period</td>
+ <td>{{ object.ta.getNotBefore }} - {{ object.ta.getNotAfter }}</td>
+ </tr>
+</table>
+
+<div class='row'>
+ <div class='span4'>
+ <h3>Delegated Addresses</h3>
+ <ul class='unstyled'>
+ {% for c in object.certs.all %}
+ {% for a in c.address_ranges.all %}
+ <li>{{ a }}</li>
+ {% endfor %}
+ {% for a in c.address_ranges_v6.all %}
+ <li>{{ a }}</li>
+ {% endfor %}
+ {% endfor %}
+ </ul>
+ </div>
+ <div class='span4'>
+ <h3>Delegated ASNs</h3>
+ <ul class='unstyled'>
+ {% for c in object.certs.all %}
+ {% for a in c.asn_ranges.all %}
+ <li>{{ a }}</li>
+ {% endfor %}
+ {% endfor %}
+ </ul>
+ </div>
+</div>
+{% endblock object_detail %}
+
+{% block actions %}
+<a class='btn' href='{{ object.get_absolute_url }}/export' title='Download XML to send to repository operator'>Export</a>
+{% endblock actions %}
diff --git a/rpkid/rpki/gui/app/templates/app/parent_import_form.html b/rpkid/rpki/gui/app/templates/app/parent_import_form.html
new file mode 100644
index 00000000..c192a4a4
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/parent_import_form.html
@@ -0,0 +1,20 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>Import Parent</h1>
+</div>
+
+<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}">
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='actions'>
+ <input class='btn primary' type="submit" value="Import">
+ <a class='btn' href="{% url rpki.gui.app.views.parent_list %}">Cancel</a>
+ </div>
+</form>
+
+{% endblock content %}
+
+<!-- vim: set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/parent_list.html b/rpkid/rpki/gui/app/templates/app/parent_list.html
new file mode 100644
index 00000000..81744130
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/parent_list.html
@@ -0,0 +1,5 @@
+{% extends "app/object_list.html" %}
+
+{% block object_detail %}
+<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li>
+{% endblock object_detail %}
diff --git a/rpkid/rpki/gui/app/templates/app/pubclient_list.html b/rpkid/rpki/gui/app/templates/app/pubclient_list.html
new file mode 100644
index 00000000..0296dcdf
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/pubclient_list.html
@@ -0,0 +1,9 @@
+{% extends "app/object_list.html" %}
+
+{% block actions %}
+<div class='actions'>
+ <a class='btn' href='{% url rpki.gui.app.views.pubclient_import %}'>Import</a>
+</div>
+{% endblock actions %}
+
+<!-- vim:set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/repository_detail.html b/rpkid/rpki/gui/app/templates/app/repository_detail.html
new file mode 100644
index 00000000..599357bd
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/repository_detail.html
@@ -0,0 +1,20 @@
+{% extends "app/object_detail.html" %}
+
+{% block object_detail %}
+<div class='row'>
+ <div class='span2'>
+ <p><strong>Name</strong>
+ </div>
+ <div class='span6'>
+ <p>{{ object.handle }}
+ </div>
+</div>
+<div class='row'>
+ <div class='span2'>
+ <p><strong>SIA</strong>
+ </div>
+ <div class='span6'>
+ <p>{{ object.sia_base }}</td>
+ </div>
+</div>
+{% endblock object_detail %}
diff --git a/rpkid/rpki/gui/app/templates/app/repository_import_form.html b/rpkid/rpki/gui/app/templates/app/repository_import_form.html
new file mode 100644
index 00000000..bf79e59c
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/repository_import_form.html
@@ -0,0 +1,18 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>Import Repository</h1>
+</div>
+
+<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}">
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='actions'>
+ <input class='btn primary' type="submit" value="Import">
+ <a class='btn' href="{% url rpki.gui.app.views.repository_list %}">Cancel</a>
+ </div>
+</form>
+
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/repository_list.html b/rpkid/rpki/gui/app/templates/app/repository_list.html
new file mode 100644
index 00000000..2ccd0223
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/repository_list.html
@@ -0,0 +1,7 @@
+{% extends "app/object_list.html" %}
+
+{% block object_detail %}
+<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li>
+{% endblock %}
+
+<!-- vim:set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html
new file mode 100644
index 00000000..4c8228b6
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html
@@ -0,0 +1,54 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-header'>
+<h1>Delete ROA Prefix</h1>
+</div>
+
+<div class='row'>
+ <div class='span8'>
+ <div class='alert-message block-message warning'>
+ <p><strong>Please confirm</strong> that you would like to delete the following ROA Request. The table to the right indicates how validation status for matching routes may change.
+
+ <table style='condensed-table'>
+ <tr>
+ <th>Prefix</th>
+ <th>Max Length</th>
+ <th>AS</th>
+ <tr>
+ <td>{{ object.prefix }}/{{ object.prefixlen }}</td>
+ <td>{{ object.max_prefixlen }}</td>
+ <td>{{ object.roa_request.asn }}</td>
+ </tr>
+ </table>
+
+ <form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ <div class='alert-actions'>
+ <input class='btn danger' type='submit' value='Delete'/>
+ <a class='btn' href="{% url rpki.gui.app.views.roa_list %}">Cancel</a>
+ </div>
+ </form>
+ </div>
+ </div>
+
+ <div class='span8'>
+ <h2>Matching Routes</h2>
+
+ <table style='zebra-striped condensed-table'>
+ <tr>
+ <th>Prefix</th>
+ <th>Origin AS</th>
+ <th>Validation Status</th>
+ </tr>
+ {% for r in routes %}
+ <tr>
+ <td>{{ r.get_prefix_display }}</td>
+ <td>{{ r.asn }}</td>
+ <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div><!-- /span8 -->
+</div><!-- /row -->
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/roa_request_list.html b/rpkid/rpki/gui/app/templates/app/roa_request_list.html
new file mode 100644
index 00000000..9ffe4f57
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/roa_request_list.html
@@ -0,0 +1,14 @@
+{% extends "app/object_table.html" %}
+
+{% block table_header %}
+<th>Prefix</th><th>Max Length</th><th>ASN</th><th>Action</th>
+{% endblock %}
+
+{% block object_detail %}
+<td>{{ object.prefix }}/{{ object.prefixlen }}</a></td>
+<td>{{ object.max_prefixlen }}</td>
+<td>{{ object.roa_request.asn }}</td>
+<td><a class='btn danger' href="{{ object.get_absolute_url }}/delete">Delete</a></td>
+{% endblock %}
+
+<!-- vim: set sw=2: -->
diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html
new file mode 100644
index 00000000..60d0b0fe
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html
@@ -0,0 +1,58 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Create ROA</h1>
+</div>
+
+<div class='row'>
+ <div class='span8'>
+ <div class='alert-message block-message warning'>
+ <p><strong>Please confirm</strong> that you would like to create the following ROA.
+ The accompanying table indicates how the validation status may change as a result.
+
+ <table class='condensed-table'>
+ <tr>
+ <th>AS</th>
+ <th>Prefix</th>
+ <th>Max Length</th>
+ </tr>
+ <tr>
+ <td>{{ asn }}</td>
+ <td>{{ prefix }}</td>
+ <td>{{ max_prefixlen }}</td>
+ </tr>
+ </table>
+
+ <form method='POST' action='{% url rpki.gui.app.views.roa_create_confirm %}'>
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='alert-actions'>
+ <input class='btn primary' type='submit' value='Create'/>
+ <a class='btn' href='{% url rpki.gui.app.views.roa_list %}'>Cancel</a>
+ </div>
+ </form>
+ </div><!-- /alert-message -->
+ </div>
+
+ <div class='span8'>
+ <h2>Matched Routes</h2>
+
+ <table style='zebra-striped condensed-table'>
+ <tr>
+ <th>Prefix</th>
+ <th>Origin AS</th>
+ <th>Validation Status</th>
+ </tr>
+ {% for r in routes %}
+ <tr>
+ <td>{{ r.get_prefix_display }}</td>
+ <td>{{ r.asn }}</td>
+ <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+
+</div>
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_form.html b/rpkid/rpki/gui/app/templates/app/roarequest_form.html
new file mode 100644
index 00000000..5385cab0
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/roarequest_form.html
@@ -0,0 +1,16 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Create ROA</h1>
+</div>
+
+<form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='actions'>
+ <input class='btn primary' type='submit' value='Create'/>
+ <a class='btn' href='{% url rpki.gui.app.views.roa_list %}'>Cancel</a>
+ </div>
+</form>
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/route_roa_list.html b/rpkid/rpki/gui/app/templates/app/route_roa_list.html
new file mode 100644
index 00000000..1907315d
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/route_roa_list.html
@@ -0,0 +1,19 @@
+{% extends "app/object_table.html" %}
+
+{# template for displaying the list of ROAs covering a specific route #}
+
+{% block table_header %}
+<th>Prefix</th>
+<th>Max Length</th>
+<th>ASN</th>
+<th>Expires</th>
+<th>URI</th>
+{% endblock %}
+
+{% block object_detail %}
+<td>{{ object.as_resource_range }}</td>
+<td>{{ object.max_length }}</td>
+<td>{{ object.roas.all.0.asid }}</td>
+<td>{{ object.roas.all.0.not_after }}</td>
+<td>{{ object.roas.all.0.repo.uri }}</td>
+{% endblock object_detail %}
diff --git a/rpkid/rpki/gui/app/templates/app/routes_view.html b/rpkid/rpki/gui/app/templates/app/routes_view.html
new file mode 100644
index 00000000..be4f8f6e
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/routes_view.html
@@ -0,0 +1,41 @@
+{% extends "app/app_base.html" %}
+
+{% block sidebar_extra %}
+<p>
+BGP data updated<br>
+IPv4: {{ timestamp.bgp_v4_import.isoformat }}<br>
+IPv6: {{ timestamp.bgp_v6_import.isoformat }}
+<p>
+rcynic cache updated<br>
+{{ timestamp.rcynic_import.isoformat }}
+
+{% endblock sidebar_extra %}
+
+{% block content %}
+
+<div class='page-header'>
+ <h1>Route View</h1>
+</div>
+
+<p>
+This view shows currently advertised routes for the prefixes listed in resource certs received from RPKI parents.
+
+<table class='zebra-striped condensed-table'>
+ <tr>
+ <th>Prefix</th>
+ <th>Origin AS</th>
+ <th>Validation Status</th>
+ </tr>
+ {% for r in routes %}
+ <tr>
+ <td>{{ r.get_prefix_display }}</td>
+ <td>{{ r.asn }}</td>
+ <td>
+ <span class='label {{ r.status_label }}'>{{ r.status }}</span>
+ <a href='{{ r.get_absolute_url }}/roa/' help='display ROAs matching this prefix'>roas</a>
+ </td>
+ </tr>
+ {% endfor %}
+</table>
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/update_bpki_form.html b/rpkid/rpki/gui/app/templates/app/update_bpki_form.html
index b232c4e9..b232c4e9 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/update_bpki_form.html
+++ b/rpkid/rpki/gui/app/templates/app/update_bpki_form.html
diff --git a/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html
new file mode 100644
index 00000000..76c66775
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html
@@ -0,0 +1,20 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Delete User</h1>
+</div>
+
+<div class='alert-message block-message warning'>
+ <p><strong>Please confirm</strong> that you would like to delete the following user account.
+ <h2>{{ object.handle }}</h2>
+ <div class='alert-actions'>
+ <form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ {{ form }}
+ <input class='btn danger' value='Delete' type='submit'>
+ <a class='btn' href='{% url rpki.gui.app.views.user_list %}'>Cancel</a>
+ </form>
+ </div>
+</div>
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/user_create_form.html b/rpkid/rpki/gui/app/templates/app/user_create_form.html
new file mode 100644
index 00000000..1a07402f
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/user_create_form.html
@@ -0,0 +1,16 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Create User</h1>
+</div>
+
+<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}">
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='actions'>
+ <input class='btn primary' type="submit" value="Create">
+ <a class='btn' href="{% url rpki.gui.app.views.child_list %}">Cancel</a>
+ </div>
+</form>
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/app/user_edit_form.html b/rpkid/rpki/gui/app/templates/app/user_edit_form.html
new file mode 100644
index 00000000..59fc01c2
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/user_edit_form.html
@@ -0,0 +1,16 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Edit User: {{ object.username }}</h1>
+</div>
+
+<form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ {% include "app/bootstrap_form.html" %}
+ <div class='actions'>
+ <input class='btn primary' type='submit' value='Save'>
+ <a class='btn' href='{% url rpki.gui.app.views.user_list %}'>Cancel</a>
+ </div>
+</form>
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/user_list.html b/rpkid/rpki/gui/app/templates/app/user_list.html
new file mode 100644
index 00000000..804e94f0
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/user_list.html
@@ -0,0 +1,29 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Users</h1>
+</div>
+
+<table class='zebra-striped'>
+ <tr>
+ <th>Username</th>
+ <th>Email</th>
+ <th>Action</th>
+ </tr>
+ {% for u in users %}
+ <tr>
+ <td>{{ u.0.handle }}</td>
+ <td>{{ u.1.email }}</td>
+ <td>
+ <a class='btn small' href='{{ u.0.get_absolute_url }}/edit'>Edit</a>
+ <a class='btn small danger' href='{{ u.0.get_absolute_url }}/delete'>Delete</a>
+ </td>
+ </tr>
+ {% endfor %}
+</table>
+
+<div class='actions'>
+ <a class='btn' href="{% url rpki.gui.app.views.user_create %}" title="create a new locally hosted resource handle">Create</a>
+</div>
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/base.html b/rpkid/rpki/gui/app/templates/base.html
index d6c859f2..ac8abd17 100644
--- a/rpkid/rpki/gui/app/templates/base.html
+++ b/rpkid/rpki/gui/app/templates/base.html
@@ -1,36 +1,44 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
<html>
-<head>
- <title>{% block title %}RPKI{% endblock %}</title>
- {% block head %}{% endblock %}
- <style type="text/css">
- #header { background-color: #00ccff; border-style: solid; border-width: thin; padding-left:2em }
- #sidebar { background-color: #dddddd; border-style: none solid solid; border-width: thin; float:left; min-width:9em }
- #content { float:left; margin-left:1em }
- ul.compact {list-style:none inside; margin-left:1em; padding-left:0}
- table { border: solid 1px; border-collapse: collapse }
- th { border: solid 1px; padding: 1em }
- td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em }
- {% block css %}{% endblock %}
- </style>
-</head>
-<body>
- <div id="header">
- {% if user.is_authenticated %}
- <span style="float: right; font-size: 80%;">Logged in as {{ user }} |
- {% if user.is_staff %}<a href="/admin/">admin</a> |{% endif %}
- <a href="{% url django.contrib.auth.views.logout %}">Log Out</a></span>
- {% else %}
- <span style="float: right; font-size: 80%;"><a href="{% url django.contrib.auth.views.login %}">Log In</a></span>
- {% endif %}
- <h1>RPKI Portal GUI</h1>
- </div>
+ <head>
+ <meta name='Content-Type' content='text/html; charset=UTF-8'>
+ <title>{% block title %}RPKI{% endblock %}</title>
+ {% block head %}{% endblock %}
+ <link rel="stylesheet" href="/site_media/css/bootstrap.min.css">
+ <style type="text/css">
+ body { padding-top: 50px; }
+ {% block css %}{% endblock %}
+ </style>
+ </head>
+ <body>
+ <!-- TOP BAR -->
+ <div class="topbar">
+ <div class="topbar-inner">
+ <div class="container">
+ <h3><a href="#">rpki.net</a></h3>
- <div id='sidebar'>
- {% block sidebar %}{% endblock %}
- </div>
+ {% if user.is_authenticated %}
+ <ul class='nav'>
+ <li><p>Logged in as {{ user }}</li>
+ <li><a href="{% url django.contrib.auth.views.logout %}">Log Out</a></li>
+ </ul>
+ {% endif %}
- <div id="content">
- {% block content %}{% endblock %}
- </div>
-</body>
+ </div>
+ </div>
+ </div><!-- topbar -->
+
+ <!-- MAIN CONTENT -->
+ <div class="container-fluid">
+ <div class='content'>
+ {% block content %}{% endblock %}
+ </div><!-- /content -->
+
+ <div class="sidebar">
+ {% block sidebar %}{% endblock %}
+ </div><!-- /sidebar -->
+ </div><!-- /container-fluid -->
+
+ </body>
</html>
diff --git a/rpkid/rpki/gui/app/templates/registration/login.html b/rpkid/rpki/gui/app/templates/registration/login.html
index f99e9a25..27ad21cf 100644
--- a/rpkid/rpki/gui/app/templates/registration/login.html
+++ b/rpkid/rpki/gui/app/templates/registration/login.html
@@ -3,24 +3,33 @@
{% block content %}
{% if form.errors %}
-<p>Your username and password didn't match. Please try again.</p>
+<div class='alert-message error'>
+ <p>Your username and password didn't match. Please try again.</p>
+</div>
{% endif %}
-<form method="post" action="{% url django.contrib.auth.views.login %}">{% csrf_token %}
-<table>
-<tr>
- <td>{{ form.username.label_tag }}</td>
- <td>{{ form.username }}</td>
-</tr>
-<tr>
- <td>{{ form.password.label_tag }}</td>
- <td>{{ form.password }}</td>
-</tr>
-</table>
-
-<input type="submit" value="login" />
-<input type="hidden" name="next" value="{{ next }}" />
+<form method="post" action="{% url django.contrib.auth.views.login %}">
+ {% csrf_token %}
+
+ <div class="clearfix">
+ {{ form.username.label_tag }}
+ <div class="input">
+ {{ form.username }}
+ </div>
+ </div>
+
+ <div class="clearfix">
+ {{ form.password.label_tag }}
+ <div class="input">
+ {{ form.password }}
+ </div>
+ </div>
+
+ <div class="actions">
+ <input type="submit" value="Login" class="btn primary" />
+ </div>
+
+ <input type="hidden" name="next" value="{{ next }}" />
</form>
{% endblock %}
-
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html b/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html
deleted file mode 100644
index 204a6677..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html
+++ /dev/null
@@ -1,93 +0,0 @@
-{% extends "base.html" %}
-
-{% block css %}
-table { border-collapse: collapse }
-th { border: solid 1px; padding: 1em }
-td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em }
-{% endblock %}
-
-{% block sidebar %}
-<ul class='compact'>
- <li> <a href="{{asn.get_absolute_url}}/allocate">give to child</a></li>
-</ul>
-{% endblock %}
-
-{% block content %}
-
-<p id='breadcrumb'>
-<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> &gt; AS View &gt; {{ asn }}
-</p>
-
-<h1>AS View</h1>
-
-<table>
- <tr> <td>ASN:</td><td>{{ asn }}</td> </tr>
- {% if asn.parent %}
- <tr>
- <td>Suballocated from:</td>
- <td><a href="{{ asn.parent.get_absolute_url }}">{{ asn.parent }}</a></td>
- </tr>
- {% endif %}
- <tr>
- <td>Received from:</td>
- <td>
- {% for p in parent %}
- <a href="{{ p.get_absolute_url }}">{{ p.handle }}</a>
- {% endfor %}
- </td>
- </tr>
- <tr><td>Validity:</td><td>{{ asn.from_cert.all.0.not_before }} - {{ asn.from_cert.all.0.not_after }} </td></tr>
-
- {% if asn.allocated %}
- <tr><td>Allocated:</td><td><a href="{{asn.allocated.get_absolute_url}}">{{asn.allocated.handle}}</a></td></tr>
- {% endif %}
-</table>
-
-{% if asn.children.count %}
-<h2>Suballocations</h2>
-
-<ul>
-{% for subaddr in asn.children.all %}
-<li><a href="{{ subaddr.get_absolute_url }}">{{ subaddr }}</a>
-{% endfor %}
-</ul>
-
-{% endif %}
-
-{% if roas %}
-<h2>ROAs</h2>
-<table>
- <tr><th>Prefixes</th></tr>
- {% for r in roas %}
- <tr>
- <td style='text-align: left'>
- <ul>
- {% for p in r.from_roa_request.all %}
- <li><a href="{{ p.prefix.get_absolute_url }}">{{ p.prefix }}</a>
- {% endfor %}
- </ul>
- </td>
- </tr>
- {% endfor %}
- </ul>
-</table>
-{% endif %} <!-- roas -->
-
-{% if unallocated %}
-<h2>Unallocated</h2>
-<ul>
-{% for u in unallocated %}
-<li>{{ u }}
-{% endfor %}
-</ul>
-{% endif %}
-
-{% if form %}
-<h2>Edit</h2>
-<form method="POST" action="{{ request.get_full_path }}">{% csrf_token %}
- {{ form.as_p }}
- <input type="submit">
-</form>
-{% endif %}
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_form.html b/rpkid/rpki/gui/app/templates/rpkigui/child_form.html
deleted file mode 100644
index 0e5a5ac2..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/child_form.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-<p id='breadcrumb'>
-<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> &gt;
-<a href="{{ child.get_absolute_url }}">{{ child.handle }}</a> &gt; Edit
-</p>
-
-<h1>Edit Child</h1>
-
-<p><span style='font-weight:bold'>Child:</span> {{ child.handle }}</p>
-
-<form method='POST' action='{{ request.get_full_path }}'>
- {% csrf_token %}
- {{ form.as_p }}
- <input type='submit'/ value='Save'>
-</form>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_view.html b/rpkid/rpki/gui/app/templates/rpkigui/child_view.html
deleted file mode 100644
index 474798ce..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/child_view.html
+++ /dev/null
@@ -1,60 +0,0 @@
-{% extends "base.html" %}
-
-{% block sidebar %}
-<ul class='compact'>
- <li><a href="{{ child.get_absolute_url }}/edit">edit</a></li>
- <li><a href="{{ child.get_absolute_url }}/export" title="download XML response file to return to child">export child response</a></li>
- <li><a href="{{ child.get_absolute_url }}/export_repo" title="download XML response to publication client request">export repo response</a></li>
- <li><a href="{{ child.get_absolute_url }}/delete" title="remove this handle as a RPKI child">delete</a></li>
- <li><a href="{{ child.get_absolute_url }}/destroy" title="completely remove a locally hosted resource handle and gui account">destroy</a></li>
-</ul>
-{% endblock %}
-
-{% block content %}
-<p id='breadcrumb'>
-<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> &gt; {{ child.handle }}
-</p>
-
-<h1>Child View</h1>
-
-<table>
- <tr>
- <td>Child</td>
- <td>{{ child.handle }}</td>
- </tr>
- <tr>
- <td>Valid until</td>
- <td>{{ child.valid_until }}</td>
- </tr>
-</table>
-
-<h2>Delegated Addresses</h2>
-{% if child.address_range.all %}
-<ul>
-{% for a in child.address_range.all %}
-<li><a href="{{ a.get_absolute_url }}">{{ a }}</a></li>
-{% endfor %}
-</ul>
-{% else %}
-<p style='font-style:italic'>none</p>
-{% endif %}
-
-<h2>Delegated ASNs</h2>
-{% if child.asn.all %}
-<ul>
-{% for a in child.asn.all %}
-<li><a href="{{ a.get_absolute_url }}">{{ a }}</a></li>
-{% endfor %}
-</ul>
-{% else %}
-<p style='font-style:italic'>none</p>
-{% endif %}
-
-{% if form %}
-<form method='POST' action='{{ request.get_full_path }}'>
- {% csrf_token %}
- <input type='submit'/>
-</form>
-{% endif %}
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html b/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html
deleted file mode 100644
index 85c85ed5..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}">
- {% csrf_token %}
- <table>
-{{ form.as_table }}
-</table>
-<input type="submit" value="Create">
-</form>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html b/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html
deleted file mode 100644
index e21eb4eb..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html
+++ /dev/null
@@ -1,193 +0,0 @@
-{% extends "base.html" %}
-
-{% block css %}
-table { border-collapse: collapse }
-th { border: solid 1px; padding: 1em }
-td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em }
-h2 { text-align:center; background-color:#dddddd }
-{% endblock %}
-
-{% block sidebar %}
-<ul class='compact'>
- <li><a href="#parents">parents</a></li>
- <li><a href="#children">children</a></li>
- <li><a href="#roas">roas</a></li>
- <li><a href="#ghostbusters">ghostbusters</a></li>
- <li><a href="#unallocated">unallocated</a></li>
-</ul>
-
-<ul class='compact'>
- <li><a href="{% url rpki.gui.app.views.conf_export %}" title="download XML identity to send to parent">export identity</a></li>
- <li><a href="{% url rpki.gui.app.views.update_bpki %}" title="renew all BPKI certificates">update bpki</a></li>
- <li><a href="{% url rpki.gui.app.views.conf_list %}" title="select a different resource handle to manage">select identity</a></li>
-</ul>
-
-<ul class='compact'>
- <li><a href="{% url rpki.gui.app.views.child_wizard %}" title="create a new locally hosted resource handle">create child wizard</a></li>
-</ul>
-
-<ul class='compact'>
- <li><a href="{% url rpki.gui.app.views.import_parent %}" title="upload XML response from remote parent">import parent</a></li>
- <li><a href="{% url rpki.gui.app.views.import_repository %}" title="upload XML response from remote repository">import repository</a></li>
-</ul>
-
-<ul class='compact'>
- <li><a href="{% url rpki.gui.app.views.import_child %}" title="import a new child's identity.xml file">import child</a></li>
- <li><a href="{% url rpki.gui.app.views.import_pubclient %}" title="import XML request from a publication client">import pubclient</a></li>
-</ul>
-
-<ul class='compact'>
- <li><a href="{% url rpki.gui.app.views.refresh %}">refresh</a></li>
-</ul>
-{% endblock %}
-
-{% block content %}
-
-<p id='breadcrumb'>{{ request.session.handle }} &gt; Dashboard</p>
-
-<h1>Dashboard</h1>
-
-<div class='separator'>
-<a name='parents'><h2>Parents</h2></a>
-
-{% if request.session.handle.parents.all %}
-<ul>
-{% for parent in request.session.handle.parents.all %}
-<li><a href="{{ parent.get_absolute_url }}">{{ parent.handle }}</a>
-<p>
-<table>
-<tr><th>Accepted Resource</th><th>Not Before</th><th>Not After</th></tr>
-{% for cert in parent.resources.all %}
-
-{% for asn in cert.asn.all %}
-<tr><td style='text-align:left'><a href="{{ asn.get_absolute_url }}">{{ asn }}</a></td>
-<td>{{cert.not_before}}</td>
-<td>{{cert.not_after}}</td>
-</tr>
-{% endfor %}
-
-{% for address in cert.address_range.all %}
-<tr>
- <td style='text-align: left'><a href="{{ address.get_absolute_url }}">{{ address }}</a></td>
- <td>{{cert.not_before}}</td>
- <td>{{cert.not_after}}</td>
-</tr>
-{% endfor %}
-
-{% endfor %} <!--certs-->
-</table>
-
-{% endfor %}
-</ul>
-{% else %}
-<p style='font-style:italic'>none</p>
-{% endif %}
-
-</div><!--parents-->
-
-<div class='separator'>
- <a name='children'><h2>Children</h2></a>
-
-{% if request.session.handle.children.all %}
-<ul>
-{% for child in request.session.handle.children.all %}
-<li><a href="{% url rpki.gui.app.views.child_view child.handle %}">{{ child.handle }}</a>, valid until {{ child.valid_until }}
-{% if child.address_range.count or child.asn.count %}
-<p>Delegated resources:
-<ul>
-{% for asn in child.asn.all %}
-<li><a href="{{ asn.get_absolute_url }}">{{ asn }}</a></li>
-{% endfor %}
-{% for address in child.address_range.all %}
-<li><a href="{{ address.get_absolute_url}}">{{ address }}</a></li>
-{% endfor %}
-</ul>
-{% endif %}
-</li>
-{% endfor %}
-</ul>
-<!--
-<a href="/myrpki/import/child">[add]</a>
--->
-{% else %}
-<p style='font-style:italic'>none</p>
-{% endif %}
-
-<p>
-Export resources delegated to children (csv): <a href="{% url rpki.gui.app.views.download_asns request.session.handle %}" title="ASs delegated to children">asns</a> |
-<a href="{% url rpki.gui.app.views.download_prefixes request.session.handle %}" title="prefixes delegated to children">prefixes</a>
-
-</div>
-
-<div class='separator'> <!-- ROAs -->
- <a name='roas'><h2>ROA Requests</h2></a>
-
- {% if request.session.handle.roas.all %}
- <table>
- <tr> <th>Prefix</th> <th>ASN</th> </tr>
-
- {% for roa in request.session.handle.roas.all %}
- <tr>
- <td style='text-align: left'>
- <ul style='list-style-position: outside'>
- {% for req in roa.from_roa_request.all %}
- <li><a href="{{ req.prefix.get_absolute_url }}">{{ req.as_roa_prefix }}</a>
- {% endfor %}
- </ul>
- </td>
- <td>{{ roa.asn }}</td>
- </tr>
- {% endfor %}
- </table>
- {% else %}
- <p style='font-style:italic'>none</p>
- {% endif %}
-
- <p><a href="{% url rpki.gui.app.views.download_roas request.session.handle %}">export (csv)</a>
-</div><!-- roas -->
-
-<div class='separator'><!-- ghostbusters -->
-<a name='ghostbusters'><h2>Ghostbuster Requests</h2></a>
- {% if request.session.handle.ghostbusters.all %}
- <ul>
- {% for gbr in request.session.handle.ghostbusters.all %}
- <li><a href="{{ gbr.get_absolute_url }}">{{ gbr.full_name }}</a> |
- <a href="{{ gbr.get_absolute_url }}/edit">edit</a> |
- <a href="{{ gbr.get_absolute_url }}/delete">delete</a>
- </li>
- {% endfor %}
- {% else %}
-<p style='font-style:italic'>none</p>
- {% endif %}
-</ul>
-<p><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></p>
-</div>
-
-<div class='separator'>
-<a name='unallocated'><h2>Unallocated Resources</h2></a>
- {% if asns or ars %}
-
- {% if asns %}
- <ul>
- {% for asn in asns %}
- <li>{{ asn.as_ul|safe }}
- {% endfor %} <!-- ASNs -->
- </ul>
- {% endif %}
-
- {% if ars %}
- <ul>
- {% for addr in ars %}
- <li>{{ addr.as_ul|safe }}
- {% endfor %} <!-- addrs -->
- </ul>
- {% endif %}
-
- {% else %}
-<p style='font-style:italic'>none</p>
- {% endif %}
-
- </ul>
-</div>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html
deleted file mode 100644
index 81f4c093..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "rpkigui/ghostbuster_detail.html" %}
-
-{% block extra %}
-
-<p>
-Please confirm that you really want to delete this object by clicking Delete.
-</p>
-
-<form method=POST action='{{ request.get_full_path }}'>
- {% csrf_token %}
- <input type='submit' value='Delete' />
-</form>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html
deleted file mode 100644
index 4a9ed73a..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html
+++ /dev/null
@@ -1,69 +0,0 @@
-{% extends "base.html" %}
-
-{% block css %}
-td { padding-right: 1em }
-td.label { font-weight:bold }
-{% endblock %}
-
-{% block sidebar %}
-<ul class='compact'>
- <li><a href='{{ object.get_absolute_url }}/edit'>edit</a></li>
- <li><a href='{{ object.get_absolute_url }}/delete'>delete</a></li>
-</ul>
-{% endblock %}
-
-{% block content %}
-<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> &gt; <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbuster Request</a> &gt; {{ object.full_name }}</p>
-
-<h1>Ghostbuster View</h1>
-
-<table>
- <tr><td class='label'>Full Name</td><td>{{ object.full_name }}</td></tr>
-
- {% if object.honorific_prefix %}
- <tr><td class='label'>Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr>
- {% endif %}
-
- {% if object.organization %}
- <tr><td class='label'>Organization</td><td>{{ object.organization }}</td></tr>
- {% endif %}
-
- {% if object.telephone %}
- <tr><td class='label'>Telephone</td><td>{{ object.telephone }}</td></tr>
- {% endif %}
-
- {% if object.email_address %}
- <tr><td class='label'>Email</td><td>{{ object.email_address }}</td></tr>
- {% endif %}
-
- {% if object.box %}
- <tr><td class='label'>P.O. Box</td><td>{{ object.box }}</td></tr>
- {% endif %}
-
- {% if object.extended %}
- <tr><td class='label'>Extended Address</td><td>{{ object.extended }}</td></tr>
- {% endif %}
-
- {% if object.street %}
- <tr><td class='label'>Street Address</td><td>{{ object.street }}</td></tr>
- {% endif %}
-
- {% if object.city %}
- <tr><td class='label'>City</td><td>{{ object.city }}</td></tr>
- {% endif %}
-
- {% if object.region %}
- <tr><td class='label'>Region</td><td>{{ object.region }}</td></tr>
- {% endif %}
-
- {% if object.code %}
- <tr><td class='label'>Postal Code</td><td>{{ object.code }}</td></tr>
- {% endif %}
-
- {% if object.country %}
- <tr><td class='label'>Country</td><td>{{ object.country }}</td></tr>
- {% endif %}
-
-</table>
-{% block extra %}{% endblock %}
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html
deleted file mode 100644
index 0d77d796..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{request.session.handle}}</a> &gt; <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbusters</a> &gt; Edit</p>
-
-<h1>Edit Ghostbuster Request</h1>
-
-<form action='{{ request.get_full_path }}' method='POST'>
- {% csrf_token %}
- <table>
-{{ form.as_table }}
-
-</table>
-<p></p><!-- add vertical space -->
- <input type='submit' value='Save' />
-</form>
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html
deleted file mode 100644
index 6890782d..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% extends "base.html" %}
-
-{% block sidebar %}
-<ul class='compact'>
- <li><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></li>
-</ul>
-{% endblock %}
-
-{% block content %}
-<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> &gt; Ghostbusters</p>
-
-<h1>Ghostbuster Requests</h1>
-
-{% if object_list %}
-<ul>
- {% for obj in object_list %}
- <li><a href="{{ obj.get_absolute_url }}">{{ obj.full_name }}</a> | <a href="{{obj.get_absolute_url}}/edit">edit</a> | <a href="{{obj.get_absolute_url}}/delete">delete</a></li>
- {% endfor %}
-</ul>
-{% else %}
-<p style='font-style:italic'>none</p>
-{% endif %}
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html
deleted file mode 100644
index acd6bf61..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}">
- {% csrf_token %}
- <table>
-{{ form.as_table }}
-</table>
-<input type="submit" value="Import">
-</form>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html
deleted file mode 100644
index acd6bf61..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}">
- {% csrf_token %}
- <table>
-{{ form.as_table }}
-</table>
-<input type="submit" value="Import">
-</form>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html
deleted file mode 100644
index acd6bf61..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}">
- {% csrf_token %}
- <table>
-{{ form.as_table }}
-</table>
-<input type="submit" value="Import">
-</form>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html b/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html
deleted file mode 100644
index 4209c537..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "rpkigui/parent_view.html" %}
-
-{% block form %}
-
-<form method="POST" action="{{ request.get_full_path }}">
-{% csrf_token %}
-{{ form }}
-<input type="submit" value="{{ submit_label }}">
-</form>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html b/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html
deleted file mode 100644
index a57bd888..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html
+++ /dev/null
@@ -1,38 +0,0 @@
-{% extends "base.html" %}
-
-{% block sidebar %}
-<ul class='compact'>
- <li><a href="{{ parent.get_absolute_url }}/delete">delete</a></li>
-</ul>
-{% endblock %}
-
-{% block content %}
-<p id='breadcrumb'>
-<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> &gt; Parent View &gt; {{ parent.handle }}
-</p>
-
-<h1>Parent View</h1>
-
-<p>Parent: {{ parent.handle }}
-
-<h2>Delegated Addresses</h2>
-<ul>
-{% for c in parent.resources.all %}
-{% for a in c.address_range.all %}
-<li><a href="{{ a.get_absolute_url }}">{{ a }}</a>
-{% endfor %}
-{% endfor %}
-</ul>
-
-<h2>Delegated ASNs</h2>
-<ul>
-{% for c in parent.resources.all %}
-{% for a in c.asn.all %}
-<li><a href="{{ a.get_absolute_url }}">{{ a }}</a>
-{% endfor %}
-{% endfor %}
-</ul>
-
-{% block form %}{% endblock %}
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html b/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html
deleted file mode 100644
index 6679eff9..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html
+++ /dev/null
@@ -1,96 +0,0 @@
-{% extends "base.html" %}
-
-{% block sidebar %}
-<ul class='compact'>
-{% if not addr.allocated %}
-<li><a href="{{addr.get_absolute_url}}/split">split</a></li>
-{% endif %}
-{% if not addr.roa_requests.all %}
-<li><a href="{{addr.get_absolute_url}}/allocate">give to child</a></li>
-{% endif %}
-{% if addr.is_prefix and not addr.allocated %}
-<li><a href="{{ addr.get_absolute_url }}/roa">roa</a></li>
-{% endif %}
-{% if not addr.allocated and addr.parent %}
-<li><a href="{{ addr.get_absolute_url }}/delete">delete</a></li>
-{% endif %}
-</ul>
-{% endblock %}
-
-{% block content %}
-<p id='breadcrumb'>
-<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> &gt; Prefix View &gt; {{ addr }}
-</p>
-
-<h1>Prefix View</h1>
-
-<table>
- <tr> <td>Range:</td><td>{{ addr }}</td> </tr>
- {% if addr.parent %}
- <tr>
- <td>Suballocated from:</td>
- <td><a href="{{ addr.parent.get_absolute_url }}">{{ addr.parent }}</a></td>
- </tr>
- {% endif %}
- <tr>
- <td>Received from:</td>
- <td>
- {% for p in parent %}
- <a href="{{ p.get_absolute_url }}">{{ p.handle }}</a>
- {% endfor %}
- </td>
- </tr>
- <tr><td>Validity:</td><td>{{ addr.from_cert.all.0.not_before }} - {{ addr.from_cert.all.0.not_after }} </td></tr>
-
- {% if addr.allocated %}
- <tr>
- <td>Allocated:</td>
- <td><a href="{{addr.allocated.get_absolute_url}}">{{ addr.allocated.handle }}</a></td>
- </tr>
- {% endif %}
-</table>
-
-{% if addr.children.count %}
-<h2>Suballocations</h2>
-<ul>
- {% for subaddr in addr.children.all %}
- <li><a href="{{ subaddr.get_absolute_url }}">{{ subaddr }}</a></li>
- {% endfor %}
-</ul>
-{% endif %} <!-- suballocations -->
-
-{% if addr.roa_requests.count %}
-<h2>ROA requests</h2>
-<table>
- <tr><th>ASN</th><th>Max Length</th></tr>
-
- {% for r in addr.roa_requests.all %}
- <tr>
- <td>{{ r.roa.asn }}</td>
- <td>{{ r.max_length }}</td>
- <td><a href="{{ r.get_absolute_url }}/delete">delete</a></td>
- </tr>
- {% endfor %}
-</table>
-{% endif %} <!-- roa requests -->
-
-{% if unallocated %}
-<h2>Unallocated</h2>
-<ul>
- {% for u in unallocated %}
- <li>{{ u }}</li>
- {% endfor %}
-</ul>
-{% endif %}
-
-{% if form %}
-<div style='background-color: #dddddd'>
-<h2>{{ form_title }}</h2>
-<form method="POST" action="{{ request.get_full_path }}">{% csrf_token %}
- {{ form.as_p }}
- <input type="submit">
-</form>
-</div>
-{% endif %} <!-- form -->
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html b/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html
deleted file mode 100644
index 7d5187d3..00000000
--- a/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-
-<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{request.session.handle}}</a> &gt; <a href="{{ object.prefix.get_absolute_url }}">{{ object.prefix }}</a> &gt; Delete ROA Request</p>
-
-<h1>Delete ROA Request</h1>
-
-<p>Please confirm that you would like to delete the following ROA request:</p>
-
-<table>
- <tr><td>AS</td> <td>{{ object.roa.asn }}</td></tr>
- <tr><td>Prefix</td> <td><a href="{{ object.prefix.get_absolute_url }}">{{ object.prefix }}</a></td></tr>
- <tr><td>Max Length</td><td>{{ object.max_length }}</td></tr>
-</table>
-
-<p></p><!--add some space-->
-
-<form method='POST' action='{{ request.get_full_path }}'>
-{% csrf_token %}
-<input type='submit' value='Delete'/>
-</form>
-
-{% endblock %}
diff --git a/rpkid/rpki/gui/app/templatetags/__init__.py b/rpkid/rpki/gui/app/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpkid/rpki/gui/app/templatetags/__init__.py
diff --git a/rpkid/rpki/gui/app/templatetags/app_extras.py b/rpkid/rpki/gui/app/templatetags/app_extras.py
new file mode 100644
index 00000000..acb17e14
--- /dev/null
+++ b/rpkid/rpki/gui/app/templatetags/app_extras.py
@@ -0,0 +1,13 @@
+from django import template
+
+register = template.Library()
+
+@register.simple_tag
+def verbose_name(obj):
+ "Return the model class' verbose name."
+ return obj._meta.verbose_name
+
+@register.simple_tag
+def verbose_name_plural(qs):
+ "Return the verbose name for the model class."
+ return qs.model._meta.verbose_name_plural
diff --git a/rpkid/rpki/gui/app/timestamp.py b/rpkid/rpki/gui/app/timestamp.py
new file mode 100644
index 00000000..959f2025
--- /dev/null
+++ b/rpkid/rpki/gui/app/timestamp.py
@@ -0,0 +1,25 @@
+# $Id$
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+#
+
+import models
+from datetime import datetime
+
+def update(name):
+ "Set the timestamp value for the given name to the current time."
+ q = models.Timestamp.objects.filter(name=name)
+ obj = q[0] if q else models.Timestamp(name=name)
+ obj.ts = datetime.utcnow()
+ obj.save()
diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py
index ae9352b1..7e2e9878 100644
--- a/rpkid/rpki/gui/app/urls.py
+++ b/rpkid/rpki/gui/app/urls.py
@@ -1,19 +1,19 @@
-# $Id$
-"""
-Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
+__version__ = '$Id$'
from django.conf.urls.defaults import *
from rpki.gui.app import views
@@ -23,44 +23,45 @@ urlpatterns = patterns('',
(r'^conf/export$', views.conf_export),
(r'^conf/list$', views.conf_list),
(r'^conf/select$', views.conf_select),
- (r'^parent/(?P<parent_handle>[^/]+)$', views.parent_view),
- (r'^parent/(?P<parent_handle>[^/]+)/delete$', views.parent_delete),
- (r'^child/(?P<child_handle>[^/]+)$', views.child_view),
- (r'^child/(?P<child_handle>[^/]+)/delete$', views.child_delete),
- (r'^child/(?P<child_handle>[^/]+)/edit$', views.child_edit),
- (r'^child/(?P<child_handle>[^/]+)/export$', views.export_child_response),
- (r'^child/(?P<child_handle>[^/]+)/export_repo$', views.export_child_repo_response),
- (r'^child/(?P<handle>[^/]+)/destroy$', views.destroy_handle),
- (r'^address/(?P<pk>\d+)$', views.address_view),
- (r'^address/(?P<pk>\d+)/split$', views.prefix_split_view),
- (r'^address/(?P<pk>\d+)/allocate$', views.prefix_allocate_view),
- (r'^address/(?P<pk>\d+)/roa$', views.prefix_roa_view),
- (r'^address/(?P<pk>\d+)/delete$', views.prefix_delete_view),
- (r'^asn/(?P<pk>\d+)$', views.asn_view),
- (r'^asn/(?P<pk>\d+)/allocate$', views.asn_allocate_view),
- (r'^gbr/$', views.ghostbusters_list),
+ (r'^parent/$', views.parent_list),
+ (r'^parent/import$', views.parent_import),
+ (r'^parent/(?P<pk>\d+)$', views.parent_detail),
+ (r'^parent/(?P<pk>\d+)/delete$', views.parent_delete),
+ (r'^parent/(?P<pk>\d+)/export$', views.parent_export),
+ (r'^child/$', views.child_list),
+ (r'^child/import$', views.child_import),
+ (r'^child/(?P<pk>\d+)$', views.child_view),
+ (r'^child/(?P<pk>\d+)/add_asn/$', views.child_add_asn),
+ (r'^child/(?P<pk>\d+)/add_address/$', views.child_add_address),
+ (r'^child/(?P<pk>\d+)/delete$', views.child_delete),
+ (r'^child/(?P<pk>\d+)/edit$', views.child_edit),
+ (r'^child/(?P<pk>\d+)/export$', views.child_response),
+ (r'^gbr/$', views.ghostbuster_list),
(r'^gbr/create$', views.ghostbuster_create),
(r'^gbr/(?P<pk>\d+)$', views.ghostbuster_view),
(r'^gbr/(?P<pk>\d+)/edit$', views.ghostbuster_edit),
(r'^gbr/(?P<pk>\d+)/delete$', views.ghostbuster_delete),
(r'^refresh$', views.refresh),
- (r'^roa/(?P<pk>\d+)$', views.roa_view),
- (r'^roareq/(?P<pk>\d+)$', views.roa_request_view),
- (r'^roareq/(?P<pk>\d+)/delete$', views.roa_request_delete_view),
- (r'^demo/down/asns/(?P<self_handle>[^/]+)$', views.download_asns),
- (r'^demo/down/prefixes/(?P<self_handle>[^/]+)$', views.download_prefixes),
- (r'^demo/down/roas/(?P<self_handle>[^/]+)$', views.download_roas),
- (r'^demo/login', views.login),
- (r'^demo/myrpki-xml/(?P<self_handle>[^/]+)$', views.myrpki_xml),
- (r'^demo/parent-request/(?P<self_handle>[^/]+)$', views.parent_request),
- (r'^demo/repository-request/(?P<self_handle>[^/]+)$', views.repository_request),
- (r'^import_child$', views.import_child),
- (r'^import_parent$', views.import_parent),
- (r'^import_pubclient$', views.import_pubclient),
- (r'^import_repository$', views.import_repository),
-# (r'^initialize$', views.initialize),
- (r'^child_wizard$', views.child_wizard),
- (r'^update_bpki', views.update_bpki),
+ (r'^client/$', views.client_list),
+ (r'^client/import$', views.client_import),
+ (r'^client/(?P<pk>\d+)$', views.client_detail),
+ (r'^client/(?P<pk>\d+)/delete$', views.client_delete),
+ (r'^client/(?P<pk>\d+)/export$', views.client_export),
+ (r'^repo/$', views.repository_list),
+ (r'^repo/import$', views.repository_import),
+ (r'^repo/(?P<pk>\d+)$', views.repository_detail),
+ (r'^repo/(?P<pk>\d+)/delete$', views.repository_delete),
+ (r'^roa/$', views.roa_list),
+ (r'^roa/create$', views.roa_create),
+ (r'^roa/confirm$', views.roa_create_confirm),
+ (r'^roa/(?P<pk>\d+)$', views.roa_detail),
+ (r'^roa/(?P<pk>\d+)/delete$', views.roa_delete),
+ (r'^routes/$', views.route_view),
+ (r'^routes/(?P<pk>\d+)$', views.route_detail),
+ (r'^routes/(?P<pk>\d+)/roa/$', views.route_roa_list),
+ (r'^user/$', views.user_list),
+ (r'^user/create$', views.user_create),
+ (r'^user/(?P<pk>\d+)$', views.user_detail),
+ (r'^user/(?P<pk>\d+)/delete$', views.user_delete),
+ (r'^user/(?P<pk>\d+)/edit$', views.user_edit),
)
-
-# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py
index 0fb34525..6ba6f1c4 100644
--- a/rpkid/rpki/gui/app/views.py
+++ b/rpkid/rpki/gui/app/views.py
@@ -1,873 +1,1025 @@
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
-# $Id$
"""
-Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
+This module contains the view functions implementing the web portal
+interface.
+
"""
-from __future__ import with_statement
+__version__ = '$Id$'
-import email.message, email.utils, mailbox
-import os, os.path
-import sys, tempfile
+import os
+import os.path
+from tempfile import NamedTemporaryFile
from django.contrib.auth.decorators import login_required
-from django.contrib import auth
from django.shortcuts import get_object_or_404, render_to_response
from django.utils.http import urlquote
from django.template import RequestContext
from django import http
from django.views.generic.list_detail import object_list, object_detail
-from django.views.generic.create_update import delete_object, update_object, create_object
+from django.views.generic.create_update import delete_object
from django.core.urlresolvers import reverse
+from django.contrib.auth.models import User
-from rpki.gui.app import models, forms, glue, misc, AllocationTree, settings
-from rpki.gui.app.asnset import asnset
+from rpki.irdb import Zookeeper, ChildASN, ChildNet
+from rpki.gui.app import models, forms, glue, range_list
+from rpki.resource_set import (resource_range_as, resource_range_ipv4,
+ resource_range_ipv6, roa_prefix_ipv4)
+from rpki.exceptions import BadIPResource
+from rpki import sundial
-debug = False
+from rpki.gui.cacheview.models import ROAPrefixV4, ROAPrefixV6, ROA
-def my_login_required(f):
- """
- A version of django.contrib.auth.decorators.login_required
- that will fail instead of redirecting to the login page when
- the user is not logged in.
- For use with the rpkidemo service URLs where we want to detect
- failure to log in. Otherwise django will return code 200 with
- the login form, and fools rpkidemo.
+def superuser_required(f):
+ """Decorator which returns HttpResponseForbidden if the user does
+ not have superuser permissions.
+
"""
- def wrapped(request, *args, **kwargs):
- if not request.user.is_authenticated():
+ @login_required
+ def _wrapped(request, *args, **kwargs):
+ if not request.user.is_superuser:
return http.HttpResponseForbidden()
return f(request, *args, **kwargs)
+ return _wrapped
+
- return wrapped
+# FIXME This method is included in Django 1.3 and can be removed when Django
+# 1.2 is out of its support window.
+def render(request, template, context):
+ """
+ https://docs.djangoproject.com/en/1.3/topics/http/shortcuts/#render
+
+ """
+ return render_to_response(template, context,
+ context_instance=RequestContext(request))
-# For each type of object, we have a detail view, a create view and
-# an update view. We heavily leverage the generic views, only
-# adding our own idea of authorization.
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
def wrapped_fn(request, *args, **kwargs):
if 'handle' not in request.session:
if request.user.is_superuser:
conf = models.Conf.objects.all()
else:
- conf = models.Conf.objects.filter(owner=request.user)
+ conf = models.Conf.objects.filter(handle=request.user.username)
+
if conf.count() == 1:
- handle = conf[0]
+ request.session['handle'] = conf[0]
elif conf.count() == 0:
- return render('rpkigui/conf_empty.html', {}, request)
- #return http.HttpResponseRedirect('/myrpki/conf/add')
+ return render(request, 'app/conf_empty.html', {})
else:
# Should reverse the view for this instead of hardcoding
# the URL.
- return http.HttpResponseRedirect(
- reverse(conf_list) + '?next=' + urlquote(request.get_full_path()))
- request.session[ 'handle' ] = handle
+ url = '%s?next=%s' % (reverse(conf_list),
+ urlquote(request.get_full_path()))
+ return http.HttpResponseRedirect(url)
+
return f(request, *args, **kwargs)
return wrapped_fn
-def render(template, context, request):
- return render_to_response(template, context,
- context_instance=RequestContext(request))
@handle_required
-def dashboard(request, template_name='rpkigui/dashboard.html'):
- '''The user's dashboard.'''
- handle = request.session[ 'handle' ]
- # ... pick out data for the dashboard and return it
- # my parents
- # the resources that my parents have given me
- # the resources that I have accepted from my parents
- # my children
- # the resources that I have given my children
- # my roas
-
- # get list of ASNs used in my ROAs
- roa_asns = [r.asn for r in handle.roas.all()]
- asns=[]
- for a in models.Asn.objects.filter(from_cert__parent__in=handle.parents.all()):
- f = AllocationTree.AllocationTreeAS(a)
- if f.unallocated():
- asns.append(f)
-
- prefixes = []
- for p in models.AddressRange.objects.filter(from_cert__parent__in=handle.parents.all()):
- f = AllocationTree.AllocationTreeIP.from_prefix(p)
- if f.unallocated():
- prefixes.append(f)
-
- asns.sort(key=lambda x: x.range.min)
- prefixes.sort(key=lambda x: x.range.min)
-
- return render(template_name, { 'conf': handle, 'asns': asns, 'ars': prefixes }, request)
-
-@login_required
+def generic_import(request, queryset, configure, form_class=None,
+ template_name=None, post_import_redirect=None):
+ """
+ Generic view function for importing XML files used in the setup
+ process.
+
+ queryset
+ queryset containing all objects of the type being imported
+
+ configure
+ method on Zookeeper to invoke with the imported XML file
+
+ form_class
+ specifies the form to use for import. If None, uses the generic
+ forms.ImportForm.
+
+ template_name
+ path to the html template to use to render the form. If None, defaults
+ to "app/<model>_import_form.html", where <model> is introspected from
+ the "queryset" argument.
+
+ post_import_redirect
+ 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 = request.session['handle']
+ if template_name is None:
+ template_name = 'app/%s_import_form.html' % queryset.model.__name__.lower()
+ if form_class is None:
+ form_class = forms.ImportForm
+ if request.method == 'POST':
+ form = form_class(request.POST, request.FILES)
+ if form.is_valid():
+ tmpf = NamedTemporaryFile(prefix='import', suffix='.xml',
+ delete=False)
+ tmpf.write(form.cleaned_data['xml'].read())
+ tmpf.close()
+ z = Zookeeper(handle=conf.handle)
+ handle = form.cleaned_data.get('handle')
+ # CharField uses an empty string for the empty value, rather than
+ # None. Convert to none in this case, since configure_child/parent
+ # expects it.
+ if handle == '':
+ handle = None
+ # configure_repository returns None, so can't use tuple expansion
+ # here. Unpack the tuple below if post_import_redirect is None.
+ r = configure(z, tmpf.name, handle)
+ # force rpkid run now
+ z.synchronize(conf.handle)
+ os.remove(tmpf.name)
+ if post_import_redirect:
+ url = post_import_redirect
+ else:
+ _, handle = r
+ url = queryset.get(issuer=conf,
+ handle=handle).get_absolute_url()
+ return http.HttpResponseRedirect(url)
+ else:
+ form = form_class()
+
+ return render(request, template_name, {'form': form})
+
+
+@handle_required
+def dashboard(request):
+ log = request.META['wsgi.errors']
+ conf = request.session['handle']
+
+ used_asns = range_list.RangeList()
+
+ # asns used in my roas
+ qs = models.ROARequest.objects.filter(issuer=conf)
+ roa_asns = set((obj.asn for obj in qs))
+ used_asns.extend((resource_range_as(asn, asn) for asn in roa_asns))
+
+ # asns given to my children
+ child_asns = ChildASN.objects.filter(child__in=conf.children.all())
+ used_asns.extend((resource_range_as(obj.start_as, obj.end_as) for obj in child_asns))
+
+ # my received asns
+ asns = models.ResourceRangeAS.objects.filter(cert__parent__issuer=conf)
+ my_asns = range_list.RangeList([resource_range_as(obj.min, obj.max) for obj in asns])
+
+ unused_asns = my_asns.difference(used_asns)
+
+ used_prefixes = range_list.RangeList()
+ used_prefixes_v6 = range_list.RangeList()
+
+ # prefixes used in my roas
+ for obj in models.ROARequestPrefix.objects.filter(roa_request__issuer=conf,
+ version='IPv4'):
+ used_prefixes.append(obj.as_resource_range())
+
+ for obj in models.ROARequestPrefix.objects.filter(roa_request__issuer=conf,
+ version='IPv6'):
+ used_prefixes_v6.append(obj.as_resource_range())
+
+ # prefixes given to my children
+ for obj in ChildNet.objects.filter(child__in=conf.children.all(),
+ version='IPv4'):
+ used_prefixes.append(obj.as_resource_range())
+
+ for obj in ChildNet.objects.filter(child__in=conf.children.all(),
+ version='IPv6'):
+ used_prefixes_v6.append(obj.as_resource_range())
+
+ # my received prefixes
+ prefixes = models.ResourceRangeAddressV4.objects.filter(cert__parent__issuer=conf).all()
+ prefixes_v6 = models.ResourceRangeAddressV6.objects.filter(cert__parent__issuer=conf).all()
+ my_prefixes = range_list.RangeList([obj.as_resource_range() for obj in prefixes])
+ my_prefixes_v6 = range_list.RangeList([obj.as_resource_range() for obj in prefixes_v6])
+
+ unused_prefixes = my_prefixes.difference(used_prefixes)
+ unused_prefixes_v6 = my_prefixes_v6.difference(used_prefixes_v6)
+
+ return render(request, 'app/dashboard.html', {
+ 'conf': conf,
+ 'unused_asns': unused_asns,
+ 'unused_prefixes': unused_prefixes,
+ 'unused_prefixes_v6': unused_prefixes_v6,
+ 'asns': asns,
+ 'prefixes': prefixes,
+ 'prefixes_v6': prefixes_v6})
+
+
+@superuser_required
def conf_list(request):
"""Allow the user to select a handle."""
- if request.user.is_superuser:
- queryset = models.Conf.objects.all()
- else:
- queryset = models.Conf.objects.filter(owner=request.user)
+ queryset = models.Conf.objects.all()
return object_list(request, queryset,
- template_name='rpkigui/conf_list.html', template_object_name='conf', extra_context={ 'select_url' : reverse(conf_select) })
+ template_name='app/conf_list.html',
+ template_object_name='conf',
+ extra_context={'select_url': reverse(conf_select)})
+
-@login_required
+@superuser_required
def conf_select(request):
- '''Change the handle for the current session.'''
+ """Change the handle for the current session."""
if not 'handle' in request.GET:
return http.HttpResponseRedirect('/myrpki/conf/select')
handle = request.GET['handle']
next_url = request.GET.get('next', reverse(dashboard))
if next_url == '':
next_url = reverse(dashboard)
+ request.session['handle'] = get_object_or_404(models.Conf, handle=handle)
+ return http.HttpResponseRedirect(next_url)
- if request.user.is_superuser:
- conf = models.Conf.objects.filter(handle=handle)
- else:
- # since the handle is passed in as a parameter, need to verify that
- # the user is actually in the group
- conf = models.Conf.objects.filter(handle=handle,
- owner=request.user)
- if conf:
- request.session['handle'] = conf[0]
- return http.HttpResponseRedirect(next_url)
-
- return http.HttpResponseRedirect(reverse(conf_list) + '?next=' + next_url)
def serve_xml(content, basename):
- resp = http.HttpResponse(content , mimetype='application/xml')
- resp['Content-Disposition'] = 'attachment; filename=%s.xml' % (basename, )
+ """
+ Generate a HttpResponse object with the content type set to XML.
+
+ `content` is a string.
+
+ `basename` is the prefix to specify for the XML filename.
+
+ """
+ resp = http.HttpResponse(content, mimetype='application/xml')
+ resp['Content-Disposition'] = 'attachment; filename=%s.xml' % (basename,)
return resp
+
@handle_required
def conf_export(request):
"""Return the identity.xml for the current handle."""
- handle = request.session['handle']
- return serve_xml(glue.read_identity(handle.handle), 'identity')
+ conf = request.session['handle']
+ z = Zookeeper(handle=conf.handle)
+ xml = z.generate_identity()
+ return serve_xml(str(xml), '%s.identity' % conf.handle)
+
@handle_required
-def parent_view(request, parent_handle):
- """Detail view for a particular parent."""
- handle = request.session['handle']
- parent = get_object_or_404(handle.parents, handle__exact=parent_handle)
- return render('rpkigui/parent_view.html', { 'parent': parent }, request)
-
-def get_parents_or_404(handle, obj):
- '''Return the Parent object(s) that the given address range derives
- from, or raise a 404 error.'''
- cert_set = misc.top_parent(obj).from_cert.filter(parent__in=handle.parents.all())
- if cert_set.count() == 0:
- raise http.Http404, 'Object is not delegated from any parent'
- return [c.parent for c in cert_set]
-
-@handle_required
-def asn_view(request, pk):
- '''view/subdivide an asn range.'''
- handle = request.session['handle']
- obj = get_object_or_404(models.Asn.objects, pk=pk)
- # ensure this resource range belongs to a parent of the current conf
- parent_set = get_parents_or_404(handle, obj)
- roas = handle.roas.filter(asn=obj.lo) # roas which contain this asn
- unallocated = AllocationTree.AllocationTreeAS(obj).unallocated()
-
- return render('rpkigui/asn_view.html',
- { 'asn': obj, 'parent': parent_set, 'roas': roas,
- 'unallocated' : unallocated }, request)
-
-@handle_required
-def child_view(request, child_handle):
- '''Detail view of child for the currently selected handle.'''
- handle = request.session['handle']
- child = get_object_or_404(handle.children, handle__exact=child_handle)
-
- return render('rpkigui/child_view.html', { 'child': child }, request)
-
-@handle_required
-def child_edit(request, child_handle):
- """Edit the end validity date for a resource handle's child."""
- handle = request.session['handle']
- child = get_object_or_404(handle.children, handle__exact=child_handle)
+def parent_import(request):
+ conf = request.session['handle']
+ return generic_import(request, conf.parents, Zookeeper.configure_parent)
- if request.method == 'POST':
- form = forms.ChildForm(request.POST, request.FILES, instance=child)
- if form.is_valid():
- form.save()
- glue.configure_resources(request.META['wsgi.errors'], handle)
- return http.HttpResponseRedirect(child.get_absolute_url())
- else:
- form = forms.ChildForm(instance=child)
-
- return render('rpkigui/child_form.html', { 'child': child, 'form': form }, request)
-
-class PrefixView(object):
- '''Extensible view for address ranges/prefixes. This view can be
- subclassed to add form handling for editing the prefix.'''
-
- form = None
- form_title = None
-
- def __init__(self, request, pk, form_class=None):
- self.handle = request.session['handle']
- self.obj = get_object_or_404(models.AddressRange.objects, pk=pk)
- # ensure this resource range belongs to a parent of the current conf
- self.parent_set = get_parents_or_404(self.handle, self.obj)
- self.form_class = form_class
- self.request = request
-
- def __call__(self, *args, **kwargs):
- if self.request.method == 'POST':
- resp = self.handle_post()
- else:
- resp = self.handle_get()
-
- # allow get/post handlers to return a custom response
- if resp:
- return resp
-
- u = AllocationTree.AllocationTreeIP.from_prefix(self.obj).unallocated()
-
- return render('rpkigui/prefix_view.html',
- { 'addr': self.obj, 'parent': self.parent_set, 'unallocated': u,
- 'form': self.form,
- 'form_title': self.form_title if self.form_title else 'Edit' },
- self.request)
-
- def handle_get(self):
- '''Virtual method for extending GET handling. Default action is
- to call the form class constructor with the prefix object.'''
- if self.form_class:
- self.form = self.form_class(self.obj)
-
- def form_valid(self):
- '''Virtual method for handling a valid form. Called by the default
- implementation of handle_post().'''
- pass
-
- def handle_post(self):
- '''Virtual method for extending POST handling. Default implementation
- creates a form object using the form_class in the constructor and passing
- the prefix object. If the form's is_valid() method is True, it then
- invokes this class's form_valid() method.'''
- resp = None
- if self.form_class:
- self.form = self.form_class(self.obj, self.request.POST)
- if self.form.is_valid():
- resp = self.form_valid()
- return resp
-
-@handle_required
-def address_view(request, pk):
- return PrefixView(request, pk)()
-
-class PrefixSplitView(PrefixView):
- '''Class for handling the prefix split form.'''
-
- form_title = 'Split'
-
- def form_valid(self):
- r = misc.parse_resource_range(self.form.cleaned_data['prefix'])
- obj = models.AddressRange(lo=str(r.min), hi=str(r.max), parent=self.obj)
- obj.save()
- return http.HttpResponseRedirect(obj.get_absolute_url())
-
-@handle_required
-def prefix_split_view(request, pk):
- return PrefixSplitView(request, pk, form_class=forms.PrefixSplitForm)()
-
-class PrefixAllocateView(PrefixView):
- '''Class to handle the allocation to child form.'''
-
- form_title = 'Give to Child'
-
- def handle_get(self):
- self.form = forms.PrefixAllocateForm(
- self.obj.allocated.pk if self.obj.allocated else None,
- self.handle.children.all())
-
- def handle_post(self):
- self.form = forms.PrefixAllocateForm(None, self.handle.children.all(), self.request.POST)
- if self.form.is_valid():
- self.obj.allocated = self.form.cleaned_data['child']
- self.obj.save()
- glue.configure_resources(self.request.META['wsgi.errors'], self.handle)
- return http.HttpResponseRedirect(self.obj.get_absolute_url())
-
-@handle_required
-def prefix_allocate_view(request, pk):
- return PrefixAllocateView(request, pk)()
-
-def add_roa_requests(handle, prefix, asns, max_length):
- for asid in asns:
- if debug:
- print 'searching for a roa for AS %d containing %s-%d' % (asid, prefix, max_length)
- req_set = prefix.roa_requests.filter(roa__asn=asid, max_length=max_length)
- if not req_set:
- if debug:
- print 'no roa for AS %d containing %s-%d' % (asid, prefix, max_length)
-
- # find ROAs for prefixes derived from the same resource cert
- # as this prefix
- certs = misc.top_parent(prefix).from_cert.all()
- roa_set = handle.roas.filter(asn=asid, cert__in=certs)
-
- # FIXME: currently only creates a ROA/request for the first
- # resource cert, not all of them
- if roa_set:
- roa = roa_set[0]
- else:
- if debug:
- print 'creating new roa for AS %d containg %s-%d' % (asid, prefix, max_length)
- # no roa is present for this ASN, create a new one
- roa = models.Roa.objects.create(asn=asid, conf=handle,
- active=False, cert=certs[0])
- roa.save()
- req = models.RoaRequest.objects.create(prefix=prefix, roa=roa,
- max_length=max_length)
- req.save()
+@handle_required
+def parent_list(request):
+ """List view for parent objects."""
+ conf = request.session['handle']
+ return object_list(request, queryset=conf.parents.all(),
+ extra_context={'create_url': reverse(parent_import),
+ 'create_label': 'Import'})
-class PrefixRoaView(PrefixView):
- '''Class for handling the ROA creation form.'''
- form_title = 'Issue ROA'
+@handle_required
+def parent_detail(request, pk):
+ """Detail view for a particular parent."""
+ conf = request.session['handle']
+ return object_detail(request, conf.parents.all(), object_id=pk)
+
- def form_valid(self):
- asns = asnset(self.form.cleaned_data['asns'])
- add_roa_requests(self.handle, self.obj, asns, self.form.cleaned_data['max_length'])
- glue.configure_resources(self.request.META['wsgi.errors'], self.handle)
- return http.HttpResponseRedirect(self.obj.get_absolute_url())
-
@handle_required
-def prefix_roa_view(request, pk):
- return PrefixRoaView(request, pk, form_class=forms.PrefixRoaForm)()
+def parent_delete(request, pk):
+ conf = request.session['handle']
+ obj = get_object_or_404(conf.parents, pk=pk) # confirm permission
+ log = request.META['wsgi.errors']
+ form_class = forms.UserDeleteForm
+ if request.method == 'POST':
+ form = form_class(request.POST, request.FILES)
+ if form.is_valid():
+ z = Zookeeper(handle=conf.handle, logstream=log)
+ z.delete_parent(obj.handle)
+ z.synchronize()
+ return http.HttpResponseRedirect(reverse(parent_list))
+ else:
+ form = form_class()
+ return render(request, 'app/parent_detail.html',
+ {'object': obj, 'form': form, 'confirm_delete': True})
-class PrefixDeleteView(PrefixView):
- form_title = 'Delete'
- def form_valid(self):
- self.obj.delete()
- return http.HttpResponseRedirect(reverse(dashboard))
-
@handle_required
-def prefix_delete_view(request, pk):
- return PrefixDeleteView(request, pk, form_class=forms.PrefixDeleteForm)()
+def parent_export(request, pk):
+ """Export XML repository request for a given parent."""
+ conf = request.session['handle']
+ parent = get_object_or_404(conf.parents, pk=pk)
+ z = Zookeeper(handle=conf.handle)
+ xml = z.generate_repository_request(parent)
+ return serve_xml(str(xml), '%s.repository' % parent.handle)
+
@handle_required
-def roa_request_delete_view(request, pk):
- """
- Remove a ROA request from a particular prefix.
- """
+def child_import(request):
+ conf = request.session['handle']
+ return generic_import(request, conf.children, Zookeeper.configure_child)
- log = request.META['wsgi.errors']
- handle = request.session['handle']
- obj = get_object_or_404(models.RoaRequest.objects, pk=pk)
- prefix = obj.prefix
- # ensure this resource range belongs to a parent of the current conf
- parent_set = get_parents_or_404(handle, prefix)
- if request.method == 'POST':
- roa = obj.roa
- obj.delete()
- if not roa.from_roa_request.all():
- roa.delete()
- glue.configure_resources(log, handle)
- return http.HttpResponseRedirect(prefix.get_absolute_url())
+@handle_required
+def child_list(request):
+ """List of children for current user."""
+ conf = request.session['handle']
+ return object_list(request, queryset=conf.children.all(),
+ template_name='app/child_list.html',
+ extra_context={
+ 'create_url': reverse(child_import),
+ 'create_label': 'Import'})
- return render('rpkigui/roa_request_confirm_delete.html', { 'object': obj }, request)
@handle_required
-def asn_allocate_view(request, pk):
+def child_add_resource(request, pk, form_class, unused_list, callback,
+ template_name='app/child_add_resource_form.html'):
+ conf = request.session['handle']
+ child = models.Child.objects.get(issuer=conf, pk=pk)
log = request.META['wsgi.errors']
- handle = request.session['handle']
- obj = get_object_or_404(models.Asn.objects, pk=pk)
- # ensure this resource range belongs to a parent of the current conf
- parent_set = get_parents_or_404(handle, obj)
-
if request.method == 'POST':
- form = forms.PrefixAllocateForm(None, handle.children.all(), request.POST)
+ form = form_class(request.POST, request.FILES)
if form.is_valid():
- obj.allocated = form.cleaned_data['child']
- obj.save()
- glue.configure_resources(log, handle)
- return http.HttpResponseRedirect(obj.get_absolute_url())
+ callback(child, form)
+ Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now()
+ return http.HttpResponseRedirect(child.get_absolute_url())
else:
- form = forms.PrefixAllocateForm(obj.allocated.pk if obj.allocated else None,
- handle.children.all())
+ form = form_class()
- return render('rpkigui/asn_view.html', { 'form': form,
- 'asn': obj, 'form': form, 'parent': parent_set }, request)
+ return render(request, template_name,
+ {'object': child, 'form': form, 'unused': unused_list})
-# this is similar to handle_required, except that the handle is given in URL
-def handle_or_404(request, handle):
- "ensure the requested handle is available to this user"
- if request.user.is_superuser:
- conf_set = models.Conf.objects.filter(handle=handle)
- else:
- conf_set = models.Conf.objects.filter(owner=request.user, handle=handle)
- if not conf_set:
- raise http.Http404, 'resource handle not found'
- return conf_set[0]
-
-def serve_file(handle, fname, content_type, error_code=404):
- content, mtime = glue.read_file_from_handle(handle, fname)
- resp = http.HttpResponse(content , mimetype=content_type)
- resp['Content-Disposition'] = 'attachment; filename=%s' % (os.path.basename(fname), )
- resp['Last-Modified'] = email.utils.formatdate(mtime, usegmt=True)
- return resp
-@my_login_required
-def download_csv(request, self_handle, fname):
- conf = handle_or_404(request, self_handle)
- return serve_file(conf.handle, fname + '.csv', 'text/csv')
+def add_asn_callback(child, form):
+ asns = form.cleaned_data.get('asns')
+ r = resource_range_as.parse_str(asns)
+ child.asns.create(start_as=r.min, end_as=r.max)
-def download_asns(request, self_handle):
- return download_csv(request, self_handle, 'asns')
-def download_roas(request, self_handle):
- return download_csv(request, self_handle, 'roas')
+def child_add_asn(request, pk):
+ conf = request.session['handle']
+ get_object_or_404(models.Child, issuer=conf, pk=pk)
+ qs = models.ResourceRangeAS.objects.filter(cert__parent__issuer=conf)
+ return child_add_resource(request, pk, forms.AddASNForm(qs), [],
+ add_asn_callback)
-def download_prefixes(request, self_handle):
- return download_csv(request, self_handle, 'prefixes')
-def save_to_inbox(conf, request_type, content):
- """
- Save an incoming request from a client to the incoming mailbox
- for processing by a human.
- """
+def add_address_callback(child, form):
+ address_range = form.cleaned_data.get('address_range')
+ try:
+ r = resource_range_ipv4.parse_str(address_range)
+ version = 'IPv4'
+ except BadIPResource:
+ r = resource_range_ipv6.parse_str(address_range)
+ version = 'IPv6'
+ child.address_ranges.create(start_ip=str(r.min), end_ip=str(r.max),
+ version=version)
- user = conf.owner.all()[0]
- filename = request_type + '.xml'
- msg = email.message.Message()
- msg['Date'] = email.utils.formatdate()
- msg['From'] = '"%s" <%s>' % (conf.handle, user.email)
- msg['Message-ID'] = email.utils.make_msgid()
- msg['Subject'] = '%s for %s' % (filename, conf.handle)
- msg['X-rpki-self-handle'] = conf.handle
- msg['X-rpki-type'] = request_type
- msg.add_header('Content-Disposition', 'attachment', filename=filename)
- msg.set_type('application/x-rpki-setup')
- msg.set_payload(content)
+def child_add_address(request, pk):
+ conf = request.session['handle']
+ get_object_or_404(models.Child, issuer=conf, pk=pk)
+ qsv4 = models.ResourceRangeAddressV4.objects.filter(cert__parent__issuer=conf)
+ qsv6 = models.ResourceRangeAddressV6.objects.filter(cert__parent__issuer=conf)
+ return child_add_resource(request, pk,
+ forms.AddNetForm(qsv4, qsv6),
+ [],
+ callback=add_address_callback)
- box = mailbox.Maildir(settings.INBOX)
- box.add(msg)
- box.close()
- return http.HttpResponse()
+@handle_required
+def child_view(request, pk):
+ """Detail view of child for the currently selected handle."""
+ conf = request.session['handle']
+ child = get_object_or_404(conf.children.all(), pk=pk)
+ return render(request, 'app/child_detail.html',
+ {'object': child, 'can_edit': True})
-def get_response(conf, request_type):
- """
- If there is cached response for the given request type, simply
- return it. Otherwise, look in the outbox mailbox for a response.
- """
- filename = glue.confpath(conf.handle) + '/' + request_type + '.xml'
- if not os.path.exists(filename):
- box = mailbox.Maildir(settings.OUTBOX, factory=None)
- for key, msg in box.iteritems():
- # look for parent responses for this child
- if msg.get('x-rpki-type') == request_type and msg.get('x-rpki-self-handle') == conf.handle:
- with open(filename, 'w') as f:
- f.write(msg.get_payload())
- break
- else:
- return http.HttpResponse('no response found', status=503)
-
- box.remove(key) # remove the msg from the outbox
-
- return serve_file(conf.handle, request_type + '.xml', 'application/xml')
-
-@my_login_required
-def parent_request(request, self_handle):
- conf = handle_or_404(request, self_handle)
+@handle_required
+def child_edit(request, pk):
+ """Edit the end validity date for a resource handle's child."""
+ log = request.META['wsgi.errors']
+ conf = request.session['handle']
+ child = get_object_or_404(conf.children.all(), pk=pk)
+ form_class = forms.ChildForm(child)
if request.method == 'POST':
- return save_to_inbox(conf, 'identity', request.POST['content'])
+ form = form_class(request.POST, request.FILES)
+ if form.is_valid():
+ child.valid_until = sundial.datetime.fromdatetime(form.cleaned_data.get('valid_until'))
+ child.save()
+ # remove AS & prefixes that are not selected in the form
+ models.ChildASN.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('as_ranges')).delete()
+ models.ChildNet.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('address_ranges')).delete()
+ Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now()
+ return http.HttpResponseRedirect(child.get_absolute_url())
else:
- return get_response(conf, 'parent')
+ form = form_class(initial={
+ 'as_ranges': child.asns.all(),
+ 'address_ranges': child.address_ranges.all()})
-@my_login_required
-def repository_request(request, self_handle):
- conf = handle_or_404(request, self_handle)
+ return render(request, 'app/child_form.html',
+ {'object': child, 'form': form})
- if request.method == 'POST':
- return save_to_inbox(conf, 'repository', request.POST['content'])
- else:
- return get_response(conf, 'repository')
+
+@handle_required
+def roa_create(request):
+ """Present the user with a form to create a ROA.
+
+ Doesn't use the generic create_object() form because we need to
+ create both the ROARequest and ROARequestPrefix objects.
-@my_login_required
-def myrpki_xml(request, self_handle):
- """
- Handles POST of the myrpki.xml file for a given resource handle.
- As a special case for resource handles hosted by APNIC, stash a
- copy of the first xml message in the rpki inbox mailbox as this
- will be required to complete the parent-child setup.
"""
- conf = handle_or_404(request, self_handle)
- log = request.META['wsgi.errors']
+ conf = request.session['handle']
if request.method == 'POST':
- fname = glue.confpath(self_handle, '/myrpki.xml')
+ form = forms.ROARequest(request.POST, request.FILES, conf=conf)
+ if form.is_valid():
+ asn = form.cleaned_data.get('asn')
+ rng = form._as_resource_range() # FIXME calling "private" method
+ max_prefixlen = int(form.cleaned_data.get('max_prefixlen'))
+
+ # find list of matching routes
+ routes = []
+ match = roa_match(rng)
+ for route, roas in match:
+ validate_route(route, roas)
+ # tweak the validation status due to the presence of the
+ # new ROA. Don't need to check the prefix bounds here
+ # because all the matches routes will be covered by this
+ # new ROA
+ if route.status == 'unknown':
+ # if the route was previously unknown (no covering
+ # ROAs), then:
+ # if the AS matches, it is valid, otherwise invalid
+ if (route.asn != 0 and route.asn == asn and route.prefixlen() <= max_prefixlen):
+ route.status = 'valid'
+ route.status_label = 'success'
+ else:
+ route.status = 'invalid'
+ route.status_label = 'important'
+ elif route.status == 'invalid':
+ # if the route was previously invalid, but this new ROA
+ # matches the ASN, it is now valid
+ if route.asn != 0 and route.asn == asn and route.prefixlen() <= max_prefixlen:
+ route.status = 'valid'
+ route.status_label = 'success'
+
+ routes.append(route)
+
+ prefix = str(rng)
+ form = forms.ROARequestConfirm(initial={'asn': asn,
+ 'prefix': prefix,
+ 'max_prefixlen': max_prefixlen})
+ return render(request, 'app/roarequest_confirm_form.html',
+ {'form': form,
+ 'asn': asn,
+ 'prefix': prefix,
+ 'max_prefixlen': max_prefixlen,
+ 'routes': routes})
+ else:
+ form = forms.ROARequest()
- if not os.path.exists(fname):
- print >>log, 'Saving a copy of myrpki.xml for handle %s to inbox' % conf.handle
- save_to_inbox(conf, 'myrpki', request.POST['content'])
+ return render(request, 'app/roarequest_form.html', {'form': form})
- print >>log, 'writing %s' % fname
- with open(fname, 'w') as myrpki_xml :
- myrpki_xml.write(request.POST['content'])
- # FIXME: used to run configure_daemons here, but it takes too
- # long with many hosted handles. rpkidemo still needs a way
- # to do initial bpki setup with rpkid!
+@handle_required
+def roa_create_confirm(request):
+ conf = request.session['handle']
+ log = request.META['wsgi.errors']
- return http.HttpResponse('<p>success</p>')
+ if request.method == 'POST':
+ form = forms.ROARequestConfirm(request.POST, request.FILES)
+ if form.is_valid():
+ asn = form.cleaned_data.get('asn')
+ prefix = form.cleaned_data.get('prefix')
+ rng = glue.str_to_resource_range(prefix)
+ max_prefixlen = form.cleaned_data.get('max_prefixlen')
+
+ roarequests = models.ROARequest.objects.filter(issuer=conf,
+ asn=asn)
+ if roarequests:
+ # FIXME need to handle the case where there are
+ # multiple ROAs for the same AS due to prefixes
+ # delegated from different resource certs.
+ roa = roarequests[0]
+ else:
+ roa = models.ROARequest.objects.create(issuer=conf,
+ asn=asn)
+ v = 'IPv4' if isinstance(rng, resource_range_ipv4) else 'IPv6'
+ roa.prefixes.create(version=v, prefix=str(rng.min),
+ prefixlen=rng.prefixlen(),
+ max_prefixlen=max_prefixlen)
+ Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now()
+ return http.HttpResponseRedirect(reverse(roa_list))
else:
- return serve_file(self_handle, 'myrpki.xml', 'application/xml')
+ return http.HttpResponseRedirect(reverse(roa_create))
+
-def login(request):
+@handle_required
+def roa_list(request):
"""
- A version of django.contrib.auth.views.login that will return an
- error response when the user/password is bad. This is needed for
- use with rpkidemo to properly detect errors. The django login
- view will return 200 with the login page when the login fails,
- which is not desirable when using rpkidemo.
+ Display a list of ROARequestPrefix objects for the current resource
+ handle.
+
"""
- log = request.META['wsgi.errors']
- if request.method == 'POST':
- username = request.POST['username']
- password = request.POST['password']
- print >>log, 'login request for user %s' % username
- user = auth.authenticate(username=username, password=password)
- if user is not None and user.is_active:
- auth.login(request, user)
- return http.HttpResponse('<p>login succeeded</p>')
- print >>log, 'failed login attempt for user %s\n' % username
- return http.HttpResponseForbidden('<p>bad username or password</p>')
- else:
- return http.HttpResponse('<p>This should never been seen by a human</p>')
+ conf = request.session['handle']
+ qs = models.ROARequestPrefix.objects.filter(roa_request__issuer=conf).order_by('prefix')
+ return object_list(request, queryset=qs,
+ template_name='app/roa_request_list.html',
+ extra_context={'create_url': reverse(roa_create)})
+
@handle_required
-def roa_request_view(request, pk):
- """not yet implemented"""
- return
+def roa_detail(request, pk):
+ """Not implemented.
+
+ This is a placeholder so that
+ models.ROARequestPrefix.get_absolute_url works. The only reason it
+ exist is so that the /delete URL works.
+
+ """
+ pass
+
@handle_required
-def roa_view(request, pk):
- """not yet implemented"""
- return
+def roa_delete(request, pk):
+ """Handles deletion of a single ROARequestPrefix object.
+
+ Uses a form for double confirmation, displaying how the route
+ validation status may change as a result.
+
+ """
+
+ conf = request.session['handle']
+ obj = get_object_or_404(models.ROARequestPrefix.objects,
+ roa_request__issuer=conf, pk=pk)
+
+ if request.method == 'POST':
+ roa = obj.roa_request
+ obj.delete()
+ # if this was the last prefix on the ROA, delete the ROA request
+ if not roa.prefixes.exists():
+ roa.delete()
+ Zookeeper(handle=conf.handle).run_rpkid_now()
+ return http.HttpResponseRedirect(reverse(roa_list))
+
+ ### Process GET ###
+
+ match = roa_match(obj.as_resource_range())
+
+ roa_pfx = obj.as_roa_prefix()
+
+ pfx = 'prefixes' if isinstance(roa_pfx, roa_prefix_ipv4) else 'prefixes_v6'
+ args = {'%s__prefix_min' % pfx: roa_pfx.min(),
+ '%s__prefix_max' % pfx: roa_pfx.max(),
+ '%s__max_length' % pfx: roa_pfx.max_prefixlen}
+ # exclude ROAs which seem to match this request and display the result
+ routes = []
+ for route, roas in match:
+ qs = roas.exclude(asid=obj.roa_request.asn, **args)
+ validate_route(route, qs)
+ routes.append(route)
+
+ return render(request, 'app/roa_request_confirm_delete.html',
+ {'object': obj, 'routes': routes})
+
+
@handle_required
-def ghostbusters_list(request):
+def ghostbuster_list(request):
"""
Display a list of all ghostbuster requests for the current Conf.
+
"""
conf = request.session['handle']
+ qs = models.GhostbusterRequest.objects.filter(issuer=conf)
+ return object_list(request, queryset=qs)
- return object_list(request, queryset=conf.ghostbusters.all(), template_name='rpkigui/ghostbuster_list.html')
@handle_required
def ghostbuster_view(request, pk):
"""
Display an individual ghostbuster request.
+
"""
conf = request.session['handle']
+ qs = models.GhostbusterRequest.objects.filter(issuer=conf)
+ return object_detail(request, queryset=qs, object_id=pk,
+ extra_context={'can_edit': True})
- return object_detail(request, queryset=conf.ghostbusters.all(), object_id=pk, template_name='rpkigui/ghostbuster_detail.html')
@handle_required
def ghostbuster_delete(request, pk):
- conf = request.session['handle']
-
- # verify that the object is owned by this conf
- obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf)
+ """
+ Handle deletion of a GhostbusterRequest object.
- # modeled loosely on the generic delete_object() view.
+ """
+ conf = request.session['handle']
+ log = request.META['wsgi.errors']
+ form_class = forms.UserDeleteForm # FIXME
+ # Ensure the GhosbusterRequest object belongs to the current user.
+ obj = get_object_or_404(models.GhostbusterRequest, issuer=conf, pk=pk)
if request.method == 'POST':
- obj.delete()
- glue.configure_resources(request.META['wsgi.errors'], conf)
- return http.HttpResponseRedirect(reverse(ghostbusters_list))
+ form = form_class(request.POST, request.FILES)
+ if form.is_valid():
+ obj.delete()
+ Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now()
+ return http.HttpResponseRedirect(reverse(ghostbuster_list))
else:
- return render('rpkigui/ghostbuster_confirm_delete.html', { 'object': obj }, request)
+ form = form_class()
+ return render(request, 'app/ghostbusterrequest_detail.html',
+ {'object': obj, 'form': form, 'confirm_delete': True})
+
def _ghostbuster_edit(request, obj=None):
"""
Common code for create/edit.
+
"""
conf = request.session['handle']
- form_class = forms.GhostbusterForm(conf.parents.all())
+ form_class = forms.GhostbusterRequestForm
if request.method == 'POST':
- form = form_class(request.POST, request.FILES, instance=obj)
+ form = form_class(conf, request.POST, request.FILES, instance=obj)
if form.is_valid():
# use commit=False for the creation case, otherwise form.save()
# will fail due to schema constraint violation because conf is
# NULL
obj = form.save(commit=False)
- obj.conf = conf
+ obj.issuer = conf
+ obj.vcard = glue.ghostbuster_to_vcard(obj)
obj.save()
- glue.configure_resources(request.META['wsgi.errors'], conf)
+ Zookeeper(handle=conf.handle).run_rpkid_now()
return http.HttpResponseRedirect(obj.get_absolute_url())
else:
- form = form_class(instance=obj)
- return render('rpkigui/ghostbuster_form.html', { 'form': form }, request)
+ form = form_class(conf, instance=obj)
+ return render(request, 'app/ghostbuster_form.html',
+ {'form': form, 'object': obj})
+
@handle_required
def ghostbuster_edit(request, pk):
conf = request.session['handle']
# verify that the object is owned by this conf
- obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf)
+ obj = get_object_or_404(models.GhostbusterRequest, pk=pk, issuer=conf)
return _ghostbuster_edit(request, obj)
+
@handle_required
def ghostbuster_create(request):
return _ghostbuster_edit(request)
+
@handle_required
def refresh(request):
- "Query rpkid, update the db, and redirect back to the dashboard."
- glue.list_received_resources(request.META['wsgi.errors'], request.session['handle'])
- return http.HttpResponseRedirect(reverse(dashboard))
+ """
+ Query rpkid, update the db, and redirect back to the dashboard.
-@handle_required
-def import_parent(request):
- conf = request.session['handle']
- log = request.META['wsgi.errors']
+ """
+ glue.list_received_resources(request.META['wsgi.errors'],
+ request.session['handle'])
+ return http.HttpResponseRedirect(reverse(dashboard))
- if request.method == 'POST':
- form = forms.ImportParentForm(conf, request.POST, request.FILES)
- if form.is_valid():
- tmpf = tempfile.NamedTemporaryFile(prefix='parent', suffix='.xml', delete=False)
- f = tmpf.name
- tmpf.write(form.cleaned_data['xml'].read())
- tmpf.close()
-
- glue.import_parent(log, conf, form.cleaned_data['handle'], f)
- os.remove(tmpf.name)
+@handle_required
+def child_response(request, pk):
+ """
+ Export the XML file containing the output of the configure_child
+ to send back to the client.
- return http.HttpResponseRedirect(reverse(dashboard))
- else:
- form = forms.ImportParentForm(conf)
+ """
+ conf = request.session['handle']
+ child = get_object_or_404(models.Child, issuer=conf, pk=pk)
+ z = Zookeeper(handle=conf.handle)
+ xml = z.generate_parental_response(child)
+ resp = serve_xml(str(xml), child.handle)
+ return resp
- return render('rpkigui/import_parent_form.html', { 'form': form }, request)
@handle_required
-def import_repository(request):
+def child_delete(request, pk):
conf = request.session['handle']
- log = request.META['wsgi.errors']
-
+ # verify this child belongs to the current user
+ obj = get_object_or_404(conf.children, pk=pk)
+ form_class = forms.UserDeleteForm # FIXME
if request.method == 'POST':
- form = forms.ImportRepositoryForm(request.POST, request.FILES)
+ form = form_class(request.POST, request.FILES)
if form.is_valid():
- tmpf = tempfile.NamedTemporaryFile(prefix='repository', suffix='.xml', delete=False)
- f = tmpf.name
- tmpf.write(form.cleaned_data['xml'].read())
- tmpf.close()
-
- glue.import_repository(log, conf, f)
-
- os.remove(tmpf.name)
-
- return http.HttpResponseRedirect(reverse(dashboard))
+ z = Zookeeper(handle=conf.handle)
+ z.delete_child(obj.handle)
+ z.synchronize()
+ return http.HttpResponseRedirect(reverse(child_list))
else:
- form = forms.ImportRepositoryForm()
+ form = form_class()
+ return render(request, 'app/child_detail.html',
+ {'object': obj, 'form': form, 'confirm_delete': True})
+
+
+def roa_match(rng):
+ """Return a list of tuples of matching routes and roas."""
+ if isinstance(rng, resource_range_ipv6):
+ route_manager = models.RouteOriginV6.objects
+ pfx = 'prefixes_v6'
+ else:
+ route_manager = models.RouteOrigin.objects
+ pfx = 'prefixes'
- return render('rpkigui/import_repository_form.html', { 'form': form }, request)
+ rv = []
+ for obj in route_manager.filter(prefix_min__gte=rng.min, prefix_max__lte=rng.max):
+ # This is a bit of a gross hack, since the foreign keys for v4 and v6
+ # prefixes have different names.
+ args = {'%s__prefix_min__lte' % pfx: obj.prefix_min,
+ '%s__prefix_max__gte' % pfx: obj.prefix_max}
+ roas = ROA.objects.filter(**args)
+ rv.append((obj, roas))
-@handle_required
-def import_pubclient(request):
- conf = request.session['handle']
- log = request.META['wsgi.errors']
+ return rv
- if request.method == 'POST':
- form = forms.ImportPubClientForm(request.POST, request.FILES)
- if form.is_valid():
- tmpf = tempfile.NamedTemporaryFile(prefix='pubclient', suffix='.xml', delete=False)
- f = tmpf.name
- tmpf.write(form.cleaned_data['xml'].read())
- tmpf.close()
-
- glue.import_pubclient(log, conf, f)
- os.remove(tmpf.name)
+def validate_route(route, roas):
+ """Annotate the route object with its validation status.
- return http.HttpResponseRedirect(reverse(dashboard))
+ `roas` is a queryset containing ROAs which cover `route`.
+
+ """
+ pfx = 'prefixes' if isinstance(route, models.RouteOrigin) else 'prefixes_v6'
+ args = {'asid': route.asn,
+ '%s__prefix_min__lte' % pfx: route.prefix_min,
+ '%s__prefix_max__gte' % pfx: route.prefix_max,
+ '%s__max_length__gte' % pfx: route.prefixlen()}
+
+ # 2. if the candidate ROA set is empty, end with unknown
+ if not roas.exists():
+ route.status = 'unknown'
+ route.status_label = 'warning'
+ # 3. if any candidate roa matches the origin AS and max_length, end with
+ # valid
+ #
+ # AS0 is always invalid.
+ elif route.asn != 0 and roas.filter(**args).exists():
+ route.status_label = 'success'
+ route.status = 'valid'
+ # 4. otherwise the route is invalid
else:
- form = forms.ImportPubClientForm()
+ route.status_label = 'important'
+ route.status = 'invalid'
+
+ return route
- return render('rpkigui/import_pubclient_form.html', { 'form': form }, request)
@handle_required
-def import_child(request):
+def route_view(request):
"""
- Import a repository response.
+ Display a list of global routing table entries which match resources
+ listed in received certificates.
+
"""
conf = request.session['handle']
log = request.META['wsgi.errors']
- if request.method == 'POST':
- form = forms.ImportChildForm(conf, request.POST, request.FILES)
- if form.is_valid():
- tmpf = tempfile.NamedTemporaryFile(prefix='identity', suffix='.xml', delete=False)
- f = tmpf.name
- tmpf.write(form.cleaned_data['xml'].read())
- tmpf.close()
-
- glue.import_child(log, conf, form.cleaned_data['handle'], f)
+ routes = []
+ for p in models.ResourceRangeAddressV4.objects.filter(cert__parent__in=conf.parents.all()):
+ r = p.as_resource_range()
+ print >>log, 'querying for routes matching %s' % r
+ routes.extend([validate_route(*x) for x in roa_match(r)])
+ for p in models.ResourceRangeAddressV6.objects.filter(cert__parent__in=conf.parents.all()):
+ r = p.as_resource_range()
+ print >>log, 'querying for routes matching %s' % r
+ routes.extend([validate_route(*x) for x in roa_match(r)])
- os.remove(tmpf.name)
+ ts = dict((attr['name'], attr['ts']) for attr in models.Timestamp.objects.values())
+ return render(request, 'app/routes_view.html',
+ {'routes': routes, 'timestamp': ts})
- return http.HttpResponseRedirect(reverse(dashboard))
- else:
- form = forms.ImportChildForm(conf)
- return render('rpkigui/import_child_form.html', { 'form': form }, request)
+def route_detail(request, pk):
+ pass
-@login_required
-def initialize(request):
- """
- Initialize a new user account.
- """
- if request.method == 'POST':
- form = forms.GenericConfirmationForm(request.POST)
- if form.is_valid():
- glue.initialize_handle(request.META['wsgi.errors'], handle=request.user.username, owner=request.user)
- return http.HttpResponseRedirect(reverse(dashboard))
- else:
- form = forms.GenericConfirmationForm()
- return render('rpkigui/initialize_form.html', { 'form': form }, request)
+def route_roa_list(request, pk):
+ """Show a list of ROAs that match a given route."""
+ object = get_object_or_404(models.RouteOrigin, pk=pk)
+ # select accepted ROAs which cover this route
+ qs = ROAPrefixV4.objects.filter(prefix_min__lte=object.prefix_min,
+ prefix_max__gte=object.prefix_max).select_related()
+ return object_list(request, qs, template_name='app/route_roa_list.html')
+
@handle_required
-def child_wizard(request):
- """
- Wizard mode to create a new locally hosted child.
- """
+def repository_list(request):
conf = request.session['handle']
- log = request.META['wsgi.errors']
- if not request.user.is_superuser:
- return http.HttpResponseForbidden()
+ qs = models.Repository.objects.filter(issuer=conf)
+ return object_list(request, queryset=qs,
+ template_name='app/repository_list.html',
+ extra_context={
+ 'create_url': reverse(repository_import),
+ 'create_label': u'Import'})
- if request.method == 'POST':
- form = forms.ChildWizardForm(conf, request.POST)
- if form.is_valid():
- glue.create_child(log, conf, form.cleaned_data['handle'])
- return http.HttpResponseRedirect(reverse(dashboard))
- else:
- form = forms.ChildWizardForm(conf)
-
- return render('rpkigui/child_wizard_form.html', { 'form': form }, request)
@handle_required
-def export_child_response(request, child_handle):
- """
- Export the XML file containing the output of the configure_child
- to send back to the client.
- """
+def repository_detail(request, pk):
conf = request.session['handle']
- log = request.META['wsgi.errors']
- return serve_xml(glue.read_child_response(log, conf, child_handle), child_handle)
+ qs = models.Repository.objects.filter(issuer=conf)
+ return object_detail(request, queryset=qs, object_id=pk,
+ template_name='app/repository_detail.html')
-@handle_required
-def export_child_repo_response(request, child_handle):
- """
- Export the XML file containing the output of the configure_child
- to send back to the client.
- """
- conf = request.session['handle']
- log = request.META['wsgi.errors']
- return serve_xml(glue.read_child_repo_response(log, conf, child_handle), child_handle)
@handle_required
-def update_bpki(request):
- conf = request.session['handle']
+def repository_delete(request, pk):
log = request.META['wsgi.errors']
-
+ conf = request.session['handle']
+ # Ensure the repository being deleted belongs to the current user.
+ obj = get_object_or_404(models.Repository, issuer=conf, pk=pk)
if request.method == 'POST':
- form = forms.GenericConfirmationForm(request.POST, request.FILES)
+ form = form_class(request.POST, request.FILES)
if form.is_valid():
- glue.update_bpki(log, conf)
- return http.HttpResponseRedirect(reverse(dashboard))
+ z = Zookeeper(handle=conf.handle, logstream=log)
+ z.delete_repository(obj.handle)
+ z.synchronize()
+ return http.HttpResponseRedirect(reverse(repository_list))
else:
- form = forms.GenericConfirmationForm()
+ form = form_class()
+ return render(request, 'app/repository_detail.html',
+ {'object': obj, 'form': form, 'confirm_delete': True})
- return render('rpkigui/update_bpki_form.html', { 'form': form }, request)
@handle_required
-def child_delete(request, child_handle):
- conf = request.session['handle']
+def repository_import(request):
+ """Import XML response file from repository operator."""
+ return generic_import(request,
+ models.Repository.objects,
+ Zookeeper.configure_repository,
+ form_class=forms.ImportRepositoryForm,
+ post_import_redirect=reverse(repository_list))
+
+
+@superuser_required
+def client_list(request):
+ return object_list(request, queryset=models.Client.objects.all(),
+ extra_context={
+ 'create_url': reverse(client_import),
+ 'create_label': u'Import'})
+
+
+@superuser_required
+def client_detail(request, pk):
+ return object_detail(request, queryset=models.Client.objects, object_id=pk)
+
+
+@superuser_required
+def client_delete(request, pk):
log = request.META['wsgi.errors']
- child = get_object_or_404(conf.children, handle__exact=child_handle)
-
+ obj = get_object_or_404(models.Client, pk=pk)
+ form_class = forms.UserDeleteForm # FIXME
if request.method == 'POST':
- form = forms.GenericConfirmationForm(request.POST, request.FILES)
+ form = form_class(request.POST, request.FILES)
if form.is_valid():
- glue.delete_child(log, conf, child_handle)
- child.delete()
- return http.HttpResponseRedirect(reverse(dashboard))
+ z = Zookeeper(logstream=log)
+ z.delete_publication_client(obj.handle)
+ z.synchronize()
+ return http.HttpResponseRedirect(reverse(client_list))
else:
- form = forms.GenericConfirmationForm()
+ form = form_class()
+ return render(request, 'app/client_detail.html',
+ {'object': obj, 'form': form, 'confirm_delete': True})
- return render('rpkigui/child_delete_form.html', { 'form': form , 'object': child }, request)
-@handle_required
-def parent_delete(request, parent_handle):
- conf = request.session['handle']
+@superuser_required
+def client_import(request):
+ return generic_import(request, models.Client.objects,
+ Zookeeper.configure_publication_client,
+ form_class=forms.ImportClientForm,
+ post_import_redirect=reverse(client_list))
+
+
+@superuser_required
+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)
+ return serve_xml(str(xml), '%s.repo' % z.handle)
+
+
+@superuser_required
+def user_list(request):
+ """Display a list of all the RPKI handles managed by this server."""
+ # create a list of tuples of (Conf, User)
+ users = []
+ for conf in models.Conf.objects.all():
+ try:
+ u = User.objects.get(username=conf.handle)
+ except User.DoesNotExist:
+ u = None
+ users.append((conf, u))
+ return render(request, 'app/user_list.html', {'users': users})
+
+
+@superuser_required
+def user_detail(request):
+ """Placeholder for Conf.get_absolute_url()."""
+ pass
+
+
+@superuser_required
+def user_delete(request, pk):
+ conf = models.Conf.objects.get(pk=pk)
log = request.META['wsgi.errors']
- parent = get_object_or_404(conf.parents, handle__exact=parent_handle)
+ if request.method == 'POST':
+ form = forms.UserDeleteForm(request.POST)
+ if form.is_valid():
+ User.objects.filter(username=conf.handle).delete()
+ z = Zookeeper(handle=conf.handle, logstream=log)
+ z.delete_self()
+ z.synchronize()
+ return http.HttpResponseRedirect(reverse(user_list))
+ else:
+ form = forms.UserDeleteForm()
+ return render(request, 'app/user_confirm_delete.html',
+ {'object': conf, 'form': form})
+
+
+@superuser_required
+def user_edit(request, pk):
+ conf = get_object_or_404(models.Conf, pk=pk)
+ # in the old model, there may be users with a different name, so create a
+ # new user object if it is missing.
+ try:
+ user = User.objects.get(username=conf.handle)
+ except User.DoesNotExist:
+ user = User(username=conf.handle)
if request.method == 'POST':
- form = forms.GenericConfirmationForm(request.POST, request.FILES)
+ form = forms.UserEditForm(request.POST)
if form.is_valid():
- glue.delete_parent(log, conf, parent_handle)
- parent.delete()
- return http.HttpResponseRedirect(reverse(dashboard))
+ pw = form.cleaned_data.get('pw')
+ if pw:
+ user.set_password(pw)
+ user.email = form.cleaned_data.get('email')
+ user.save()
+ return http.HttpResponseRedirect(reverse(user_list))
else:
- form = forms.GenericConfirmationForm()
+ form = forms.UserEditForm(initial={'email': user.email})
+ return render(request, 'app/user_edit_form.html',
+ {'object': user, 'form': form})
- return render('rpkigui/parent_form.html', { 'form': form ,
- 'parent': parent, 'submit_label': 'Delete' }, request)
-@login_required
-def destroy_handle(request, handle):
- """
- Completely remove a hosted resource handle.
+@handle_required
+def user_create(request):
"""
+ Wizard mode to create a new locally hosted child.
- log = request.META['wsgi.errors']
-
+ """
if not request.user.is_superuser:
return http.HttpResponseForbidden()
- conf = get_object_or_404(models.Conf, handle=handle)
-
+ log = request.META['wsgi.errors']
if request.method == 'POST':
- form = forms.GenericConfirmationForm(request.POST, request.FILES)
+ form = forms.UserCreateForm(request.POST, request.FILES)
if form.is_valid():
- glue.destroy_handle(log, handle)
- return render('rpkigui/generic_result.html',
- { 'operation': 'Destroy ' + handle,
- 'result': 'Succeeded' }, request)
- else:
- form = forms.GenericConfirmationForm()
+ handle = form.cleaned_data.get('handle')
+ pw = form.cleaned_data.get('password')
+ email = form.cleaned_data.get('email')
+ parent = form.cleaned_data.get('parent')
+
+ User.objects.create_user(handle, email, pw)
+
+ zk_child = Zookeeper(handle=handle, logstream=log)
+ identity_xml = zk_child.initialize()
+ if parent:
+ # FIXME etree_wrapper should allow us to deal with file objects
+ t = NamedTemporaryFile(delete=False)
+ t.close()
+
+ identity_xml.save(t.name)
+ zk_parent = Zookeeper(handle=parent.handle, logstream=log)
+ parent_response, _ = zk_parent.configure_child(t.name)
+ parent_response.save(t.name)
+ repo_req, _ = zk_child.configure_parent(t.name)
+ repo_req.save(t.name)
+ repo_resp, _ = zk_parent.configure_publication_client(t.name)
+ repo_resp.save(t.name)
+ zk_child.configure_repository(t.name)
+ os.remove(t.name)
+ zk_child.synchronize()
- return render('rpkigui/destroy_handle_form.html', { 'form': form ,
- 'handle': handle }, request)
+ return http.HttpResponseRedirect(reverse(dashboard))
+ else:
+ conf = request.session['handle']
+ form = forms.UserCreateForm(initial={'parent': conf})
-# vim:sw=4 ts=8 expandtab
+ return render(request, 'app/user_create_form.html', {'form': form})
diff --git a/rpkid/rpki/gui/cacheview/admin.py b/rpkid/rpki/gui/cacheview/admin.py
deleted file mode 100644
index 05bab881..00000000
--- a/rpkid/rpki/gui/cacheview/admin.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""
-$Id$
-
-Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
-
-from django.contrib import admin
-from rpki.gui.cacheview import models
-
-class ASRangeAdmin(admin.ModelAdmin):
- pass
-
-class AddressRangeAdmin(admin.ModelAdmin):
- pass
-
-class CertAdmin(admin.ModelAdmin):
- pass
-
-class ROAPrefixAdmin(admin.ModelAdmin):
- pass
-
-class ROAAdmin(admin.ModelAdmin):
- pass
-
-class GhostbusterAdmin(admin.ModelAdmin):
- pass
-
-class ValidationLabelAdmin(admin.ModelAdmin): pass
-
-class ValidationStatus_CertAdmin(admin.ModelAdmin): pass
-
-class ValidationStatus_ROAAdmin(admin.ModelAdmin): pass
-
-class ValidationStatus_GhostbusterAdmin(admin.ModelAdmin): pass
-
-admin.site.register(models.AddressRange, AddressRangeAdmin)
-admin.site.register(models.ASRange, AddressRangeAdmin)
-admin.site.register(models.Cert, CertAdmin)
-admin.site.register(models.Ghostbuster, GhostbusterAdmin)
-admin.site.register(models.ROA, ROAAdmin)
-admin.site.register(models.ROAPrefix, ROAPrefixAdmin)
-admin.site.register(models.ValidationLabel, ValidationLabelAdmin)
-admin.site.register(models.ValidationStatus_Cert, ValidationStatus_CertAdmin)
-admin.site.register(models.ValidationStatus_ROA, ValidationStatus_ROAAdmin)
-admin.site.register(models.ValidationStatus_Ghostbuster, ValidationStatus_GhostbusterAdmin)
-
-# vim:sw=4 ts=8
diff --git a/rpkid/rpki/gui/cacheview/models.py b/rpkid/rpki/gui/cacheview/models.py
index 077a28ff..4be45b5c 100644
--- a/rpkid/rpki/gui/cacheview/models.py
+++ b/rpkid/rpki/gui/cacheview/models.py
@@ -1,106 +1,87 @@
-"""
-Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-"""
+# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
from datetime import datetime
import time
from django.db import models
-from rpki.resource_set import resource_range_ipv4, resource_range_ipv6
-from rpki.exceptions import MustBePrefix
+import rpki.ipaddrs
+import rpki.resource_set
+import rpki.gui.models
+
class TelephoneField(models.CharField):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 255
models.CharField.__init__(self, *args, **kwargs)
-class AddressRange(models.Model):
- family = models.IntegerField()
- min = models.IPAddressField(db_index=True)
- max = models.IPAddressField(db_index=True)
-
- class Meta:
- ordering = ('family', 'min', 'max')
- unique_together = ('family', 'min', 'max')
+class AddressRange(rpki.gui.models.PrefixV4):
@models.permalink
def get_absolute_url(self):
return ('rpki.gui.cacheview.views.addressrange_detail', [str(self.pk)])
- def __unicode__(self):
- if self.min == self.max:
- return u'%s' % self.min
-
- if self.family == 4:
- r = resource_range_ipv4.from_strings(self.min, self.max)
- elif self.family == 6:
- r = resource_range_ipv6.from_strings(self.min, self.max)
-
- try:
- prefixlen = r.prefixlen()
- except MustBePrefix:
- return u'%s-%s' % (self.min, self.max)
- return u'%s/%d' % (self.min, prefixlen)
-class ASRange(models.Model):
- min = models.PositiveIntegerField(db_index=True)
- max = models.PositiveIntegerField(db_index=True)
-
- class Meta:
- ordering = ('min', 'max')
- #unique_together = ('min', 'max')
+class AddressRangeV6(rpki.gui.models.PrefixV6):
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.cacheview.views.addressrange_detail_v6',
+ [str(self.pk)])
- def __unicode__(self):
- if self.min == self.max:
- return u'AS%d' % self.min
- else:
- return u'AS%s-%s' % (self.min, self.max)
+class ASRange(rpki.gui.models.ASN):
@models.permalink
def get_absolute_url(self):
return ('rpki.gui.cacheview.views.asrange_detail', [str(self.pk)])
kinds = list(enumerate(('good', 'warn', 'bad')))
-kinds_dict = dict((v,k) for k,v in kinds)
+kinds_dict = dict((v, k) for k, v in kinds)
+
class ValidationLabel(models.Model):
"""
Represents a specific error condition defined in the rcynic XML
output file.
"""
- label = models.CharField(max_length=30, db_index=True, unique=True)
+ label = models.CharField(max_length=79, db_index=True, unique=True)
status = models.CharField(max_length=255)
kind = models.PositiveSmallIntegerField(choices=kinds)
def __unicode__(self):
return self.label
- class Meta:
- verbose_name_plural = 'ValidationLabels'
+
+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')))
generations_dict = dict((val, key) for (key, val) in generations)
+
class ValidationStatus(models.Model):
- timestamp = models.DateTimeField()
+ timestamp = models.DateTimeField()
generation = models.PositiveSmallIntegerField(choices=generations, null=True)
- status = models.ForeignKey('ValidationLabel')
+ status = models.ForeignKey(ValidationLabel)
+ repo = models.ForeignKey(RepositoryObject, related_name='statuses')
- class Meta:
- abstract = True
class SignedObject(models.Model):
"""
@@ -108,24 +89,20 @@ class SignedObject(models.Model):
The signing certificate is ommitted here in order to give a proper
value for the 'related_name' attribute.
"""
- # attributes from rcynic's output XML file
- uri = models.URLField(unique=True, db_index=True)
+ repo = models.ForeignKey(RepositoryObject, related_name='cert', unique=True)
# on-disk file modification time
- mtime = models.PositiveIntegerField(default=0)
+ mtime = models.PositiveIntegerField(default=0)
# SubjectName
- name = models.CharField(max_length=255)
+ name = models.CharField(max_length=255)
# value from the SKI extension
- keyid = models.CharField(max_length=50, db_index=True)
+ keyid = models.CharField(max_length=60, db_index=True)
# validity period from EE cert which signed object
not_before = models.DateTimeField()
- not_after = models.DateTimeField()
-
- class Meta:
- abstract = True
+ not_after = models.DateTimeField()
def mtime_as_datetime(self):
"""
@@ -133,13 +110,6 @@ class SignedObject(models.Model):
"""
return datetime.utcfromtimestamp(self.mtime + time.timezone)
- def is_valid(self):
- """
- Returns a boolean value indicating whether this object has passed
- validation checks.
- """
- return bool(self.statuses.filter(status=ValidationLabel.objects.get(label="object_accepted")))
-
def status_id(self):
"""
Returns a HTML class selector for the current object based on its validation status.
@@ -149,70 +119,90 @@ class SignedObject(models.Model):
for x in reversed(kinds):
if self.statuses.filter(generation=generations_dict['current'], status__kind=x[0]):
return x[1]
- return None # should not happen
+ return None # should not happen
def __unicode__(self):
return u'%s' % self.name
+
class Cert(SignedObject):
"""
Object representing a resource certificate.
"""
addresses = models.ManyToManyField(AddressRange, related_name='certs')
- asns = models.ManyToManyField(ASRange, related_name='certs')
- issuer = models.ForeignKey('Cert', related_name='children', null=True, blank=True)
- sia = models.CharField(max_length=255)
+ addresses_v6 = models.ManyToManyField(AddressRangeV6, related_name='certs')
+ asns = models.ManyToManyField(ASRange, related_name='certs')
+ issuer = models.ForeignKey('self', related_name='children', null=True)
+ sia = models.CharField(max_length=255)
@models.permalink
def get_absolute_url(self):
return ('rpki.gui.cacheview.views.cert_detail', [str(self.pk)])
-class ValidationStatus_Cert(ValidationStatus):
- cert = models.ForeignKey('Cert', related_name='statuses')
class ROAPrefix(models.Model):
- family = models.PositiveIntegerField()
- prefix = models.IPAddressField()
- bits = models.PositiveIntegerField()
- max_length = models.PositiveIntegerField()
+ "Abstract base class for ROA mixin."
+
+ max_length = models.PositiveSmallIntegerField()
class Meta:
- ordering = ['family', 'prefix', 'bits', 'max_length']
+ abstract = True
+
+ def as_roa_prefix(self):
+ "Return value as a rpki.resource_set.roa_prefix_ip object."
+ rng = self.as_resource_range()
+ return self.roa_cls(rng.prefix_min, rng.prefixlen(), self.max_length)
def __unicode__(self):
- if self.bits == self.max_length:
- return u'%s/%d' % (self.prefix, self.bits)
- else:
- return u'%s/%d-%d' % (self.prefix, self.bits, self.max_length)
+ p = self.as_resource_range()
+ if p.prefixlen() == self.max_length:
+ return str(p)
+ return '%s-%s' % (str(p), self.max_length)
+
+
+# ROAPrefix is declared first, so subclass picks up __unicode__ from it.
+class ROAPrefixV4(ROAPrefix, rpki.gui.models.PrefixV4):
+ "One v4 prefix in a ROA."
+
+ roa_cls = rpki.resource_set.roa_prefix_ipv4
+
+ class Meta:
+ ordering = ('prefix_min',)
+
+
+# ROAPrefix is declared first, so subclass picks up __unicode__ from it.
+class ROAPrefixV6(ROAPrefix, rpki.gui.models.PrefixV6):
+ "One v6 prefix in a ROA."
+
+ roa_cls = rpki.resource_set.roa_prefix_ipv6
+
+ class Meta:
+ ordering = ('prefix_min',)
+
class ROA(SignedObject):
- asid = models.PositiveIntegerField()
- prefixes = models.ManyToManyField(ROAPrefix, related_name='roas')
- issuer = models.ForeignKey('Cert', related_name='roas')
+ asid = models.PositiveIntegerField()
+ prefixes = models.ManyToManyField(ROAPrefixV4, related_name='roas')
+ prefixes_v6 = models.ManyToManyField(ROAPrefixV6, related_name='roas')
+ issuer = models.ForeignKey('Cert', related_name='roas')
@models.permalink
def get_absolute_url(self):
return ('rpki.gui.cacheview.views.roa_detail', [str(self.pk)])
class Meta:
- ordering = ['asid']
+ ordering = ('asid',)
def __unicode__(self):
return u'ROA for AS%d' % self.asid
- @models.permalink
- def get_absolute_url(self):
- return ('rpki.gui.cacheview.views.roa_detail', [str(self.pk)])
-
-class ValidationStatus_ROA(ValidationStatus):
- roa = models.ForeignKey('ROA', related_name='statuses')
class Ghostbuster(SignedObject):
- full_name = models.CharField(max_length=40)
+ full_name = models.CharField(max_length=40)
email_address = models.EmailField(blank=True, null=True)
- organization = models.CharField(blank=True, null=True, max_length=255)
- telephone = TelephoneField(blank=True, null=True)
- issuer = models.ForeignKey('Cert', related_name='ghostbusters')
+ organization = models.CharField(blank=True, null=True, max_length=255)
+ telephone = TelephoneField(blank=True, null=True)
+ issuer = models.ForeignKey('Cert', related_name='ghostbusters')
@models.permalink
def get_absolute_url(self):
@@ -226,8 +216,3 @@ class Ghostbuster(SignedObject):
if self.email_address:
return self.email_address
return self.telephone
-
-class ValidationStatus_Ghostbuster(ValidationStatus):
- gbr = models.ForeignKey('Ghostbuster', related_name='statuses')
-
-# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/models.py b/rpkid/rpki/gui/models.py
new file mode 100644
index 00000000..749f335f
--- /dev/null
+++ b/rpkid/rpki/gui/models.py
@@ -0,0 +1,132 @@
+"""
+$Id$
+
+Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+Common classes for reuse in apps.
+"""
+
+import struct
+
+from django.db import models
+
+import rpki.resource_set
+import rpki.ipaddrs
+
+class IPv6AddressField(models.Field):
+ "Field large enough to hold a 128-bit unsigned integer."
+
+ __metaclass__ = models.SubfieldBase
+
+ def db_type(self, connection):
+ return 'binary(16)'
+
+ def to_python(self, value):
+ if isinstance(value, rpki.ipaddrs.v6addr):
+ return value
+ x = struct.unpack('!QQ', value)
+ return rpki.ipaddrs.v6addr((x[0] << 64) | x[1])
+
+ def get_db_prep_value(self, value, connection, prepared):
+ return struct.pack('!QQ', (long(value) >> 64) & 0xFFFFFFFFFFFFFFFFL, long(value) & 0xFFFFFFFFFFFFFFFFL)
+
+class IPv4AddressField(models.Field):
+ "Wrapper around rpki.ipaddrs.v4addr."
+
+ __metaclass__ = models.SubfieldBase
+
+ def db_type(self, connection):
+ return 'int UNSIGNED'
+
+ def to_python(self, value):
+ if isinstance(value, rpki.ipaddrs.v4addr):
+ return value
+ return rpki.ipaddrs.v4addr(value)
+
+ def get_db_prep_value(self, value, connection, prepared):
+ return long(value)
+
+class Prefix(models.Model):
+ """Common implementation for models with an IP address range.
+
+ Expects that `range_cls` is set to the appropriate subclass of
+ rpki.resource_set.resource_range_ip."""
+
+ def as_resource_range(self):
+ """
+ Returns the prefix as a rpki.resource_set.resource_range_ip object.
+ """
+ return self.range_cls(self.prefix_min, self.prefix_max)
+
+ def prefixlen(self):
+ "Returns the prefix length for the prefix in this object."
+ return self.as_resource_range().prefixlen()
+
+ def get_prefix_display(self):
+ "Return a string representatation of this IP prefix."
+ return str(self.as_resource_range())
+
+ def __unicode__(self):
+ """This method may be overridden by subclasses. The default
+ implementation calls get_prefix_display(). """
+ return self.get_prefix_display()
+
+ class Meta:
+ abstract = True
+
+ # default sort order reflects what "sh ip bgp" outputs
+ ordering = ('prefix_min',)
+
+class PrefixV4(Prefix):
+ "IPv4 Prefix."
+
+ range_cls = rpki.resource_set.resource_range_ipv4
+
+ prefix_min = IPv4AddressField(db_index=True, null=False)
+ prefix_max = IPv4AddressField(db_index=True, null=False)
+
+ class Meta(Prefix.Meta):
+ abstract = True
+
+class PrefixV6(Prefix):
+ "IPv6 Prefix."
+
+ range_cls = rpki.resource_set.resource_range_ipv6
+
+ prefix_min = IPv6AddressField(db_index=True, null=False)
+ prefix_max = IPv6AddressField(db_index=True, null=False)
+
+ class Meta(Prefix.Meta):
+ abstract = True
+
+class ASN(models.Model):
+ """Represents a range of ASNs.
+
+ This model is abstract, and is intended to be reused by applications."""
+
+ min = models.PositiveIntegerField(null=False)
+ max = models.PositiveIntegerField(null=False)
+
+ class Meta:
+ abstract = True
+ ordering = ('min', 'max')
+
+ def as_resource_range(self):
+ return rpki.resource_set.resource_range_as(self.min, self.max)
+
+ def __unicode__(self):
+ return u'AS%s' % self.as_resource_range()
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/routeview/__init__.py b/rpkid/rpki/gui/routeview/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpkid/rpki/gui/routeview/__init__.py
diff --git a/rpkid/rpki/gui/routeview/models.py b/rpkid/rpki/gui/routeview/models.py
new file mode 100644
index 00000000..321fde5d
--- /dev/null
+++ b/rpkid/rpki/gui/routeview/models.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id'
+
+from django.db.models import PositiveIntegerField
+import rpki.gui.models
+
+
+class RouteOrigin(rpki.gui.models.PrefixV4):
+ "Represents an IPv4 BGP routing table entry."
+
+ asn = PositiveIntegerField(help_text='origin AS', null=False)
+
+ def __unicode__(self):
+ return u"AS%d's route origin for %s" % (self.asn,
+ self.get_prefix_display())
+
+ class Meta:
+ # sort by increasing mask length (/16 before /24)
+ ordering = ('prefix_min', '-prefix_max')
+
+
+class RouteOriginV6(rpki.gui.models.PrefixV6):
+ "Represents an IPv6 BGP routing table entry."
+
+ asn = PositiveIntegerField(help_text='origin AS', null=False)
+
+ def __unicode__(self):
+ return u"AS%d's route origin for %s" % (self.asn,
+ self.get_prefix_display())
+
+ class Meta:
+ ordering = ('prefix_min', '-prefix_max')
diff --git a/rpkid/rpki/gui/urls.py b/rpkid/rpki/gui/urls.py
index 70ea4056..d643ad27 100644
--- a/rpkid/rpki/gui/urls.py
+++ b/rpkid/rpki/gui/urls.py
@@ -1,21 +1,19 @@
-# $Id$
-
-"""
-Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-
-"""
+# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions
+# Copyright (C) 2012 SPARTA, Inc. a Parsons Company
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+__version__ = '$Id$'
from django.conf.urls.defaults import *
@@ -24,17 +22,22 @@ admin.autodiscover()
urlpatterns = patterns('',
- # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
+ # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
# to INSTALLED_APPS to enable admin documentation:
- (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+ #(r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
- (r'^admin/', include(admin.site.urls)),
+ #(r'^admin/', include(admin.site.urls)),
(r'^rpki/', include('rpki.gui.app.urls')),
(r'^cacheview/', include('rpki.gui.cacheview.urls')),
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
(r'^accounts/logout/$', 'django.contrib.auth.views.logout',
- { 'next_page': '/rpki/' }),
+ {'next_page': '/rpki/'}),
+
+ # !!!REMOVE THIS BEFORE COMMITTING!!!
+ # for testing with the django test webserver
+ (r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': '/usr/local/share/rpki/media'}),
)
diff --git a/rpkid/rpki/http.py b/rpkid/rpki/http.py
index 0df7e6f2..a0055ac9 100644
--- a/rpkid/rpki/http.py
+++ b/rpkid/rpki/http.py
@@ -534,7 +534,7 @@ class http_server(http_stream):
raise
except Exception, e:
rpki.log.traceback()
- self.send_error(500, "Unhandled exception %s" % e)
+ self.send_error(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e))
else:
self.send_error(code = error[0], reason = error[1])
diff --git a/rpkid/rpki/ipaddrs.py b/rpkid/rpki/ipaddrs.py
index 531bcbb9..a192f92b 100644
--- a/rpkid/rpki/ipaddrs.py
+++ b/rpkid/rpki/ipaddrs.py
@@ -57,6 +57,8 @@ class v4addr(long):
"""
Construct a v4addr object.
"""
+ if isinstance(x, unicode):
+ x = x.encode("ascii")
if isinstance(x, str):
return cls.from_bytes(socket.inet_pton(socket.AF_INET, ".".join(str(int(i)) for i in x.split("."))))
else:
@@ -94,6 +96,8 @@ class v6addr(long):
"""
Construct a v6addr object.
"""
+ if isinstance(x, unicode):
+ x = x.encode("ascii")
if isinstance(x, str):
return cls.from_bytes(socket.inet_pton(socket.AF_INET6, x))
else:
diff --git a/rpkid/rpki/irdb/__init__.py b/rpkid/rpki/irdb/__init__.py
new file mode 100644
index 00000000..3eb6fab7
--- /dev/null
+++ b/rpkid/rpki/irdb/__init__.py
@@ -0,0 +1,23 @@
+"""
+Django really wants its models packaged as a models module within a
+Python package, so humor it.
+
+$Id$
+
+Copyright (C) 2011 Internet Systems Consortium ("ISC")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+"""
+
+from rpki.irdb.models import *
+from rpki.irdb.zookeeper import Zookeeper
diff --git a/rpkid/rpki/irdb/models.py b/rpkid/rpki/irdb/models.py
new file mode 100644
index 00000000..3aaebdcf
--- /dev/null
+++ b/rpkid/rpki/irdb/models.py
@@ -0,0 +1,585 @@
+"""
+IR Database, Django-style.
+
+This is the back-end code's interface to the database. It's intended
+to be usable by command line programs and other scripts, not just
+Django GUI code, so be careful.
+
+$Id$
+
+Copyright (C) 2011 Internet Systems Consortium ("ISC")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+"""
+
+import django.db.models
+import rpki.x509
+import rpki.sundial
+import rpki.resource_set
+import rpki.ipaddrs
+import socket
+
+## @var ip_version_choices
+# Choice argument for fields implementing IP version numbers.
+
+ip_version_choices = ((4, "IPv4"), (6, "IPv6"))
+
+## @var ca_certificate_lifetime
+# Lifetime for a BPKI CA certificate.
+
+ca_certificate_lifetime = rpki.sundial.timedelta(days = 3652)
+
+## @var crl_interval
+# Expected interval between BPKI CRL updates
+
+crl_interval = rpki.sundial.timedelta(days = 1)
+
+## @var ee_certificate_lifetime
+# Lifetime for a BPKI EE certificate.
+
+ee_certificate_lifetime = rpki.sundial.timedelta(days = 60)
+
+###
+
+# Field types
+
+class HandleField(django.db.models.CharField):
+ """
+ A handle field type.
+ """
+
+ description = 'A "handle" in one of the RPKI protocols'
+
+ def __init__(self, *args, **kwargs):
+ kwargs["max_length"] = 120
+ django.db.models.CharField.__init__(self, *args, **kwargs)
+
+class EnumField(django.db.models.PositiveSmallIntegerField):
+ """
+ An enumeration type that uses strings in Python and small integers
+ in SQL.
+ """
+
+ description = "An enumeration type"
+
+ __metaclass__ = django.db.models.SubfieldBase
+
+ def __init__(self, *args, **kwargs):
+ if isinstance(kwargs["choices"], (tuple, list)) and isinstance(kwargs["choices"][0], str):
+ kwargs["choices"] = tuple(enumerate(kwargs["choices"], 1))
+ django.db.models.PositiveSmallIntegerField.__init__(self, *args, **kwargs)
+ self.enum_i2s = dict(self.flatchoices)
+ self.enum_s2i = dict((v, k) for k, v in self.flatchoices)
+
+ def to_python(self, value):
+ return self.enum_i2s.get(value, value)
+
+ def get_prep_value(self, value):
+ return self.enum_s2i.get(value, value)
+
+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"
+
+ def to_python(self, value):
+ if isinstance(value, rpki.sundial.pydatetime.datetime):
+ return rpki.sundial.datetime.fromdatetime(
+ django.db.models.DateTimeField.to_python(self, value))
+ else:
+ return value
+
+ def get_prep_value(self, value):
+ if isinstance(value, rpki.sundial.datetime):
+ return value.to_sql()
+ else:
+ return value
+
+###
+
+# Kludge to work around Django 1.2 problem.
+#
+# This should be a simple abstract base class DERField which we then
+# subclass with trivial customization for specific kinds of DER
+# objects. Sadly, subclassing of user defined field classes doesn't
+# work in Django 1.2 with the django.db.models.SubfieldBase metaclass,
+# so instead we fake it by defining methods externally and defining
+# each concrete class as a direct subclass of django.db.models.Field.
+#
+# The bug has been fixed in Django 1.3, so we can revert this to the
+# obvious form once we're ready to require Django 1.3 or later. The
+# fix may have been backported to the 1.2 branch, but trying to test
+# for it is likely more work than just working around it.
+#
+# See https://code.djangoproject.com/ticket/10728 for details.
+
+def DERField_init(self, *args, **kwargs):
+ kwargs["serialize"] = False
+ kwargs["blank"] = True
+ kwargs["default"] = None
+ django.db.models.Field.__init__(self, *args, **kwargs)
+
+def DERField_db_type(self, connection):
+ if connection.settings_dict['ENGINE'] == "django.db.backends.posgresql":
+ return "bytea"
+ else:
+ return "BLOB"
+
+def DERField_to_python(self, value):
+ assert value is None or isinstance(value, (self.rpki_type, str))
+ if isinstance(value, str):
+ return self.rpki_type(DER = value)
+ else:
+ return value
+
+def DERField_get_prep_value(self, value):
+ assert value is None or isinstance(value, (self.rpki_type, str))
+ if isinstance(value, self.rpki_type):
+ return value.get_DER()
+ else:
+ return value
+
+def DERField(cls):
+ cls.__init__ = DERField_init
+ cls.db_type = DERField_db_type
+ cls.to_python = DERField_to_python
+ cls.get_prep_value = DERField_get_prep_value
+ return cls
+
+@DERField
+class CertificateField(django.db.models.Field):
+ __metaclass__ = django.db.models.SubfieldBase
+ description = "X.509 certificate"
+ rpki_type = rpki.x509.X509
+
+@DERField
+class RSAKeyField(django.db.models.Field):
+ __metaclass__ = django.db.models.SubfieldBase
+ description = "RSA keypair"
+ rpki_type = rpki.x509.RSA
+
+@DERField
+class CRLField(django.db.models.Field):
+ __metaclass__ = django.db.models.SubfieldBase
+ description = "Certificate Revocation List"
+ rpki_type = rpki.x509.CRL
+
+@DERField
+class PKCS10Field(django.db.models.Field):
+ __metaclass__ = django.db.models.SubfieldBase
+ description = "PKCS #10 certificate request"
+ rpki_type = rpki.x509.PKCS10
+
+@DERField
+class SignedReferralField(django.db.models.Field):
+ __metaclass__ = django.db.models.SubfieldBase
+ description = "CMS signed object containing XML"
+ rpki_type = rpki.x509.SignedReferral
+
+###
+
+# Custom managers
+
+class CertificateManager(django.db.models.Manager):
+
+ def get_or_certify(self, **kwargs):
+ """
+ Sort of like .get_or_create(), but for models containing
+ certificates which need to be generated based on other fields.
+
+ Takes keyword arguments like .get(), checks for existing object.
+ If none, creates a new one; if found an existing object but some
+ of the non-key fields don't match, updates the existing object.
+ Runs certification method for new or updated objects. Returns a
+ tuple consisting of the object and a boolean indicating whether
+ anything has changed.
+ """
+
+ changed = False
+
+ try:
+ obj = self.get(**self._get_or_certify_keys(kwargs))
+
+ except self.model.DoesNotExist:
+ obj = self.model(**kwargs)
+ changed = True
+
+ else:
+ for k in kwargs:
+ if getattr(obj, k) != kwargs[k]:
+ setattr(obj, k, kwargs[k])
+ changed = True
+
+ if changed:
+ obj.avow()
+ obj.save()
+
+ return obj, changed
+
+ def _get_or_certify_keys(self, kwargs):
+ assert len(self.model._meta.unique_together) == 1
+ return dict((k, kwargs[k]) for k in self.model._meta.unique_together[0])
+
+class ResourceHolderCAManager(CertificateManager):
+ def _get_or_certify_keys(self, kwargs):
+ return { "handle" : kwargs["handle"] }
+
+class ServerCAManager(CertificateManager):
+ def _get_or_certify_keys(self, kwargs):
+ return { "pk" : 1 }
+
+class ResourceHolderEEManager(CertificateManager):
+ def _get_or_certify_keys(self, kwargs):
+ return { "issuer" : kwargs["issuer"] }
+
+###
+
+class CA(django.db.models.Model):
+ certificate = CertificateField()
+ private_key = RSAKeyField()
+ latest_crl = CRLField()
+
+ # Might want to bring these into line with what rpkid does. Current
+ # variables here were chosen to map easily to what OpenSSL command
+ # line tool was keeping on disk.
+
+ next_serial = django.db.models.BigIntegerField(default = 1)
+ next_crl_number = django.db.models.BigIntegerField(default = 1)
+ last_crl_update = SundialField()
+ next_crl_update = SundialField()
+
+ class Meta:
+ abstract = True
+
+ def avow(self):
+ if self.private_key is None:
+ self.private_key = rpki.x509.RSA.generate()
+ now = rpki.sundial.now()
+ notAfter = now + ca_certificate_lifetime
+ self.certificate = rpki.x509.X509.bpki_self_certify(
+ keypair = self.private_key,
+ subject_name = self.subject_name,
+ serial = self.next_serial,
+ now = now,
+ notAfter = notAfter)
+ self.next_serial += 1
+ self.generate_crl()
+ return self.certificate
+
+ def certify(self, subject_name, subject_key, validity_interval, is_ca, pathLenConstraint = None):
+ now = rpki.sundial.now()
+ notAfter = now + validity_interval
+ result = self.certificate.bpki_certify(
+ keypair = self.private_key,
+ subject_name = subject_name,
+ subject_key = subject_key,
+ serial = self.next_serial,
+ now = now,
+ notAfter = notAfter,
+ is_ca = is_ca,
+ pathLenConstraint = pathLenConstraint)
+ self.next_serial += 1
+ return result
+
+ def revoke(self, cert):
+ Revocations.objects.create(
+ issuer = self,
+ revoked = rpki.sundial.now(),
+ serial = cert.certificate.getSerial(),
+ expires = cert.certificate.getNotAfter() + crl_interval)
+ cert.delete()
+ self.generate_crl()
+
+ def generate_crl(self):
+ now = rpki.sundial.now()
+ self.revocations.filter(expires__lt = now).delete()
+ revoked = [(r.serial, rpki.sundial.datetime.fromdatetime(r.revoked).toASN1tuple(), ())
+ for r in self.revocations.all()]
+ self.latest_crl = rpki.x509.CRL.generate(
+ keypair = self.private_key,
+ issuer = self.certificate,
+ serial = self.next_crl_number,
+ thisUpdate = now,
+ nextUpdate = now + crl_interval,
+ revokedCertificates = revoked)
+ self.last_crl_update = now
+ self.next_crl_update = now + crl_interval
+ self.next_crl_number += 1
+
+class ServerCA(CA):
+ objects = ServerCAManager()
+
+ def __unicode__(self):
+ return ""
+
+ @property
+ def subject_name(self):
+ if self.certificate is not None:
+ return self.certificate.getSubject()
+ else:
+ return rpki.x509.X501DN("%s BPKI server CA" % socket.gethostname())
+
+class ResourceHolderCA(CA):
+ handle = HandleField(unique = True)
+ objects = ResourceHolderCAManager()
+
+ def __unicode__(self):
+ return self.handle
+
+ @property
+ def subject_name(self):
+ if self.certificate is not None:
+ return self.certificate.getSubject()
+ else:
+ return rpki.x509.X501DN("%s BPKI resource CA" % self.handle)
+
+class Certificate(django.db.models.Model):
+
+ certificate = CertificateField()
+ objects = CertificateManager()
+
+ class Meta:
+ abstract = True
+ unique_together = ("issuer", "handle")
+
+ def revoke(self):
+ self.issuer.revoke(self)
+
+class CrossCertification(Certificate):
+ handle = HandleField()
+ ta = CertificateField()
+
+ class Meta:
+ abstract = True
+
+ def avow(self):
+ self.certificate = self.issuer.certify(
+ subject_name = self.ta.getSubject(),
+ subject_key = self.ta.getPublicKey(),
+ validity_interval = ee_certificate_lifetime,
+ is_ca = True,
+ pathLenConstraint = 0)
+
+ def __unicode__(self):
+ return self.handle
+
+class HostedCA(Certificate):
+ issuer = django.db.models.ForeignKey(ServerCA)
+ hosted = django.db.models.OneToOneField(ResourceHolderCA, related_name = "hosted_by")
+
+ def avow(self):
+ self.certificate = self.issuer.certify(
+ subject_name = self.hosted.certificate.getSubject(),
+ subject_key = self.hosted.certificate.getPublicKey(),
+ validity_interval = ee_certificate_lifetime,
+ is_ca = True,
+ pathLenConstraint = 1)
+
+ class Meta:
+ unique_together = ("issuer", "hosted")
+
+ def __unicode__(self):
+ return self.hosted_ca.handle
+
+class Revocation(django.db.models.Model):
+ serial = django.db.models.BigIntegerField()
+ revoked = SundialField()
+ expires = SundialField()
+
+ class Meta:
+ abstract = True
+ unique_together = ("issuer", "serial")
+
+class ServerRevocation(Revocation):
+ issuer = django.db.models.ForeignKey(ServerCA, related_name = "revocations")
+
+class ResourceHolderRevocation(Revocation):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "revocations")
+
+class EECertificate(Certificate):
+ private_key = RSAKeyField()
+
+ class Meta:
+ abstract = True
+
+ def avow(self):
+ if self.private_key is None:
+ self.private_key = rpki.x509.RSA.generate()
+ self.certificate = self.issuer.certify(
+ subject_name = self.subject_name,
+ subject_key = self.private_key.get_RSApublic(),
+ validity_interval = ee_certificate_lifetime,
+ is_ca = False)
+
+class ServerEE(EECertificate):
+ issuer = django.db.models.ForeignKey(ServerCA, related_name = "ee_certificates")
+ purpose = EnumField(choices = ("rpkid", "pubd", "irdbd", "irbe"))
+
+ class Meta:
+ unique_together = ("issuer", "purpose")
+
+ @property
+ def subject_name(self):
+ return rpki.x509.X501DN("%s BPKI %s EE" % (socket.gethostname(), self.get_purpose_display()))
+
+class Referral(EECertificate):
+ issuer = django.db.models.OneToOneField(ResourceHolderCA, related_name = "referral_certificate")
+ objects = ResourceHolderEEManager()
+
+ @property
+ def subject_name(self):
+ return rpki.x509.X501DN("%s BPKI Referral EE" % self.issuer.handle)
+
+class Turtle(django.db.models.Model):
+ service_uri = django.db.models.CharField(max_length = 255)
+
+class Rootd(EECertificate, Turtle):
+ issuer = django.db.models.OneToOneField(ResourceHolderCA, related_name = "rootd")
+ objects = ResourceHolderEEManager()
+
+ @property
+ def subject_name(self):
+ return rpki.x509.X501DN("%s BPKI rootd EE" % self.issuer.handle)
+
+class BSC(Certificate):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "bscs")
+ handle = HandleField()
+ pkcs10 = PKCS10Field()
+
+ def avow(self):
+ self.certificate = self.issuer.certify(
+ subject_name = self.pkcs10.getSubject(),
+ subject_key = self.pkcs10.getPublicKey(),
+ validity_interval = ee_certificate_lifetime,
+ is_ca = False)
+
+ def __unicode__(self):
+ return self.handle
+
+class Child(CrossCertification):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "children")
+ name = django.db.models.TextField(null = True, blank = True)
+ valid_until = SundialField()
+
+ @property
+ def resource_bag(self):
+ asns = rpki.resource_set.resource_set_as.from_django(
+ (a.start_as, a.end_as) for a in self.asns.all())
+ ipv4 = rpki.resource_set.resource_set_ipv4.from_django(
+ (a.start_ip, a.end_ip) for a in self.address_ranges.filter(version = 'IPv4'))
+ ipv6 = rpki.resource_set.resource_set_ipv6.from_django(
+ (a.start_ip, a.end_ip) for a in self.address_ranges.filter(version = 'IPv6'))
+ return rpki.resource_set.resource_bag(
+ valid_until = self.valid_until, asn = asns, v4 = ipv4, v6 = ipv6)
+
+ # Writing of .setter method deferred until something needs it.
+
+ # This shouldn't be necessary
+ class Meta:
+ unique_together = ("issuer", "handle")
+
+class ChildASN(django.db.models.Model):
+ child = django.db.models.ForeignKey(Child, related_name = "asns")
+ start_as = django.db.models.BigIntegerField()
+ end_as = django.db.models.BigIntegerField()
+
+ def as_resource_range(self):
+ return rpki.resource_set.resource_range_as(self.start_as, self.end_as)
+
+ class Meta:
+ unique_together = ("child", "start_as", "end_as")
+
+class ChildNet(django.db.models.Model):
+ child = django.db.models.ForeignKey(Child, related_name = "address_ranges")
+ start_ip = django.db.models.CharField(max_length = 40)
+ end_ip = django.db.models.CharField(max_length = 40)
+ version = EnumField(choices = ip_version_choices)
+
+ def as_resource_range(self):
+ return rpki.resource_set.resource_range_ip.from_strings(self.start_ip, self.end_ip)
+
+ class Meta:
+ unique_together = ("child", "start_ip", "end_ip", "version")
+
+class Parent(CrossCertification, Turtle):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "parents")
+ parent_handle = HandleField()
+ child_handle = HandleField()
+ repository_type = EnumField(choices = ("none", "offer", "referral"))
+ referrer = HandleField(null = True, blank = True)
+ referral_authorization = SignedReferralField(null = True, blank = True)
+
+ # This shouldn't be necessary
+ class Meta:
+ unique_together = ("issuer", "handle")
+
+class ROARequest(django.db.models.Model):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "roa_requests")
+ asn = django.db.models.BigIntegerField()
+
+ @property
+ def roa_prefix_bag(self):
+ v4 = rpki.resource_set.roa_prefix_set_ipv4.from_django(
+ (p.prefix, p.prefixlen, p.max_prefixlen) for p in self.prefixes.filter(version = 'IPv4'))
+ v6 = rpki.resource_set.roa_prefix_set_ipv6.from_django(
+ (p.prefix, p.prefixlen, p.max_prefixlen) for p in self.prefixes.filter(version = 'IPv6'))
+ return rpki.resource_set.roa_prefix_bag(v4 = v4, v6 = v6)
+
+ # Writing of .setter method deferred until something needs it.
+
+class ROARequestPrefix(django.db.models.Model):
+ roa_request = django.db.models.ForeignKey(ROARequest, related_name = "prefixes")
+ version = EnumField(choices = ip_version_choices)
+ prefix = django.db.models.CharField(max_length = 40)
+ prefixlen = django.db.models.PositiveSmallIntegerField()
+ max_prefixlen = django.db.models.PositiveSmallIntegerField()
+
+ def as_roa_prefix(self):
+ if self.version == 'IPv4':
+ return rpki.resource_set.roa_prefix_ipv4(rpki.ipaddrs.v4addr(self.prefix), self.prefixlen, self.max_prefixlen)
+ else:
+ return rpki.resource_set.roa_prefix_ipv6(rpki.ipaddrs.v6addr(self.prefix), self.prefixlen, self.max_prefixlen)
+
+ def as_resource_range(self):
+ return self.as_roa_prefix().to_resource_range()
+
+ class Meta:
+ unique_together = ("roa_request", "version", "prefix", "prefixlen", "max_prefixlen")
+
+class GhostbusterRequest(django.db.models.Model):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "ghostbuster_requests")
+ parent = django.db.models.ForeignKey(Parent, related_name = "ghostbuster_requests", null = True)
+ vcard = django.db.models.TextField()
+
+class Repository(CrossCertification):
+ issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "repositories")
+ client_handle = HandleField()
+ service_uri = django.db.models.CharField(max_length = 255)
+ sia_base = django.db.models.TextField()
+ turtle = django.db.models.OneToOneField(Turtle, related_name = "repository")
+
+ # This shouldn't be necessary
+ class Meta:
+ unique_together = ("issuer", "handle")
+
+class Client(CrossCertification):
+ issuer = django.db.models.ForeignKey(ServerCA, related_name = "clients")
+ sia_base = django.db.models.TextField()
+ parent_handle = HandleField()
+
+ # This shouldn't be necessary
+ class Meta:
+ unique_together = ("issuer", "handle")
diff --git a/rpkid/rpki/irdb/zookeeper.py b/rpkid/rpki/irdb/zookeeper.py
new file mode 100644
index 00000000..33f5264e
--- /dev/null
+++ b/rpkid/rpki/irdb/zookeeper.py
@@ -0,0 +1,1264 @@
+"""
+Management code for the IRDB.
+
+$Id$
+
+Copyright (C) 2009--2012 Internet Systems Consortium ("ISC")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+"""
+
+import subprocess
+import csv
+import re
+import os
+import getopt
+import sys
+import base64
+import time
+import glob
+import copy
+import warnings
+import rpki.config
+import rpki.cli
+import rpki.sundial
+import rpki.log
+import rpki.oids
+import rpki.http
+import rpki.resource_set
+import rpki.relaxng
+import rpki.exceptions
+import rpki.left_right
+import rpki.x509
+import rpki.async
+import rpki.irdb
+import django.db.transaction
+
+from lxml.etree import (Element, SubElement, ElementTree,
+ fromstring as ElementFromString,
+ tostring as ElementToString)
+
+from rpki.csv_utils import (csv_reader, csv_writer, BadCSVSyntax)
+
+
+
+# XML namespace and protocol version for OOB setup protocol. The name
+# is historical and may change before we propose this as the basis for
+# a standard.
+
+myrpki_namespace = "http://www.hactrn.net/uris/rpki/myrpki/"
+myrpki_version = "2"
+myrpki_namespaceQName = "{" + myrpki_namespace + "}"
+
+myrpki_section = "myrpki"
+irdbd_section = "irdbd"
+rpkid_section = "rpkid"
+pubd_section = "pubd"
+rootd_section = "rootd"
+
+# A whole lot of exceptions
+
+class MissingHandle(Exception): "Missing handle"
+class CouldntTalkToDaemon(Exception): "Couldn't talk to daemon."
+class BadXMLMessage(Exception): "Bad XML message."
+class PastExpiration(Exception): "Expiration date has already passed."
+class CantRunRootd(Exception): "Can't run rootd."
+
+
+
+def B64Element(e, tag, obj, **kwargs):
+ """
+ Create an XML element containing Base64 encoded data taken from a
+ DER object.
+ """
+
+ if e is None:
+ se = Element(tag, **kwargs)
+ else:
+ se = SubElement(e, tag, **kwargs)
+ if e is not None and e.text is None:
+ e.text = "\n"
+ se.text = "\n" + obj.get_Base64()
+ se.tail = "\n"
+ return se
+
+class PEM_writer(object):
+ """
+ Write PEM files to disk, keeping track of which ones we've already
+ written and setting the file mode appropriately.
+ """
+
+ def __init__(self, logstream = None):
+ self.wrote = set()
+ self.logstream = logstream
+
+ def __call__(self, filename, obj):
+ filename = os.path.realpath(filename)
+ if filename in self.wrote:
+ return
+ tempname = filename
+ if not filename.startswith("/dev/"):
+ tempname += ".%s.tmp" % os.getpid()
+ mode = 0400 if filename.endswith(".key") else 0444
+ if self.logstream is not None:
+ self.logstream.write("Writing %s\n" % filename)
+ f = os.fdopen(os.open(tempname, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode), "w")
+ f.write(obj.get_PEM())
+ f.close()
+ if tempname != filename:
+ os.rename(tempname, filename)
+ self.wrote.add(filename)
+
+
+
+
+def etree_read(filename):
+ """
+ Read an etree from a file, verifying then stripping XML namespace
+ cruft.
+ """
+
+ e = ElementTree(file = filename).getroot()
+ rpki.relaxng.myrpki.assertValid(e)
+ for i in e.getiterator():
+ if i.tag.startswith(myrpki_namespaceQName):
+ i.tag = i.tag[len(myrpki_namespaceQName):]
+ else:
+ raise BadXMLMessage, "XML tag %r is not in namespace %r" % (i.tag, myrpki_namespace)
+ return e
+
+
+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):
+ self.msg = msg
+ e = copy.deepcopy(e)
+ e.set("version", myrpki_version)
+ for i in e.getiterator():
+ if i.tag[0] != "{":
+ i.tag = myrpki_namespaceQName + i.tag
+ assert i.tag.startswith(myrpki_namespaceQName)
+ if debug:
+ print ElementToString(e)
+ rpki.relaxng.myrpki.assertValid(e)
+ self.etree = e
+
+ def __str__(self):
+ return ElementToString(self.etree)
+
+ def save(self, filename, logstream = None):
+ filename = os.path.realpath(filename)
+ tempname = filename
+ if not filename.startswith("/dev/"):
+ tempname += ".%s.tmp" % os.getpid()
+ ElementTree(self.etree).write(tempname)
+ if tempname != filename:
+ os.rename(tempname, filename)
+ if logstream is not None:
+ logstream.write("Wrote %s\n" % filename)
+ if self.msg is not None:
+ logstream.write(self.msg + "\n")
+
+
+
+class Zookeeper(object):
+
+ ## @var show_xml
+ # Whether to show XML for debugging
+
+ show_xml = False
+
+ def __init__(self, cfg = None, handle = None, logstream = None):
+
+ if cfg is None:
+ cfg = rpki.config.parser()
+
+ if handle is None:
+ handle = cfg.get("handle", section = myrpki_section)
+
+ self.cfg = cfg
+
+ self.logstream = logstream
+
+ self.run_rpkid = cfg.getboolean("run_rpkid", section = myrpki_section)
+ self.run_pubd = cfg.getboolean("run_pubd", section = myrpki_section)
+ self.run_rootd = cfg.getboolean("run_rootd", section = myrpki_section)
+
+ if self.run_rootd and (not self.run_pubd or not self.run_rpkid):
+ raise CantRunRootd, "Can't run rootd unless also running rpkid and pubd"
+
+ self.default_repository = cfg.get("default_repository", "", section = myrpki_section)
+ self.pubd_contact_info = cfg.get("pubd_contact_info", "", section = myrpki_section)
+
+ self.rsync_module = cfg.get("publication_rsync_module", section = myrpki_section)
+ self.rsync_server = cfg.get("publication_rsync_server", section = myrpki_section)
+
+ self.reset_identity(handle)
+
+
+ def reset_identity(self, handle):
+ """
+ Select handle of current resource holding entity.
+ """
+
+ if handle is None:
+ raise MissingHandle
+ self.handle= handle
+
+
+ def set_logstream(self, logstream):
+ """
+ Set log stream for this Zookeeper. The log stream is a file-like
+ object, or None to suppress all logging.
+ """
+
+ self.logstream = logstream
+
+
+ def log(self, msg):
+ """
+ Send some text to this Zookeeper's log stream, if one is set.
+ """
+
+ if self.logstream is not None:
+ self.logstream.write(msg)
+ self.logstream.write("\n")
+
+
+ @property
+ def resource_ca(self):
+ """
+ Get ResourceHolderCA object associated with current handle.
+ """
+
+ assert self.handle is not None
+ try:
+ return rpki.irdb.ResourceHolderCA.objects.get(handle = self.handle)
+ except rpki.irdb.ResourceHolderCA.DoesNotExist:
+ return None
+
+
+ @property
+ def server_ca(self):
+ """
+ Get ServerCA object.
+ """
+
+ try:
+ return rpki.irdb.ServerCA.objects.get()
+ except rpki.irdb.ServerCA.DoesNotExist:
+ return None
+
+
+ @django.db.transaction.commit_on_success
+ def initialize(self):
+ """
+ Initialize an RPKI installation. Reads the configuration file,
+ creates the BPKI and EntityDB directories, generates the initial
+ BPKI certificates, and creates an XML file describing the
+ resource-holding aspect of this RPKI installation.
+ """
+
+ resource_ca, created = rpki.irdb.ResourceHolderCA.objects.get_or_certify(handle = self.handle)
+
+ if self.run_rpkid or self.run_pubd:
+ server_ca, created = rpki.irdb.ServerCA.objects.get_or_certify()
+ rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "irbe")
+
+ if self.run_rpkid:
+ rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "rpkid")
+ rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "irdbd")
+
+ if self.run_pubd:
+ rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "pubd")
+
+ return self.generate_identity()
+
+
+ def generate_identity(self):
+ """
+ Generate identity XML. Broken out of .initialize() because it's
+ easier for the GUI this way.
+ """
+
+ e = Element("identity", handle = self.handle)
+ B64Element(e, "bpki_ta", self.resource_ca.certificate)
+ return etree_wrapper(e, msg = 'This is the "identity" file you will need to send to your parent')
+
+
+ @django.db.transaction.commit_on_success
+ def delete_self(self):
+ """
+ Delete the ResourceHolderCA object corresponding to the current handle.
+ This corresponds to deleting an rpkid <self/> object.
+
+ This code assumes the normal Django cascade-on-delete behavior,
+ that is, we assume that deleting the ResourceHolderCA object
+ deletes all the subordinate objects that refer to it via foreign
+ key relationships.
+ """
+
+ resource_ca = self.resource_ca
+ if resource_ca is not None:
+ resource_ca.delete()
+ else:
+ self.log("No such ResourceHolderCA \"%s\"" % self.handle)
+
+
+ @django.db.transaction.commit_on_success
+ def configure_rootd(self):
+
+ assert self.run_rpkid and self.run_pubd and self.run_rootd
+
+ rpki.irdb.Rootd.objects.get_or_certify(
+ issuer = self.resource_ca,
+ service_uri = "http://localhost:%s/" % self.cfg.get("rootd_server_port", section = myrpki_section))
+
+ return self.generate_rootd_repository_offer()
+
+
+ def generate_rootd_repository_offer(self):
+ """
+ Generate repository offer for rootd. Split out of
+ configure_rootd() because that's easier for the GUI.
+ """
+
+ # The following assumes we'll set up the respository manually.
+ # Not sure this is a reasonable assumption, particularly if we
+ # ever fix rootd to use the publication protocol.
+
+ try:
+ self.resource_ca.repositories.get(handle = self.handle)
+ return None
+
+ except rpki.irdb.Repository.DoesNotExist:
+ e = Element("repository", type = "offer", handle = self.handle, parent_handle = self.handle)
+ B64Element(e, "bpki_client_ta", self.resource_ca.certificate)
+ return etree_wrapper(e, msg = 'This is the "repository offer" file for you to use if you want to publish in your own repository')
+
+
+ def write_bpki_files(self):
+ """
+ Write out BPKI certificate, key, and CRL files for daemons that
+ need them.
+ """
+
+ writer = PEM_writer(self.logstream)
+
+ if self.run_rpkid:
+ rpkid = self.server_ca.ee_certificates.get(purpose = "rpkid")
+ writer(self.cfg.get("bpki-ta", section = rpkid_section), self.server_ca.certificate)
+ writer(self.cfg.get("rpkid-key", section = rpkid_section), rpkid.private_key)
+ writer(self.cfg.get("rpkid-cert", section = rpkid_section), rpkid.certificate)
+ writer(self.cfg.get("irdb-cert", section = rpkid_section),
+ self.server_ca.ee_certificates.get(purpose = "irdbd").certificate)
+ writer(self.cfg.get("irbe-cert", section = rpkid_section),
+ self.server_ca.ee_certificates.get(purpose = "irbe").certificate)
+
+ if self.run_pubd:
+ pubd = self.server_ca.ee_certificates.get(purpose = "pubd")
+ writer(self.cfg.get("bpki-ta", section = pubd_section), self.server_ca.certificate)
+ writer(self.cfg.get("pubd-key", section = pubd_section), pubd.private_key)
+ writer(self.cfg.get("pubd-cert", section = pubd_section), pubd.certificate)
+ writer(self.cfg.get("irbe-cert", section = pubd_section),
+ self.server_ca.ee_certificates.get(purpose = "irbe").certificate)
+
+ if self.run_rootd:
+ rootd = rpki.irdb.ResourceHolderCA.objects.get(handle = self.cfg.get("handle", section = myrpki_section)).rootd
+ writer(self.cfg.get("bpki-ta", section = rootd_section), self.server_ca.certificate)
+ writer(self.cfg.get("rootd-bpki-crl", section = rootd_section), self.server_ca.latest_crl)
+ writer(self.cfg.get("rootd-bpki-key", section = rootd_section), rootd.private_key)
+ writer(self.cfg.get("rootd-bpki-cert", section = rootd_section), rootd.certificate)
+ writer(self.cfg.get("child-bpki-cert", section = rootd_section), rootd.issuer.certificate)
+
+
+ @django.db.transaction.commit_on_success
+ def update_bpki(self):
+ """
+ Update BPKI certificates. Assumes an existing RPKI installation.
+
+ Basic plan here is to reissue all BPKI certificates we can, right
+ now. In the long run we might want to be more clever about only
+ touching ones that need maintenance, but this will do for a start.
+
+ We also reissue CRLs for all CAs.
+
+ Most likely this should be run under cron.
+ """
+
+ for model in (rpki.irdb.ServerCA,
+ rpki.irdb.ResourceHolderCA,
+ rpki.irdb.ServerEE,
+ rpki.irdb.Referral,
+ rpki.irdb.Rootd,
+ rpki.irdb.HostedCA,
+ rpki.irdb.BSC,
+ rpki.irdb.Child,
+ rpki.irdb.Parent,
+ rpki.irdb.Client,
+ rpki.irdb.Repository):
+ for obj in model.objects.all():
+ self.log("Regenerating certificate %s" % obj.certificate.getSubject())
+ obj.avow()
+ obj.save()
+
+ self.log("Regenerating Server CRL")
+ self.server_ca.generate_crl()
+ self.server_ca.save()
+
+ for ca in rpki.irdb.ResourceHolderCA.objects.all():
+ self.log("Regenerating CRL for %s" % ca.handle)
+ ca.generate_crl()
+ ca.save()
+
+
+ @django.db.transaction.commit_on_success
+ def configure_child(self, filename, child_handle = None):
+ """
+ Configure a new child of this RPKI entity, given the child's XML
+ identity file as an input. Extracts the child's data from the
+ XML, cross-certifies the child's resource-holding BPKI
+ certificate, and generates an XML file describing the relationship
+ between the child and this parent, including this parent's BPKI
+ data and up-down protocol service URI.
+ """
+
+ c = etree_read(filename)
+
+ if child_handle is None:
+ child_handle = c.get("handle")
+
+ valid_until = rpki.sundial.now() + rpki.sundial.timedelta(days = 365)
+
+ self.log("Child calls itself %r, we call it %r" % (c.get("handle"), child_handle))
+
+ child, created = rpki.irdb.Child.objects.get_or_certify(
+ issuer = self.resource_ca,
+ handle = child_handle,
+ ta = rpki.x509.X509(Base64 = c.findtext("bpki_ta")),
+ valid_until = valid_until)
+
+ return self.generate_parental_response(child), child_handle
+
+
+ @django.db.transaction.commit_on_success
+ def generate_parental_response(self, child):
+ """
+ Generate parental response XML. Broken out of .configure_child()
+ for GUI.
+ """
+
+ service_uri = "http://%s:%s/up-down/%s/%s" % (
+ self.cfg.get("rpkid_server_host", section = myrpki_section),
+ self.cfg.get("rpkid_server_port", section = myrpki_section),
+ self.handle, child.handle)
+
+ e = Element("parent", parent_handle = self.handle, child_handle = child.handle,
+ service_uri = service_uri, valid_until = str(child.valid_until))
+ B64Element(e, "bpki_resource_ta", self.resource_ca.certificate)
+ B64Element(e, "bpki_child_ta", child.ta)
+
+ try:
+ if self.default_repository:
+ repo = self.resource_ca.repositories.get(handle = self.default_repository)
+ else:
+ repo = self.resource_ca.repositories.get()
+ except rpki.irdb.Repository.DoesNotExist:
+ repo = None
+
+ if repo is None:
+ self.log("Couldn't find any usable repositories, not giving referral")
+
+ elif repo.handle == self.handle:
+ SubElement(e, "repository", type = "offer")
+
+ else:
+ proposed_sia_base = repo.sia_base + child.handle + "/"
+ referral_cert, created = rpki.irdb.Referral.objects.get_or_certify(issuer = self.resource_ca)
+ auth = rpki.x509.SignedReferral()
+ auth.set_content(B64Element(None, myrpki_namespaceQName + "referral", child.ta,
+ version = myrpki_version,
+ authorized_sia_base = proposed_sia_base))
+ auth.schema_check()
+ auth.sign(referral_cert.private_key, referral_cert.certificate, self.resource_ca.latest_crl)
+
+ r = SubElement(e, "repository", type = "referral")
+ B64Element(r, "authorization", auth, referrer = repo.client_handle)
+ SubElement(r, "contact_info")
+
+ return etree_wrapper(e, msg = "Send this file back to the child you just configured")
+
+
+ @django.db.transaction.commit_on_success
+ def delete_child(self, child_handle):
+ """
+ Delete a child of this RPKI entity.
+ """
+
+ assert child_handle is not None
+ try:
+ self.resource_ca.children.get(handle = child_handle).delete()
+ except rpki.irdb.Child.DoesNotExist:
+ self.log("No such child \"%s\"" % arg)
+
+
+ @django.db.transaction.commit_on_success
+ def configure_parent(self, filename, parent_handle = None):
+ """
+ Configure a new parent of this RPKI entity, given the output of
+ the parent's configure_child command as input. Reads the parent's
+ response XML, extracts the parent's BPKI and service URI
+ information, cross-certifies the parent's BPKI data into this
+ entity's BPKI, and checks for offers or referrals of publication
+ service. If a publication offer or referral is present, we
+ generate a request-for-service message to that repository, in case
+ the user wants to avail herself of the referral or offer.
+ """
+
+ p = etree_read(filename)
+
+ if parent_handle is None:
+ parent_handle = p.get("parent_handle")
+
+ r = p.find("repository")
+
+ repository_type = "none"
+ referrer = None
+ referral_authorization = None
+
+ if r is not None:
+ repository_type = r.get("type")
+
+ if repository_type == "referral":
+ a = r.find("authorization")
+ referrer = a.get("referrer")
+ referral_authorization = rpki.x509.SignedReferral(Base64 = a.text)
+
+ self.log("Parent calls itself %r, we call it %r" % (p.get("parent_handle"), parent_handle))
+ self.log("Parent calls us %r" % p.get("child_handle"))
+
+ parent, created = rpki.irdb.Parent.objects.get_or_certify(
+ issuer = self.resource_ca,
+ handle = parent_handle,
+ child_handle = p.get("child_handle"),
+ parent_handle = p.get("parent_handle"),
+ service_uri = p.get("service_uri"),
+ ta = rpki.x509.X509(Base64 = p.findtext("bpki_resource_ta")),
+ repository_type = repository_type,
+ referrer = referrer,
+ referral_authorization = referral_authorization)
+
+ return self.generate_repository_request(parent), parent_handle
+
+
+ def generate_repository_request(self, parent):
+ """
+ Generate repository request for a given parent.
+ """
+
+ e = Element("repository", handle = self.handle,
+ parent_handle = parent.handle, type = parent.repository_type)
+ if parent.repository_type == "referral":
+ B64Element(e, "authorization", parent.referral_authorization, referrer = parent.referrer)
+ SubElement(e, "contact_info")
+ B64Element(e, "bpki_client_ta", self.resource_ca.certificate)
+ return etree_wrapper(e, msg = "This is the file to send to the repository operator")
+
+
+ @django.db.transaction.commit_on_success
+ def delete_parent(self, parent_handle):
+ """
+ Delete a parent of this RPKI entity.
+ """
+
+ assert parent_handle is not None
+ try:
+ self.resource_ca.parents.get(handle = parent_handle).delete()
+ except rpki.irdb.Parent.DoesNotExist:
+ self.log("No such parent \"%s\"" % arg)
+
+
+ @django.db.transaction.commit_on_success
+ def configure_publication_client(self, filename, sia_base = None):
+ """
+ Configure publication server to know about a new client, given the
+ client's request-for-service message as input. Reads the client's
+ request for service, cross-certifies the client's BPKI data, and
+ generates a response message containing the repository's BPKI data
+ and service URI.
+ """
+
+ client = etree_read(filename)
+
+ client_ta = rpki.x509.X509(Base64 = client.findtext("bpki_client_ta"))
+
+ if sia_base is None and client.get("handle") == self.handle and self.resource_ca.certificate == client_ta:
+ self.log("This looks like self-hosted publication")
+ sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, self.handle)
+
+ if sia_base is None and client.get("type") == "referral":
+ self.log("This looks like a referral, checking")
+ try:
+ auth = client.find("authorization")
+ referrer = self.server_ca.clients.get(handle = auth.get("referrer"))
+ referral_cms = rpki.x509.SignedReferral(Base64 = auth.text)
+ referral_xml = referral_cms.unwrap(ta = (referrer.certificate, self.server_ca.certificate))
+ if rpki.x509.X509(Base64 = referral_xml.text) != client_ta:
+ raise BadXMLMessage, "Referral trust anchor does not match"
+ sia_base = referral_xml.get("authorized_sia_base")
+ except rpki.irdb.Client.DoesNotExist:
+ self.log("We have no record of the client (%s) alleged to have made this referral" % auth.get("referrer"))
+
+ if sia_base is None and client.get("type") == "offer" and client.get("parent_handle") == self.handle:
+ self.log("This looks like an offer, client claims to be our child, checking")
+ try:
+ child = self.resource_ca.children.get(ta = client_ta)
+ except rpki.irdb.Child.DoesNotExist:
+ self.log("Can't find a child matching this client")
+ else:
+ sia_base = "rsync://%s/%s/%s/%s/" % (self.rsync_server, self.rsync_module,
+ self.handle, client.get("handle"))
+
+ # If we still haven't figured out what to do with this client, it
+ # gets a top-level tree of its own, no attempt at nesting.
+
+ if sia_base is None:
+ self.log("Don't know where to nest this client, defaulting to top-level")
+ sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, client.get("handle"))
+
+ if not sia_base.startswith("rsync://"):
+ raise BadXMLMessage, "Malformed sia_base parameter %r, should start with 'rsync://'" % sia_base
+
+ client_handle = "/".join(sia_base.rstrip("/").split("/")[4:])
+
+ parent_handle = client.get("parent_handle")
+
+ self.log("Client calls itself %r, we call it %r" % (client.get("handle"), client_handle))
+ self.log("Client says its parent handle is %r" % parent_handle)
+
+ client, created = rpki.irdb.Client.objects.get_or_certify(
+ issuer = self.server_ca,
+ handle = client_handle,
+ parent_handle = parent_handle,
+ ta = client_ta,
+ sia_base = sia_base)
+
+ return self.generate_repository_response(client), client_handle
+
+
+ def generate_repository_response(self, client):
+ """
+ Generate repository response XML to a given client.
+ """
+
+ service_uri = "http://%s:%s/client/%s" % (
+ self.cfg.get("pubd_server_host", section = myrpki_section),
+ self.cfg.get("pubd_server_port", section = myrpki_section),
+ client.handle)
+
+ e = Element("repository", type = "confirmed",
+ client_handle = client.handle,
+ parent_handle = client.parent_handle,
+ sia_base = client.sia_base,
+ service_uri = service_uri)
+
+ B64Element(e, "bpki_server_ta", self.server_ca.certificate)
+ B64Element(e, "bpki_client_ta", client.ta)
+ SubElement(e, "contact_info").text = self.pubd_contact_info
+ return etree_wrapper(e, msg = "Send this file back to the publication client you just configured")
+
+
+ @django.db.transaction.commit_on_success
+ def delete_publication_client(self, client_handle):
+ """
+ Delete a publication client of this RPKI entity.
+ """
+
+ assert client_handle is not None
+ try:
+ self.server_ca.clients.get(handle = client_handle).delete()
+ except rpki.irdb.Client.DoesNotExist:
+ self.log("No such client \"%s\"" % arg)
+
+
+ @django.db.transaction.commit_on_success
+ def configure_repository(self, filename, parent_handle = None):
+ """
+ Configure a publication repository for this RPKI entity, given the
+ repository's response to our request-for-service message as input.
+ Reads the repository's response, extracts and cross-certifies the
+ BPKI data and service URI, and links the repository data with the
+ corresponding parent data in our local database.
+ """
+
+ r = etree_read(filename)
+
+ if parent_handle is None:
+ parent_handle = r.get("parent_handle")
+
+ self.log("Repository calls us %r" % (r.get("client_handle")))
+ self.log("Repository response associated with parent_handle %r" % parent_handle)
+
+ try:
+ if parent_handle == self.handle:
+ turtle = self.resource_ca.rootd
+ else:
+ turtle = self.resource_ca.parents.get(handle = parent_handle)
+
+ except (rpki.irdb.Parent.DoesNotExist, rpki.irdb.Rootd.DoesNotExist):
+ self.log("Could not find parent %r in our database" % parent_handle)
+
+ else:
+ rpki.irdb.Repository.objects.get_or_certify(
+ issuer = self.resource_ca,
+ handle = parent_handle,
+ client_handle = r.get("client_handle"),
+ service_uri = r.get("service_uri"),
+ sia_base = r.get("sia_base"),
+ ta = rpki.x509.X509(Base64 = r.findtext("bpki_server_ta")),
+ turtle = turtle)
+
+
+ @django.db.transaction.commit_on_success
+ def delete_repository(self, repository_handle):
+ """
+ Delete a repository of this RPKI entity.
+ """
+
+ assert repository_handle is not None
+ try:
+ self.resource_ca.repositories.get(handle = arg).delete()
+ except rpki.irdb.Repository.DoesNotExist:
+ self.log("No such repository \"%s\"" % arg)
+
+
+ @django.db.transaction.commit_on_success
+ def renew_children(self, child_handle, valid_until = None):
+ """
+ Update validity period for one child entity or, if child_handle is
+ None, for all child entities.
+ """
+
+ if child_handle is None:
+ children = self.resource_ca.children.all()
+ else:
+ children = self.resource_ca.children.filter(handle = child_handle)
+
+ if valid_until is None:
+ valid_until = rpki.sundial.now() + rpki.sundial.timedelta(days = 365)
+ else:
+ valid_until = rpki.sundial.fromXMLtime(valid_until)
+ if valid_until < rpki.sundial.now():
+ raise PastExpiration, "Specified new expiration time %s has passed" % valid_until
+
+ self.log("New validity date %s" % valid_until)
+
+ for child in children:
+ child.valid_until = valid_until
+ child.save()
+
+
+ @django.db.transaction.commit_on_success
+ def load_prefixes(self, filename):
+ """
+ Whack IRDB to match prefixes.csv.
+ """
+
+ grouped4 = {}
+ grouped6 = {}
+
+ for handle, prefix in csv_reader(filename, columns = 2):
+ grouped = grouped6 if ":" in prefix else grouped4
+ if handle not in grouped:
+ grouped[handle] = []
+ grouped[handle].append(prefix)
+
+ primary_keys = []
+
+ for version, grouped, rset in ((4, grouped4, rpki.resource_set.resource_set_ipv4),
+ (6, grouped6, rpki.resource_set.resource_set_ipv6)):
+ for handle, prefixes in grouped.iteritems():
+ child = self.resource_ca.children.get(handle = handle)
+ for prefix in rset(",".join(prefixes)):
+ obj, created = rpki.irdb.ChildNet.objects.get_or_create(
+ child = child,
+ start_ip = str(prefix.min),
+ end_ip = str(prefix.max),
+ version = version)
+ primary_keys.append(obj.pk)
+
+ q = rpki.irdb.ChildNet.objects
+ q = q.filter(child__issuer__exact = self.resource_ca)
+ q = q.exclude(pk__in = primary_keys)
+ q.delete()
+
+
+ @django.db.transaction.commit_on_success
+ def load_asns(self, filename):
+ """
+ Whack IRDB to match asns.csv.
+ """
+
+ grouped = {}
+
+ for handle, asn in csv_reader(filename, columns = 2):
+ if handle not in grouped:
+ grouped[handle] = []
+ grouped[handle].append(asn)
+
+ primary_keys = []
+
+ for handle, asns in grouped.iteritems():
+ child = self.resource_ca.children.get(handle = handle)
+ for asn in rpki.resource_set.resource_set_as(",".join(asns)):
+ obj, created = rpki.irdb.ChildASN.objects.get_or_create(
+ child = child,
+ start_as = str(asn.min),
+ end_as = str(asn.max))
+ primary_keys.append(obj.pk)
+
+ q = rpki.irdb.ChildASN.objects
+ q = q.filter(child__issuer__exact = self.resource_ca)
+ q = q.exclude(pk__in = primary_keys)
+ q.delete()
+
+
+ @django.db.transaction.commit_on_success
+ def load_roa_requests(self, filename):
+ """
+ Whack IRDB to match roa.csv.
+ """
+
+ grouped = {}
+
+ # format: p/n-m asn group
+ for pnm, asn, group in csv_reader(filename, columns = 3):
+ key = (asn, group)
+ if key not in grouped:
+ grouped[key] = []
+ grouped[key].append(pnm)
+
+ # Deleting and recreating all the ROA requests is inefficient,
+ # but rpkid's current representation of ROA requests is wrong
+ # (see #32), so it's not worth a lot of effort here as we're
+ # just going to have to rewrite this soon anyway.
+
+ self.resource_ca.roa_requests.all().delete()
+
+ for key, pnms in grouped.iteritems():
+ asn, group = key
+
+ roa_request = self.resource_ca.roa_requests.create(asn = asn)
+
+ for pnm in pnms:
+ if ":" in pnm:
+ p = rpki.resource_set.roa_prefix_ipv6.parse_str(pnm)
+ v = 6
+ else:
+ p = rpki.resource_set.roa_prefix_ipv4.parse_str(pnm)
+ v = 4
+ roa_request.prefixes.create(
+ version = v,
+ prefix = str(p.prefix),
+ prefixlen = int(p.prefixlen),
+ max_prefixlen = int(p.max_prefixlen))
+
+
+ def call_rpkid(self, *pdus):
+ """
+ Issue a call to rpkid, return result.
+
+ Implementation is a little silly, constructs a wrapper object,
+ invokes it once, then throws it away. Hard to do better without
+ rewriting a bit of the HTTP code, as we want to be sure we're
+ using the current BPKI certificate and key objects.
+ """
+
+ url = "http://%s:%s/left-right" % (
+ self.cfg.get("rpkid_server_host", section = myrpki_section),
+ self.cfg.get("rpkid_server_port", section = myrpki_section))
+
+ rpkid = self.server_ca.ee_certificates.get(purpose = "rpkid")
+ irbe = self.server_ca.ee_certificates.get(purpose = "irbe")
+
+ call_rpkid = rpki.async.sync_wrapper(rpki.http.caller(
+ proto = rpki.left_right,
+ client_key = irbe.private_key,
+ client_cert = irbe.certificate,
+ server_ta = self.server_ca.certificate,
+ server_cert = rpkid.certificate,
+ url = url,
+ debug = self.show_xml))
+
+ return call_rpkid(*pdus)
+
+
+ def run_rpkid_now(self):
+ """
+ Poke rpkid to immediately run the cron job for the current handle.
+
+ This method is used by the gui when a user has changed something in the
+ IRDB (ghostbuster, roa) which does not require a full synchronize() call,
+ to force the object to be immediately issued.
+ """
+
+ self.call_rpkid(rpki.left_right.self_elt.make_pdu(
+ action = "set", self_handle = self.handle, run_now = "yes"))
+
+
+ def call_pubd(self, *pdus):
+ """
+ Issue a call to pubd, return result.
+
+ Implementation is a little silly, constructs a wrapper object,
+ invokes it once, then throws it away. Hard to do better without
+ rewriting a bit of the HTTP code, as we want to be sure we're
+ using the current BPKI certificate and key objects.
+ """
+
+ url = "http://%s:%s/control" % (
+ self.cfg.get("pubd_server_host", section = myrpki_section),
+ self.cfg.get("pubd_server_port", section = myrpki_section))
+
+ pubd = self.server_ca.ee_certificates.get(purpose = "pubd")
+ irbe = self.server_ca.ee_certificates.get(purpose = "irbe")
+
+ call_pubd = rpki.async.sync_wrapper(rpki.http.caller(
+ proto = rpki.publication,
+ client_key = irbe.private_key,
+ client_cert = irbe.certificate,
+ server_ta = self.server_ca.certificate,
+ server_cert = pubd.certificate,
+ url = url,
+ debug = self.show_xml))
+
+ return call_pubd(*pdus)
+
+
+ def check_error_report(self, pdus):
+ """
+ Check a response from rpkid or pubd for error_report PDUs, log and
+ throw exceptions as needed.
+ """
+
+ if any(isinstance(pdu, (rpki.left_right.report_error_elt, rpki.publication.report_error_elt)) for pdu in pdus):
+ for pdu in pdus:
+ if isinstance(pdu, rpki.left_right.report_error_elt):
+ self.log("rpkid reported failure: %s" % pdu.error_code)
+ elif isinstance(pdu, rpki.publication.report_error_elt):
+ self.log("pubd reported failure: %s" % pdu.error_code)
+ else:
+ continue
+ if pdu.error_text:
+ self.log(pdu.error_text)
+ raise CouldntTalkToDaemon
+
+
+ @django.db.transaction.commit_on_success
+ def synchronize(self, *handles_to_poke):
+ """
+ Configure RPKI daemons with the data built up by the other
+ commands in this program. Most commands which modify the IRDB
+ should call this when they're done.
+
+ Any arguments given are handles to be sent to rpkid at the end of
+ the synchronization run with a <self run_now="yes"/> operation.
+ """
+
+ # We can use a single BSC for everything -- except BSC key
+ # rollovers. Drive off that bridge when we get to it.
+
+ bsc_handle = "bsc"
+
+ # Default values for CRL parameters are low, for testing. Not
+ # quite as low as they once were, too much expired CRL whining.
+
+ self_crl_interval = self.cfg.getint("self_crl_interval", 2 * 60 * 60,
+ section = myrpki_section)
+ self_regen_margin = self.cfg.getint("self_regen_margin", self_crl_interval / 4,
+ section = myrpki_section)
+
+ # Make sure that pubd's BPKI CRL is up to date.
+
+ if self.run_pubd:
+ self.call_pubd(rpki.publication.config_elt.make_pdu(
+ action = "set",
+ bpki_crl = self.server_ca.latest_crl))
+
+ for ca in rpki.irdb.ResourceHolderCA.objects.all():
+
+ # See what rpkid and pubd already have on file for this entity.
+
+ if self.run_pubd:
+ pubd_reply = self.call_pubd(rpki.publication.client_elt.make_pdu(action = "list"))
+ client_pdus = dict((x.client_handle, x) for x in pubd_reply if isinstance(x, rpki.publication.client_elt))
+
+ rpkid_reply = self.call_rpkid(
+ rpki.left_right.self_elt.make_pdu( action = "get", tag = "self", self_handle = ca.handle),
+ rpki.left_right.bsc_elt.make_pdu( action = "list", tag = "bsc", self_handle = ca.handle),
+ rpki.left_right.repository_elt.make_pdu(action = "list", tag = "repository", self_handle = ca.handle),
+ rpki.left_right.parent_elt.make_pdu( action = "list", tag = "parent", self_handle = ca.handle),
+ rpki.left_right.child_elt.make_pdu( action = "list", tag = "child", self_handle = ca.handle))
+
+ self_pdu = rpkid_reply[0]
+ bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
+ repository_pdus = dict((x.repository_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.repository_elt))
+ parent_pdus = dict((x.parent_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.parent_elt))
+ child_pdus = dict((x.child_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.child_elt))
+
+ pubd_query = []
+ rpkid_query = []
+
+ self_cert, created = rpki.irdb.HostedCA.objects.get_or_certify(
+ issuer = self.server_ca,
+ hosted = ca)
+
+ # There should be exactly one <self/> object per hosted entity, by definition
+
+ if (isinstance(self_pdu, rpki.left_right.report_error_elt) or
+ self_pdu.crl_interval != self_crl_interval or
+ self_pdu.regen_margin != self_regen_margin or
+ self_pdu.bpki_cert != self_cert.certificate):
+ rpkid_query.append(rpki.left_right.self_elt.make_pdu(
+ action = "create" if isinstance(self_pdu, rpki.left_right.report_error_elt) else "set",
+ tag = "self",
+ self_handle = ca.handle,
+ bpki_cert = ca.certificate,
+ crl_interval = self_crl_interval,
+ regen_margin = self_regen_margin))
+
+ # In general we only need one <bsc/> per <self/>. BSC objects
+ # are a little unusual in that the keypair and PKCS #10
+ # subelement is generated by rpkid, so complete setup requires
+ # two round trips.
+
+ bsc_pdu = bsc_pdus.pop(bsc_handle, None)
+
+ if bsc_pdu is None:
+ rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
+ action = "create",
+ tag = "bsc",
+ self_handle = ca.handle,
+ bsc_handle = bsc_handle,
+ generate_keypair = "yes"))
+
+ elif bsc_pdu.pkcs10_request is None:
+ rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
+ action = "set",
+ tag = "bsc",
+ self_handle = ca.handle,
+ bsc_handle = bsc_handle,
+ generate_keypair = "yes"))
+
+ rpkid_query.extend(rpki.left_right.bsc_elt.make_pdu(
+ action = "destroy", self_handle = ca.handle, bsc_handle = b) for b in bsc_pdus)
+
+ # If we've already got actions queued up, run them now, so we
+ # can finish setting up the BSC before anything tries to use it.
+
+ if rpkid_query:
+ rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(action = "list", tag = "bsc", self_handle = ca.handle))
+ rpkid_reply = self.call_rpkid(*rpkid_query)
+ bsc_pdus = dict((x.bsc_handle, x)
+ for x in rpkid_reply
+ if isinstance(x, rpki.left_right.bsc_elt) and x.action == "list")
+ bsc_pdu = bsc_pdus.pop(bsc_handle, None)
+ self.check_error_report(rpkid_reply)
+
+ rpkid_query = []
+
+ assert bsc_pdu.pkcs10_request is not None
+
+ bsc, created = rpki.irdb.BSC.objects.get_or_certify(
+ issuer = ca,
+ handle = bsc_handle,
+ pkcs10 = bsc_pdu.pkcs10_request)
+
+ if bsc_pdu.signing_cert != bsc.certificate or bsc_pdu.signing_cert_crl != ca.latest_crl:
+ rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(
+ action = "set",
+ tag = "bsc",
+ self_handle = ca.handle,
+ bsc_handle = bsc_handle,
+ signing_cert = bsc.certificate,
+ signing_cert_crl = ca.latest_crl))
+
+ # At present we need one <repository/> per <parent/>, not because
+ # rpkid requires that, but because pubd does. pubd probably should
+ # be fixed to support a single client allowed to update multiple
+ # trees, but for the moment the easiest way forward is just to
+ # enforce a 1:1 mapping between <parent/> and <repository/> objects
+
+ for repository in ca.repositories.all():
+
+ repository_pdu = repository_pdus.pop(repository.handle, None)
+
+ if (repository_pdu is None or
+ repository_pdu.bsc_handle != bsc_handle or
+ repository_pdu.peer_contact_uri != repository.service_uri or
+ repository_pdu.bpki_cert != repository.certificate):
+ rpkid_query.append(rpki.left_right.repository_elt.make_pdu(
+ action = "create" if repository_pdu is None else "set",
+ tag = repository.handle,
+ self_handle = ca.handle,
+ repository_handle = repository.handle,
+ bsc_handle = bsc_handle,
+ peer_contact_uri = repository.service_uri,
+ bpki_cert = repository.certificate))
+
+ rpkid_query.extend(rpki.left_right.repository_elt.make_pdu(
+ action = "destroy", self_handle = ca.handle, repository_handle = r) for r in repository_pdus)
+
+ # <parent/> setup code currently assumes 1:1 mapping between
+ # <repository/> and <parent/>, and further assumes that the handles
+ # for an associated pair are the identical (that is:
+ # parent.repository_handle == parent.parent_handle).
+
+ for parent in ca.parents.all():
+
+ parent_pdu = parent_pdus.pop(parent.handle, None)
+
+ if (parent_pdu is None or
+ parent_pdu.bsc_handle != bsc_handle or
+ parent_pdu.repository_handle != parent.handle or
+ parent_pdu.peer_contact_uri != parent.service_uri or
+ parent_pdu.sia_base != parent.repository.sia_base or
+ parent_pdu.sender_name != parent.child_handle or
+ parent_pdu.recipient_name != parent.parent_handle or
+ parent_pdu.bpki_cms_cert != parent.certificate):
+ rpkid_query.append(rpki.left_right.parent_elt.make_pdu(
+ action = "create" if parent_pdu is None else "set",
+ tag = parent.handle,
+ self_handle = ca.handle,
+ parent_handle = parent.handle,
+ bsc_handle = bsc_handle,
+ repository_handle = parent.handle,
+ peer_contact_uri = parent.service_uri,
+ sia_base = parent.repository.sia_base,
+ sender_name = parent.child_handle,
+ recipient_name = parent.parent_handle,
+ bpki_cms_cert = parent.certificate))
+
+ try:
+
+ parent_pdu = parent_pdus.pop(ca.handle, None)
+
+ if (parent_pdu is None or
+ parent_pdu.bsc_handle != bsc_handle or
+ parent_pdu.repository_handle != ca.handle or
+ parent_pdu.peer_contact_uri != ca.rootd.service_uri or
+ parent_pdu.sia_base != ca.rootd.repository.sia_base or
+ parent_pdu.sender_name != ca.handle or
+ parent_pdu.recipient_name != ca.handle or
+ parent_pdu.bpki_cms_cert != ca.rootd.certificate):
+ rpkid_query.append(rpki.left_right.parent_elt.make_pdu(
+ action = "create" if parent_pdu is None else "set",
+ tag = ca.handle,
+ self_handle = ca.handle,
+ parent_handle = ca.handle,
+ bsc_handle = bsc_handle,
+ repository_handle = ca.handle,
+ peer_contact_uri = ca.rootd.service_uri,
+ sia_base = ca.rootd.repository.sia_base,
+ sender_name = ca.handle,
+ recipient_name = ca.handle,
+ bpki_cms_cert = ca.rootd.certificate))
+
+ except rpki.irdb.Rootd.DoesNotExist:
+ pass
+
+ rpkid_query.extend(rpki.left_right.parent_elt.make_pdu(
+ action = "destroy", self_handle = ca.handle, parent_handle = p) for p in parent_pdus)
+
+ # Children are simpler than parents, because they call us, so no URL
+ # to construct and figuring out what certificate to use is their
+ # problem, not ours.
+
+ for child in ca.children.all():
+
+ child_pdu = child_pdus.pop(child.handle, None)
+
+ if (child_pdu is None or
+ child_pdu.bsc_handle != bsc_handle or
+ child_pdu.bpki_cert != child.certificate):
+ rpkid_query.append(rpki.left_right.child_elt.make_pdu(
+ action = "create" if child_pdu is None else "set",
+ tag = child.handle,
+ self_handle = ca.handle,
+ child_handle = child.handle,
+ bsc_handle = bsc_handle,
+ bpki_cert = child.certificate))
+
+ rpkid_query.extend(rpki.left_right.child_elt.make_pdu(
+ action = "destroy", self_handle = ca.handle, child_handle = c) for c in child_pdus)
+
+ # Publication setup.
+
+ # Um, why are we doing this per resource holder?
+
+ if self.run_pubd:
+
+ for client in self.server_ca.clients.all():
+
+ client_pdu = client_pdus.pop(client.handle, None)
+
+ if (client_pdu is None or
+ client_pdu.base_uri != client.sia_base or
+ client_pdu.bpki_cert != client.certificate):
+ pubd_query.append(rpki.publication.client_elt.make_pdu(
+ action = "create" if client_pdu is None else "set",
+ client_handle = client.handle,
+ bpki_cert = client.certificate,
+ base_uri = client.sia_base))
+
+ pubd_query.extend(rpki.publication.client_elt.make_pdu(
+ action = "destroy", client_handle = p) for p in client_pdus)
+
+ # Poke rpkid to run immediately for any requested handles.
+
+ rpkid_query.extend(rpki.left_right.self_elt.make_pdu(
+ action = "set", self_handle = h, run_now = "yes") for h in handles_to_poke)
+
+ # If we changed anything, ship updates off to daemons
+
+ if rpkid_query:
+ rpkid_reply = self.call_rpkid(*rpkid_query)
+ bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt))
+ if bsc_handle in bsc_pdus and bsc_pdus[bsc_handle].pkcs10_request:
+ bsc_req = bsc_pdus[bsc_handle].pkcs10_request
+ self.check_error_report(rpkid_reply)
+
+ if pubd_query:
+ assert self.run_pubd
+ pubd_reply = self.call_pubd(*pubd_query)
+ self.check_error_report(pubd_reply)
+
+ # Finally, clean up any <self/> objects rpkid might be holding
+ # that don't match ResourceCA object.
+
+ rpkid_reply = self.call_rpkid(rpki.left_right.self_elt.make_pdu(action = "list"))
+ self.check_error_report(rpkid_reply)
+
+ self_handles = set(s.self_handle for s in rpkid_reply)
+ ca_handles = set(ca.handle for ca in rpki.irdb.ResourceHolderCA.objects.all())
+ assert ca_handles <= self_handles
+
+ rpkid_query = [rpki.left_right.self_elt.make_pdu(action = "destroy", self_handle = handle)
+ for handle in (self_handles - ca_handles)]
+ rpkid_reply = self.call_rpkid(*rpkid_query)
+ self.check_error_report(rpkid_reply)
diff --git a/rpkid/rpki/irdbd.py b/rpkid/rpki/irdbd.py
index c2e01287..28e26b07 100644
--- a/rpkid/rpki/irdbd.py
+++ b/rpkid/rpki/irdbd.py
@@ -5,7 +5,7 @@ Usage: python irdbd.py [ { -c | --config } configfile ] [ { -h | --help } ]
$Id$
-Copyright (C) 2009--2011 Internet Systems Consortium ("ISC")
+Copyright (C) 2009--2012 Internet Systems Consortium ("ISC")
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
@@ -38,160 +38,90 @@ import sys, os, time, getopt, urlparse, warnings
import rpki.http, rpki.config, rpki.resource_set, rpki.relaxng
import rpki.exceptions, rpki.left_right, rpki.log, rpki.x509
-from rpki.mysql_import import MySQLdb
-
class main(object):
-
def handle_list_resources(self, q_pdu, r_msg):
-
+ child = rpki.irdb.Child.objects.get(issuer__handle__exact = q_pdu.self_handle,
+ handle = q_pdu.child_handle)
+ resources = child.resource_bag
r_pdu = rpki.left_right.list_resources_elt()
r_pdu.tag = q_pdu.tag
r_pdu.self_handle = q_pdu.self_handle
r_pdu.child_handle = q_pdu.child_handle
-
- self.cur.execute(
- "SELECT registrant_id, valid_until FROM registrant WHERE registry_handle = %s AND registrant_handle = %s",
- (q_pdu.self_handle, q_pdu.child_handle))
-
- if self.cur.rowcount != 1:
- raise rpki.exceptions.NotInDatabase, \
- "This query should have produced a single exact match, something's messed up (rowcount = %d, self_handle = %s, child_handle = %s)" \
- % (self.cur.rowcount, q_pdu.self_handle, q_pdu.child_handle)
-
- registrant_id, valid_until = self.cur.fetchone()
-
- r_pdu.valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ")
-
- r_pdu.asn = rpki.resource_set.resource_set_as.from_sql(
- self.cur,
- "SELECT start_as, end_as FROM registrant_asn WHERE registrant_id = %s",
- (registrant_id,))
-
- r_pdu.ipv4 = rpki.resource_set.resource_set_ipv4.from_sql(
- self.cur,
- "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 4",
- (registrant_id,))
-
- r_pdu.ipv6 = rpki.resource_set.resource_set_ipv6.from_sql(
- self.cur,
- "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 6",
- (registrant_id,))
-
+ r_pdu.valid_until = child.valid_until.strftime("%Y-%m-%dT%H:%M:%SZ")
+ r_pdu.asn = resources.asn
+ r_pdu.ipv4 = resources.v4
+ r_pdu.ipv6 = resources.v6
r_msg.append(r_pdu)
-
def handle_list_roa_requests(self, q_pdu, r_msg):
-
- self.cur.execute(
- "SELECT roa_request_id, asn FROM roa_request WHERE roa_request_handle = %s",
- (q_pdu.self_handle,))
-
- for roa_request_id, asn in self.cur.fetchall():
-
+ for request in rpki.irdb.ROARequest.objects.filter(issuer__handle__exact = q_pdu.self_handle):
+ prefix_bag = request.roa_prefix_bag
r_pdu = rpki.left_right.list_roa_requests_elt()
r_pdu.tag = q_pdu.tag
r_pdu.self_handle = q_pdu.self_handle
- r_pdu.asn = asn
-
- r_pdu.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql(
- self.cur,
- "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 4",
- (roa_request_id,))
-
- r_pdu.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql(
- self.cur,
- "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 6",
- (roa_request_id,))
-
+ r_pdu.asn = request.asn
+ r_pdu.ipv4 = prefix_bag.v4
+ r_pdu.ipv6 = prefix_bag.v6
r_msg.append(r_pdu)
-
def handle_list_ghostbuster_requests(self, q_pdu, r_msg):
-
- self.cur.execute(
- "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle = %s",
- (q_pdu.self_handle, q_pdu.parent_handle))
-
- vcards = [result[0] for result in self.cur.fetchall()]
-
- if not vcards:
-
- self.cur.execute(
- "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle IS NULL",
- (q_pdu.self_handle,))
-
- vcards = [result[0] for result in self.cur.fetchall()]
-
- for vcard in vcards:
+ ghostbusters = rpki.irdb.GhostbusterRequest.objects.filter(
+ issuer__handle__exact = q_pdu.self_handle,
+ parent__handle__exact = q_pdu.parent_handle)
+ if ghostbusters.count() == 0:
+ ghostbusters = rpki.irdb.GhostbusterRequest.objects.filter(
+ issuer__handle__exact = q_pdu.self_handle,
+ parent = None)
+ for ghostbuster in ghostbusters:
r_pdu = rpki.left_right.list_ghostbuster_requests_elt()
r_pdu.tag = q_pdu.tag
r_pdu.self_handle = q_pdu.self_handle
r_pdu.parent_handle = q_pdu.parent_handle
- r_pdu.vcard = vcard
+ r_pdu.vcard = ghostbuster.vcard
r_msg.append(r_pdu)
-
- handle_dispatch = {
- rpki.left_right.list_resources_elt : handle_list_resources,
- rpki.left_right.list_roa_requests_elt : handle_list_roa_requests,
- rpki.left_right.list_ghostbuster_requests_elt : handle_list_ghostbuster_requests}
-
-
def handler(self, query, path, cb):
try:
-
- self.db.ping(True)
-
+ q_pdu = None
r_msg = rpki.left_right.msg.reply()
-
+ self.start_new_transaction()
+ serverCA = rpki.irdb.ServerCA.objects.get()
+ rpkid = serverCA.ee_certificates.get(purpose = "rpkid")
try:
-
- q_msg = rpki.left_right.cms_msg(DER = query).unwrap((self.bpki_ta, self.rpkid_cert))
-
+ q_msg = rpki.left_right.cms_msg(DER = query).unwrap((serverCA.certificate, rpkid.certificate))
if not isinstance(q_msg, rpki.left_right.msg) or not q_msg.is_query():
- raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_msg
-
+ raise rpki.exceptions.BadQuery("Unexpected %r PDU" % q_msg)
for q_pdu in q_msg:
-
- try:
-
- try:
- h = self.handle_dispatch[type(q_pdu)]
- except KeyError:
- raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_pdu
- else:
- h(self, q_pdu, r_msg)
-
- except (rpki.async.ExitNow, SystemExit):
- raise
-
- except Exception, e:
- rpki.log.traceback()
- r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag))
-
+ self.dispatch(q_pdu, r_msg)
except (rpki.async.ExitNow, SystemExit):
raise
-
except Exception, e:
rpki.log.traceback()
- r_msg.append(rpki.left_right.report_error_elt.from_exception(e))
-
- cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, self.irdbd_key, self.irdbd_cert))
-
+ if q_pdu is None:
+ r_msg.append(rpki.left_right.report_error_elt.from_exception(e))
+ else:
+ r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag))
+ irdbd = serverCA.ee_certificates.get(purpose = "irdbd")
+ cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, irdbd.private_key, irdbd.certificate))
except (rpki.async.ExitNow, SystemExit):
raise
-
except Exception, e:
rpki.log.traceback()
-
- # We only get here in cases where we couldn't or wouldn't generate
- # <report_error/>, so just return HTTP failure.
-
cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e))
+ def dispatch(self, q_pdu, r_msg):
+ try:
+ handler = self.dispatch_vector[type(q_pdu)]
+ except KeyError:
+ raise rpki.exceptions.BadQuery("Unexpected %r PDU" % q_pdu)
+ else:
+ handler(q_pdu, r_msg)
+
+ def __init__(self, **kwargs):
- def __init__(self):
+ global rpki
+ from django.conf import settings
os.environ["TZ"] = "UTC"
time.tzset()
@@ -208,31 +138,69 @@ class main(object):
elif o in ("-d", "--debug"):
rpki.log.use_syslog = False
if argv:
- raise rpki.exceptions.CommandParseFailure, "Unexpected arguments %s" % argv
+ raise rpki.exceptions.CommandParseFailure("Unexpected arguments %s" % argv)
rpki.log.init("irdbd")
- self.cfg = rpki.config.parser(cfg_file, "irdbd")
+ cfg = rpki.config.parser(cfg_file, "irdbd")
- startup_msg = self.cfg.get("startup-message", "")
+ startup_msg = cfg.get("startup-message", "")
if startup_msg:
rpki.log.info(startup_msg)
- self.cfg.set_global_flags()
-
- self.db = MySQLdb.connect(user = self.cfg.get("sql-username"),
- db = self.cfg.get("sql-database"),
- passwd = self.cfg.get("sql-password"))
-
- self.cur = self.db.cursor()
- self.db.autocommit(True)
-
- self.bpki_ta = rpki.x509.X509(Auto_update = self.cfg.get("bpki-ta"))
- self.rpkid_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpkid-cert"))
- self.irdbd_cert = rpki.x509.X509(Auto_update = self.cfg.get("irdbd-cert"))
- self.irdbd_key = rpki.x509.RSA( Auto_update = self.cfg.get("irdbd-key"))
-
- u = urlparse.urlparse(self.cfg.get("http-url"))
+ cfg.set_global_flags()
+
+ # Do -not- turn on DEBUG here except for short-lived tests,
+ # otherwise irdbd will eventually run out of memory and crash.
+ #
+ # If you must enable debugging, use django.db.reset_queries() to
+ # clear the query list manually, but it's probably better just to
+ # run with debugging disabled, since that's the expectation for
+ # production code.
+ #
+ # https://docs.djangoproject.com/en/dev/faq/models/#why-is-django-leaking-memory
+
+ settings.configure(
+ DATABASES = {
+ "default" : {
+ "ENGINE" : "django.db.backends.mysql",
+ "NAME" : cfg.get("sql-database"),
+ "USER" : cfg.get("sql-username"),
+ "PASSWORD" : cfg.get("sql-password"),
+ "HOST" : "",
+ "PORT" : "" }},
+ INSTALLED_APPS = ("rpki.irdb",),)
+
+ import rpki.irdb
+
+ # Entirely too much fun with read-only access to transactional databases.
+ #
+ # http://stackoverflow.com/questions/3346124/how-do-i-force-django-to-ignore-any-caches-and-reload-data
+ # http://devblog.resolversystems.com/?p=439
+ # http://groups.google.com/group/django-users/browse_thread/thread/e25cec400598c06d
+ # http://stackoverflow.com/questions/1028671/python-mysqldb-update-query-fails
+ # http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
+ #
+ # It turns out that MySQL is doing us a favor with this weird
+ # transactional behavior on read, because without it there's a
+ # race condition if multiple updates are committed to the IRDB
+ # while we're in the middle of processing a query. Note that
+ # proper transaction management by the committers doesn't protect
+ # us, this is a transactional problem on read. So we need to use
+ # explicit transaction management. Since irdbd is a read-only
+ # consumer of IRDB data, this means we need to commit an empty
+ # transaction at the beginning of processing each query, to reset
+ # the transaction isolation snapshot.
+
+ import django.db.transaction
+ self.start_new_transaction = django.db.transaction.commit_manually(django.db.transaction.commit)
+
+ self.dispatch_vector = {
+ rpki.left_right.list_resources_elt : self.handle_list_resources,
+ rpki.left_right.list_roa_requests_elt : self.handle_list_roa_requests,
+ rpki.left_right.list_ghostbuster_requests_elt : self.handle_list_ghostbuster_requests }
+
+ u = urlparse.urlparse(cfg.get("http-url"))
assert u.scheme in ("", "http") and \
u.username is None and \
@@ -241,6 +209,7 @@ class main(object):
u.query == "" and \
u.fragment == ""
- rpki.http.server(host = u.hostname or "localhost",
- port = u.port or 443,
- handlers = ((u.path, self.handler),))
+ rpki.http.server(
+ host = u.hostname or "localhost",
+ port = u.port or 443,
+ handlers = ((u.path, self.handler),))
diff --git a/rpkid/rpki/left_right.py b/rpkid/rpki/left_right.py
index 7cb18f8c..17d665c9 100644
--- a/rpkid/rpki/left_right.py
+++ b/rpkid/rpki/left_right.py
@@ -405,7 +405,7 @@ class self_elt(data_elt):
def list_failed(e):
rpki.log.traceback()
- rpki.log.warn("Couldn't get resource class list from parent %r, skipping: %s" % (parent, e))
+ rpki.log.warn("Couldn't get resource class list from parent %r, skipping: %s (%r)" % (parent, e, e))
parent_iterator()
rpki.up_down.list_pdu.query(parent, got_list, list_failed)
diff --git a/rpkid/rpki/myrpki.py b/rpkid/rpki/myrpki.py
index 2fa2f8cb..ec36371c 100644
--- a/rpkid/rpki/myrpki.py
+++ b/rpkid/rpki/myrpki.py
@@ -793,9 +793,17 @@ class CA(object):
Write PEM certificate to file, then cross-certify.
"""
fn = os.path.join(self.dir, filename or "temp.%s.cer" % os.getpid())
+ der = base64.b64decode(b64)
+ if True:
+ try:
+ text = self.run_openssl("x509", "-inform", "DER", "-noout",
+ "-issuer", "-subject", stdin = der)
+ except:
+ text = ""
+ print "fxcert():", self.dir, filename, text
try:
self.run_openssl("x509", "-inform", "DER", "-out", fn,
- stdin = base64.b64decode(b64))
+ stdin = der)
return self.xcert(fn, path_restriction)
finally:
if not filename and os.path.exists(fn):
diff --git a/rpkid/rpki/oids.py b/rpkid/rpki/oids.py
index 2557d7cf..3fbe214c 100644
--- a/rpkid/rpki/oids.py
+++ b/rpkid/rpki/oids.py
@@ -67,6 +67,7 @@ oid2name = {
(2, 5, 29, 35) : "authorityKeyIdentifier",
(2, 5, 29, 37) : "extendedKeyUsage",
(2, 5, 4, 3) : "commonName",
+ (2, 5, 4, 5) : "serialNumber",
}
## @var name2oid
diff --git a/rpkid/rpki/old_irdbd.py b/rpkid/rpki/old_irdbd.py
new file mode 100644
index 00000000..c63ce9e2
--- /dev/null
+++ b/rpkid/rpki/old_irdbd.py
@@ -0,0 +1,249 @@
+"""
+IR database daemon.
+
+Usage: python irdbd.py [ { -c | --config } configfile ] [ { -h | --help } ]
+
+This is the old (pre-Django) version of irdbd, still used by smoketest
+and perhaps still useful as a minimal example.
+
+$Id$
+
+Copyright (C) 2009--2012 Internet Systems Consortium ("ISC")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+"""
+
+import sys, os, time, getopt, urlparse, warnings
+import rpki.http, rpki.config, rpki.resource_set, rpki.relaxng
+import rpki.exceptions, rpki.left_right, rpki.log, rpki.x509
+
+from rpki.mysql_import import MySQLdb
+
+class main(object):
+
+
+ def handle_list_resources(self, q_pdu, r_msg):
+
+ r_pdu = rpki.left_right.list_resources_elt()
+ r_pdu.tag = q_pdu.tag
+ r_pdu.self_handle = q_pdu.self_handle
+ r_pdu.child_handle = q_pdu.child_handle
+
+ self.cur.execute(
+ "SELECT registrant_id, valid_until FROM registrant WHERE registry_handle = %s AND registrant_handle = %s",
+ (q_pdu.self_handle, q_pdu.child_handle))
+
+ if self.cur.rowcount != 1:
+ raise rpki.exceptions.NotInDatabase, \
+ "This query should have produced a single exact match, something's messed up (rowcount = %d, self_handle = %s, child_handle = %s)" \
+ % (self.cur.rowcount, q_pdu.self_handle, q_pdu.child_handle)
+
+ registrant_id, valid_until = self.cur.fetchone()
+
+ r_pdu.valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+ r_pdu.asn = rpki.resource_set.resource_set_as.from_sql(
+ self.cur,
+ "SELECT start_as, end_as FROM registrant_asn WHERE registrant_id = %s",
+ (registrant_id,))
+
+ r_pdu.ipv4 = rpki.resource_set.resource_set_ipv4.from_sql(
+ self.cur,
+ "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 4",
+ (registrant_id,))
+
+ r_pdu.ipv6 = rpki.resource_set.resource_set_ipv6.from_sql(
+ self.cur,
+ "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 6",
+ (registrant_id,))
+
+ r_msg.append(r_pdu)
+
+
+ def handle_list_roa_requests(self, q_pdu, r_msg):
+
+ self.cur.execute(
+ "SELECT roa_request_id, asn FROM roa_request WHERE roa_request_handle = %s",
+ (q_pdu.self_handle,))
+
+ for roa_request_id, asn in self.cur.fetchall():
+
+ r_pdu = rpki.left_right.list_roa_requests_elt()
+ r_pdu.tag = q_pdu.tag
+ r_pdu.self_handle = q_pdu.self_handle
+ r_pdu.asn = asn
+
+ r_pdu.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql(
+ self.cur,
+ "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 4",
+ (roa_request_id,))
+
+ r_pdu.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql(
+ self.cur,
+ "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 6",
+ (roa_request_id,))
+
+ r_msg.append(r_pdu)
+
+
+ def handle_list_ghostbuster_requests(self, q_pdu, r_msg):
+
+ self.cur.execute(
+ "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle = %s",
+ (q_pdu.self_handle, q_pdu.parent_handle))
+
+ vcards = [result[0] for result in self.cur.fetchall()]
+
+ if not vcards:
+
+ self.cur.execute(
+ "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle IS NULL",
+ (q_pdu.self_handle,))
+
+ vcards = [result[0] for result in self.cur.fetchall()]
+
+ for vcard in vcards:
+ r_pdu = rpki.left_right.list_ghostbuster_requests_elt()
+ r_pdu.tag = q_pdu.tag
+ r_pdu.self_handle = q_pdu.self_handle
+ r_pdu.parent_handle = q_pdu.parent_handle
+ r_pdu.vcard = vcard
+ r_msg.append(r_pdu)
+
+
+ handle_dispatch = {
+ rpki.left_right.list_resources_elt : handle_list_resources,
+ rpki.left_right.list_roa_requests_elt : handle_list_roa_requests,
+ rpki.left_right.list_ghostbuster_requests_elt : handle_list_ghostbuster_requests}
+
+
+ def handler(self, query, path, cb):
+ try:
+
+ self.db.ping(True)
+
+ r_msg = rpki.left_right.msg.reply()
+
+ try:
+
+ q_msg = rpki.left_right.cms_msg(DER = query).unwrap((self.bpki_ta, self.rpkid_cert))
+
+ if not isinstance(q_msg, rpki.left_right.msg) or not q_msg.is_query():
+ raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_msg
+
+ for q_pdu in q_msg:
+
+ try:
+
+ try:
+ h = self.handle_dispatch[type(q_pdu)]
+ except KeyError:
+ raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_pdu
+ else:
+ h(self, q_pdu, r_msg)
+
+ except (rpki.async.ExitNow, SystemExit):
+ raise
+
+ except Exception, e:
+ rpki.log.traceback()
+ r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag))
+
+ except (rpki.async.ExitNow, SystemExit):
+ raise
+
+ except Exception, e:
+ rpki.log.traceback()
+ r_msg.append(rpki.left_right.report_error_elt.from_exception(e))
+
+ cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, self.irdbd_key, self.irdbd_cert))
+
+ except (rpki.async.ExitNow, SystemExit):
+ raise
+
+ except Exception, e:
+ rpki.log.traceback()
+
+ # We only get here in cases where we couldn't or wouldn't generate
+ # <report_error/>, so just return HTTP failure.
+
+ cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e))
+
+
+ def __init__(self):
+
+ os.environ["TZ"] = "UTC"
+ time.tzset()
+
+ cfg_file = None
+
+ opts, argv = getopt.getopt(sys.argv[1:], "c:dh?", ["config=", "debug", "help"])
+ for o, a in opts:
+ if o in ("-h", "--help", "-?"):
+ print __doc__
+ sys.exit(0)
+ if o in ("-c", "--config"):
+ cfg_file = a
+ elif o in ("-d", "--debug"):
+ rpki.log.use_syslog = False
+ if argv:
+ raise rpki.exceptions.CommandParseFailure, "Unexpected arguments %s" % argv
+
+ rpki.log.init("irdbd")
+
+ self.cfg = rpki.config.parser(cfg_file, "irdbd")
+
+ startup_msg = self.cfg.get("startup-message", "")
+ if startup_msg:
+ rpki.log.info(startup_msg)
+
+ self.cfg.set_global_flags()
+
+ self.db = MySQLdb.connect(user = self.cfg.get("sql-username"),
+ db = self.cfg.get("sql-database"),
+ passwd = self.cfg.get("sql-password"))
+
+ self.cur = self.db.cursor()
+ self.db.autocommit(True)
+
+ self.bpki_ta = rpki.x509.X509(Auto_update = self.cfg.get("bpki-ta"))
+ self.rpkid_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpkid-cert"))
+ self.irdbd_cert = rpki.x509.X509(Auto_update = self.cfg.get("irdbd-cert"))
+ self.irdbd_key = rpki.x509.RSA( Auto_update = self.cfg.get("irdbd-key"))
+
+ u = urlparse.urlparse(self.cfg.get("http-url"))
+
+ assert u.scheme in ("", "http") and \
+ u.username is None and \
+ u.password is None and \
+ u.params == "" and \
+ u.query == "" and \
+ u.fragment == ""
+
+ rpki.http.server(host = u.hostname or "localhost",
+ port = u.port or 443,
+ handlers = ((u.path, self.handler),))
diff --git a/rpkid/rpki/pubd.py b/rpkid/rpki/pubd.py
index bde1260e..6968780d 100644
--- a/rpkid/rpki/pubd.py
+++ b/rpkid/rpki/pubd.py
@@ -134,7 +134,7 @@ class main(object):
raise
except Exception, e:
rpki.log.traceback()
- cb(500, reason = "Unhandled exception %s" % e)
+ cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e))
client_url_regexp = re.compile("/client/([-A-Z0-9_/]+)$", re.I)
diff --git a/rpkid/rpki/rcynic.py b/rpkid/rpki/rcynic.py
index c2562cbd..b7e493ec 100644
--- a/rpkid/rpki/rcynic.py
+++ b/rpkid/rpki/rcynic.py
@@ -226,6 +226,7 @@ class rcynic_xml_iterator(object):
unauthenticated_subdir = "unauthenticated"):
self.rcynic_root = rcynic_root
self.xml_file = xml_file
+ self.authenticated_subdir = os.path.join(rcynic_root, 'authenticated')
self.authenticated_old_subdir = os.path.join(rcynic_root, authenticated_old_subdir)
self.unauthenticated_subdir = os.path.join(rcynic_root, unauthenticated_subdir)
@@ -245,8 +246,14 @@ class rcynic_xml_iterator(object):
generation = validation_status.get("generation")
# determine the path to this object
- filename = os.path.join(self.authenticated_old_subdir if generation == 'backup' else self.unauthenticated_subdir,
- self.uri_to_filename(uri))
+ if status == 'object_accepted':
+ d = self.authenticated_subdir
+ elif generation == 'backup':
+ d = self.authenticated_old_subdir
+ else:
+ d = self.unauthenticated_subdir
+
+ filename = os.path.join(d, self.uri_to_filename(uri))
ext = os.path.splitext(filename)[1]
if ext in file_name_classes:
diff --git a/rpkid/rpki/relaxng.py b/rpkid/rpki/relaxng.py
index 31881329..eed3ca2c 100644
--- a/rpkid/rpki/relaxng.py
+++ b/rpkid/rpki/relaxng.py
@@ -6,7 +6,7 @@ import lxml.etree
## Parsed RelaxNG left_right schema
left_right = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?>
<!--
- $Id: left-right-schema.rnc 4346 2012-02-17 01:11:06Z sra $
+ $Id: left-right-schema.rnc 4403 2012-03-19 21:14:48Z sra $
RelaxNG Schema for RPKI left-right protocol.
@@ -1839,3 +1839,382 @@ publication = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" en
-->
'''))
+## @var myrpki
+## Parsed RelaxNG myrpki schema
+myrpki = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ $Id: myrpki.rnc 3723 2011-03-14 20:43:16Z sra $
+
+ RelaxNG Schema for MyRPKI XML messages.
+
+ This message protocol is on its way out, as we're in the process of
+ moving on from the user interface model that produced it, but even
+ after we finish replacing it we'll still need the schema for a while
+ to validate old messages when upgrading.
+
+ libxml2 (including xmllint) only groks the XML syntax of RelaxNG, so
+ run the compact syntax through trang to get XML syntax.
+
+ Copyright (C) 2009-2011 Internet Systems Consortium ("ISC")
+
+ Permission to use, copy, modify, and distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+ OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ PERFORMANCE OF THIS SOFTWARE.
+-->
+<grammar ns="http://www.hactrn.net/uris/rpki/myrpki/" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+ <define name="version">
+ <value>2</value>
+ </define>
+ <define name="base64">
+ <data type="base64Binary">
+ <param name="maxLength">512000</param>
+ </data>
+ </define>
+ <define name="object_handle">
+ <data type="string">
+ <param name="maxLength">255</param>
+ <param name="pattern">[\-_A-Za-z0-9]*</param>
+ </data>
+ </define>
+ <define name="pubd_handle">
+ <data type="string">
+ <param name="maxLength">255</param>
+ <param name="pattern">[\-_A-Za-z0-9/]*</param>
+ </data>
+ </define>
+ <define name="uri">
+ <data type="anyURI">
+ <param name="maxLength">4096</param>
+ </data>
+ </define>
+ <define name="asn">
+ <data type="positiveInteger"/>
+ </define>
+ <define name="asn_list">
+ <data type="string">
+ <param name="maxLength">512000</param>
+ <param name="pattern">[\-,0-9]*</param>
+ </data>
+ </define>
+ <define name="ipv4_list">
+ <data type="string">
+ <param name="maxLength">512000</param>
+ <param name="pattern">[\-,0-9/.]*</param>
+ </data>
+ </define>
+ <define name="ipv6_list">
+ <data type="string">
+ <param name="maxLength">512000</param>
+ <param name="pattern">[\-,0-9/:a-fA-F]*</param>
+ </data>
+ </define>
+ <define name="timestamp">
+ <data type="dateTime">
+ <param name="pattern">.*Z</param>
+ </data>
+ </define>
+ <!--
+ Message formate used between configure_resources and
+ configure_daemons.
+ -->
+ <start combine="choice">
+ <element name="myrpki">
+ <attribute name="version">
+ <ref name="version"/>
+ </attribute>
+ <attribute name="handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <optional>
+ <attribute name="service_uri">
+ <ref name="uri"/>
+ </attribute>
+ </optional>
+ <zeroOrMore>
+ <element name="roa_request">
+ <attribute name="asn">
+ <ref name="asn"/>
+ </attribute>
+ <attribute name="v4">
+ <ref name="ipv4_list"/>
+ </attribute>
+ <attribute name="v6">
+ <ref name="ipv6_list"/>
+ </attribute>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="child">
+ <attribute name="handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <attribute name="valid_until">
+ <ref name="timestamp"/>
+ </attribute>
+ <optional>
+ <attribute name="asns">
+ <ref name="asn_list"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="v4">
+ <ref name="ipv4_list"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="v6">
+ <ref name="ipv6_list"/>
+ </attribute>
+ </optional>
+ <optional>
+ <element name="bpki_certificate">
+ <ref name="base64"/>
+ </element>
+ </optional>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="parent">
+ <attribute name="handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <optional>
+ <attribute name="service_uri">
+ <ref name="uri"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="myhandle">
+ <ref name="object_handle"/>
+ </attribute>
+ </optional>
+ <optional>
+ <attribute name="sia_base">
+ <ref name="uri"/>
+ </attribute>
+ </optional>
+ <optional>
+ <element name="bpki_cms_certificate">
+ <ref name="base64"/>
+ </element>
+ </optional>
+ </element>
+ </zeroOrMore>
+ <zeroOrMore>
+ <element name="repository">
+ <attribute name="handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <optional>
+ <attribute name="service_uri">
+ <ref name="uri"/>
+ </attribute>
+ </optional>
+ <optional>
+ <element name="bpki_certificate">
+ <ref name="base64"/>
+ </element>
+ </optional>
+ </element>
+ </zeroOrMore>
+ <optional>
+ <element name="bpki_ca_certificate">
+ <ref name="base64"/>
+ </element>
+ </optional>
+ <optional>
+ <element name="bpki_crl">
+ <ref name="base64"/>
+ </element>
+ </optional>
+ <optional>
+ <element name="bpki_bsc_certificate">
+ <ref name="base64"/>
+ </element>
+ </optional>
+ <optional>
+ <element name="bpki_bsc_pkcs10">
+ <ref name="base64"/>
+ </element>
+ </optional>
+ </element>
+ </start>
+ <!-- Format of an identity.xml file. -->
+ <start combine="choice">
+ <element name="identity">
+ <attribute name="version">
+ <ref name="version"/>
+ </attribute>
+ <attribute name="handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <element name="bpki_ta">
+ <ref name="base64"/>
+ </element>
+ </element>
+ </start>
+ <!--
+ Format of <authorization/> element used in referrals. The Base64
+ text is a <referral/> (q. v.) element signed with CMS.
+ -->
+ <define name="authorization">
+ <element name="authorization">
+ <attribute name="referrer">
+ <ref name="pubd_handle"/>
+ </attribute>
+ <ref name="base64"/>
+ </element>
+ </define>
+ <!-- Format of <contact_info/> element used in referrals. -->
+ <define name="contact_info">
+ <element name="contact_info">
+ <optional>
+ <attribute name="uri">
+ <ref name="uri"/>
+ </attribute>
+ </optional>
+ <data type="string"/>
+ </element>
+ </define>
+ <!-- Variant payload portion of a <repository/> element. -->
+ <define name="repository_payload">
+ <choice>
+ <attribute name="type">
+ <value>none</value>
+ </attribute>
+ <attribute name="type">
+ <value>offer</value>
+ </attribute>
+ <group>
+ <attribute name="type">
+ <value>referral</value>
+ </attribute>
+ <ref name="authorization"/>
+ <ref name="contact_info"/>
+ </group>
+ </choice>
+ </define>
+ <!-- <parent/> element (response from configure_child). -->
+ <start combine="choice">
+ <element name="parent">
+ <attribute name="version">
+ <ref name="version"/>
+ </attribute>
+ <attribute name="valid_until">
+ <ref name="timestamp"/>
+ </attribute>
+ <optional>
+ <attribute name="service_uri">
+ <ref name="uri"/>
+ </attribute>
+ </optional>
+ <attribute name="child_handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <attribute name="parent_handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <element name="bpki_resource_ta">
+ <ref name="base64"/>
+ </element>
+ <element name="bpki_child_ta">
+ <ref name="base64"/>
+ </element>
+ <optional>
+ <element name="repository">
+ <ref name="repository_payload"/>
+ </element>
+ </optional>
+ </element>
+ </start>
+ <!--
+ <repository/> element, types offer and referral
+ (input to configure_publication_client).
+ -->
+ <start combine="choice">
+ <element name="repository">
+ <attribute name="version">
+ <ref name="version"/>
+ </attribute>
+ <attribute name="handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <attribute name="parent_handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <ref name="repository_payload"/>
+ <element name="bpki_client_ta">
+ <ref name="base64"/>
+ </element>
+ </element>
+ </start>
+ <!--
+ <repository/> element, confirmation type (output of
+ configure_publication_client).
+ -->
+ <start combine="choice">
+ <element name="repository">
+ <attribute name="version">
+ <ref name="version"/>
+ </attribute>
+ <attribute name="type">
+ <value>confirmed</value>
+ </attribute>
+ <attribute name="parent_handle">
+ <ref name="object_handle"/>
+ </attribute>
+ <attribute name="client_handle">
+ <ref name="pubd_handle"/>
+ </attribute>
+ <attribute name="service_uri">
+ <ref name="uri"/>
+ </attribute>
+ <attribute name="sia_base">
+ <ref name="uri"/>
+ </attribute>
+ <element name="bpki_server_ta">
+ <ref name="base64"/>
+ </element>
+ <element name="bpki_client_ta">
+ <ref name="base64"/>
+ </element>
+ <optional>
+ <ref name="authorization"/>
+ </optional>
+ <optional>
+ <ref name="contact_info"/>
+ </optional>
+ </element>
+ </start>
+ <!--
+ <referral/> element. This is the entirety of a separate message
+ which is signed with CMS then included ase the Base64 content of an
+ <authorization/> element in the main message.
+ -->
+ <start combine="choice">
+ <element name="referral">
+ <attribute name="version">
+ <ref name="version"/>
+ </attribute>
+ <attribute name="authorized_sia_base">
+ <ref name="uri"/>
+ </attribute>
+ <ref name="base64"/>
+ </element>
+ </start>
+</grammar>
+<!--
+ Local Variables:
+ indent-tabs-mode: nil
+ End:
+-->
+'''))
+
diff --git a/rpkid/rpki/resource_set.py b/rpkid/rpki/resource_set.py
index 2fd10756..be39df75 100644
--- a/rpkid/rpki/resource_set.py
+++ b/rpkid/rpki/resource_set.py
@@ -500,6 +500,18 @@ class resource_set(list):
for (b, e) in sql.fetchall()])
@classmethod
+ def from_django(cls, iterable):
+ """
+ Create resource set from a Django query.
+
+ iterable is something which returns (min, max) pairs.
+ """
+
+ return cls(ini = [cls.range_type(cls.range_type.datum_type(b),
+ cls.range_type.datum_type(e))
+ for (b, e) in iterable])
+
+ @classmethod
def parse_str(cls, s):
"""
Parse resource set from text string (eg, XML attributes). This is
@@ -983,6 +995,19 @@ class roa_prefix_set(list):
return cls([cls.prefix_type(cls.prefix_type.range_type.datum_type(x), int(y), int(z))
for (x, y, z) in sql.fetchall()])
+ @classmethod
+ def from_django(cls, iterable):
+ """
+ Create ROA prefix set from a Django query.
+
+ iterable is something which returns (prefix, prefixlen,
+ max_prefixlen) triples.
+ """
+
+ return cls([cls.prefix_type(cls.prefix_type.range_type.datum_type(x), int(y), int(z))
+ for (x, y, z) in iterable])
+
+
def to_roa_tuple(self):
"""
Convert ROA prefix set into tuple format used by ROA ASN.1
@@ -1029,6 +1054,29 @@ class roa_prefix_set_ipv6(roa_prefix_set):
# Fix back link from resource_set to roa_prefix
resource_set_ipv6.roa_prefix_set_type = roa_prefix_set_ipv6
+class roa_prefix_bag(object):
+ """
+ Container to simplify passing around the combination of an IPv4 ROA
+ prefix set and an IPv6 ROA prefix set.
+ """
+
+ ## @var v4
+ # Set of IPv4 prefixes.
+
+ ## @var v6
+ # Set of IPv6 prefixes.
+
+ def __init__(self, v4 = None, v6 = None):
+ self.v4 = v4 or roa_prefix_set_ipv4()
+ self.v6 = v6 or roa_prefix_set_ipv6()
+
+ def __eq__(self, other):
+ return self.v4 == other.v4 and self.v6 == other.v6
+
+ def __ne__(self, other):
+ return not (self == other)
+
+
# Test suite for set operations.
if __name__ == "__main__":
diff --git a/rpkid/rpki/rootd.py b/rpkid/rpki/rootd.py
index 668e4027..feceffc5 100644
--- a/rpkid/rpki/rootd.py
+++ b/rpkid/rpki/rootd.py
@@ -257,7 +257,7 @@ class main(object):
return cb(400, reason = "Could not process PDU: %s" % e)
def done(r_msg):
- cb(200, body = cms_msg().wrap(r_msg, self.rootd_bpki_key, self.rootd_bpki_cert, self.rootd_bpki_crl))
+ cb(200, body = cms_msg().wrap(r_msg, self.rootd_bpki_key, self.rootd_bpki_cert))
try:
q_msg.serve_top_level(None, done)
@@ -345,7 +345,7 @@ class main(object):
self.rpki_root_dir = self.cfg.get("rpki-root-dir")
self.rpki_base_uri = self.cfg.get("rpki-base-uri", "rsync://" + self.rpki_class_name + ".invalid/")
- self.rpki_root_key = rpki.x509.RSA( Auto_file = self.cfg.get("rpki-root-key"))
+ self.rpki_root_key = rpki.x509.RSA(Auto_update = self.cfg.get("rpki-root-key"))
self.rpki_root_cert_file = self.cfg.get("rpki-root-cert")
self.rpki_root_cert_uri = self.cfg.get("rpki-root-cert-uri", self.rpki_base_uri + "Root.cer")
diff --git a/rpkid/rpki/rpkic.py b/rpkid/rpki/rpkic.py
new file mode 100644
index 00000000..dbaee3bb
--- /dev/null
+++ b/rpkid/rpki/rpkic.py
@@ -0,0 +1,486 @@
+"""
+This is a command line configuration and control tool for rpkid et al.
+
+Type "help" on the prompt, or run the program with the --help option for an
+overview of the available commands; type "help foo" for (more) detailed help
+on the "foo" command.
+
+
+This program is a rewrite of the old myrpki program, replacing ten
+zillion XML and X.509 disk files and subprocess calls to the OpenSSL
+command line tool with SQL data and direct calls to the rpki.POW
+library. This version abandons all pretense that this program might
+somehow work without rpki.POW, lxml, and Django installed, but since
+those packages are required for rpkid anyway, this seems like a small
+price to pay for major simplification of the code and better
+integration with the Django-based GUI interface.
+
+$Id$
+
+Copyright (C) 2009--2012 Internet Systems Consortium ("ISC")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+"""
+
+# NB: As of this writing, I'm trying really hard to avoid having this
+# program depend on a Django settings.py file. This may prove to be a
+# waste of time in the long run, but for for now, this means that one
+# has to be careful about exactly how and when one imports Django
+# modules, or anything that imports Django modules. Bottom line is
+# that we don't import such modules until we need them.
+
+import csv
+import re
+import os
+import getopt
+import sys
+import base64
+import time
+import glob
+import copy
+import warnings
+import rpki.config
+import rpki.cli
+import rpki.sundial
+import rpki.log
+import rpki.oids
+import rpki.http
+import rpki.resource_set
+import rpki.relaxng
+import rpki.exceptions
+import rpki.left_right
+import rpki.x509
+import rpki.async
+
+class BadCommandSyntax(Exception): "Bad command line syntax."
+class BadPrefixSyntax(Exception): "Bad prefix syntax."
+class CouldntTalkToDaemon(Exception): "Couldn't talk to daemon."
+class BadXMLMessage(Exception): "Bad XML message."
+class PastExpiration(Exception): "Expiration date has already passed."
+class CantRunRootd(Exception): "Can't run rootd."
+
+class main(rpki.cli.Cmd):
+
+ prompt = "rpkic> "
+
+ completedefault = rpki.cli.Cmd.filename_complete
+
+ def __init__(self):
+ os.environ["TZ"] = "UTC"
+ time.tzset()
+
+ rpki.log.use_syslog = False
+
+ cfg_file = None
+ handle = None
+
+ opts, argv = getopt.getopt(sys.argv[1:], "c:hi:?", ["config=", "help", "identity="])
+ for o, a in opts:
+ if o in ("-c", "--config"):
+ cfg_file = a
+ elif o in ("-h", "--help", "-?"):
+ argv = ["help"]
+ elif o in ("-i", "--identity"):
+ handle = a
+
+ if not argv or argv[0] != "help":
+ rpki.log.init("rpkic")
+ self.read_config(cfg_file, handle)
+
+ rpki.cli.Cmd.__init__(self, argv)
+
+ def read_config(self, cfg_file, handle):
+ global rpki
+
+ cfg = rpki.config.parser(cfg_file, "myrpki")
+ cfg.set_global_flags()
+
+ from django.conf import settings
+
+ settings.configure(
+ DATABASES = { "default" : {
+ "ENGINE" : "django.db.backends.mysql",
+ "NAME" : cfg.get("sql-database", section = "irdbd"),
+ "USER" : cfg.get("sql-username", section = "irdbd"),
+ "PASSWORD" : cfg.get("sql-password", section = "irdbd"),
+ "HOST" : "",
+ "PORT" : "",
+ "OPTIONS" : { "init_command": "SET storage_engine=INNODB" }}},
+ INSTALLED_APPS = ("rpki.irdb",),
+ )
+
+ import rpki.irdb
+
+ try:
+ rpki.irdb.models.ca_certificate_lifetime = rpki.sundial.timedelta.parse(
+ cfg.get("bpki_ca_certificate_lifetime", section = "rpkic"))
+ except rpki.config.ConfigParser.Error:
+ pass
+
+ try:
+ rpki.irdb.models.ee_certificate_lifetime = rpki.sundial.timedelta.parse(
+ cfg.get("bpki_ee_certificate_lifetime", section = "rpkic"))
+ except rpki.config.ConfigParser.Error:
+ pass
+
+ try:
+ rpki.irdb.models.crl_interval = rpki.sundial.timedelta.parse(
+ cfg.get("bpki_crl_interval", section = "rpkic"))
+ except rpki.config.ConfigParser.Error:
+ pass
+
+ import django.core.management
+ django.core.management.call_command("syncdb", verbosity = 0, load_initial_data = False)
+
+ self.zoo = rpki.irdb.Zookeeper(cfg = cfg, handle = handle, logstream = sys.stdout)
+
+ def help_overview(self):
+ """
+ Show program __doc__ string. Perhaps there's some clever way to
+ do this using the textwrap module, but for now something simple
+ and crude will suffice.
+ """
+
+ for line in __doc__.splitlines(True):
+ self.stdout.write(" " * 4 + line)
+ self.stdout.write("\n")
+
+ def irdb_handle_complete(self, klass, text, line, begidx, endidx):
+ return [obj.handle for obj in klass.objects.all() if obj.handle and obj.handle.startswith(text)]
+
+ def do_select_identity(self, arg):
+ """
+ Select an identity handle for use with later commands.
+ """
+
+ argv = arg.split()
+ if len(argv) != 1:
+ raise BadCommandSyntax("This command expexcts one argument, not %r" % arg)
+ self.zoo.reset_identity(argv[0])
+
+ def complete_select_identity(self, *args):
+ return self.irdb_handle_complete(rpki.irdb.ResourceHolderCA, *args)
+
+
+ def do_initialize(self, arg):
+ """
+ Initialize an RPKI installation. This command reads the
+ configuration file, creates the BPKI and EntityDB directories,
+ generates the initial BPKI certificates, and creates an XML file
+ describing the resource-holding aspect of this RPKI installation.
+ """
+
+ if arg:
+ raise BadCommandSyntax, "This command takes no arguments"
+
+ r = self.zoo.initialize()
+ r.save("%s.identity.xml" % self.zoo.handle,
+ None if self.zoo.run_pubd else sys.stdout)
+
+ if self.zoo.run_rootd and self.zoo.handle == self.zoo.cfg.get("handle"):
+ r = self.zoo.configure_rootd()
+ if r is not None:
+ r.save("%s.%s.repository-request.xml" % (self.zoo.handle, self.zoo.handle), sys.stdout)
+
+ self.zoo.write_bpki_files()
+
+
+ def do_update_bpki(self, arg):
+ """
+ Update BPKI certificates. Assumes an existing RPKI installation.
+
+ Basic plan here is to reissue all BPKI certificates we can, right
+ now. In the long run we might want to be more clever about only
+ touching ones that need maintenance, but this will do for a start.
+
+ We also reissue CRLs for all CAs.
+
+ Most likely this should be run under cron.
+ """
+
+ self.zoo.update_bpki()
+ self.zoo.write_bpki_files()
+
+
+ def do_configure_child(self, arg):
+ """
+ Configure a new child of this RPKI entity, given the child's XML
+ identity file as an input. This command extracts the child's data
+ from the XML, cross-certifies the child's resource-holding BPKI
+ certificate, and generates an XML file describing the relationship
+ between the child and this parent, including this parent's BPKI
+ data and up-down protocol service URI.
+ """
+
+ child_handle = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["child_handle="])
+ for o, a in opts:
+ if o == "--child_handle":
+ child_handle = a
+
+ if len(argv) != 1:
+ raise BadCommandSyntax, "Need to specify filename for child.xml"
+
+ r, child_handle = self.zoo.configure_child(argv[0], child_handle)
+ r.save("%s.%s.parent-response.xml" % (self.zoo.handle, child_handle), sys.stdout)
+
+
+ def do_delete_child(self, arg):
+ """
+ Delete a child of this RPKI entity.
+ """
+
+ try:
+ self.zoo.delete_child(arg)
+ except rpki.irdb.Child.DoesNotExist:
+ print "No such child \"%s\"" % arg
+
+ def complete_delete_child(self, *args):
+ return self.irdb_handle_complete(rpki.irdb.Child, *args)
+
+
+ def do_configure_parent(self, arg):
+ """
+ Configure a new parent of this RPKI entity, given the output of
+ the parent's configure_child command as input. This command reads
+ the parent's response XML, extracts the parent's BPKI and service
+ URI information, cross-certifies the parent's BPKI data into this
+ entity's BPKI, and checks for offers or referrals of publication
+ service. If a publication offer or referral is present, we
+ generate a request-for-service message to that repository, in case
+ the user wants to avail herself of the referral or offer.
+ """
+
+ parent_handle = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="])
+ for o, a in opts:
+ if o == "--parent_handle":
+ parent_handle = a
+
+ if len(argv) != 1:
+ raise BadCommandSyntax, "Need to specify filename for parent.xml on command line"
+
+ r, parent_handle = self.zoo.configure_parent(argv[0], parent_handle)
+ r.save("%s.%s.repository-request.xml" % (self.zoo.handle, parent_handle), sys.stdout)
+
+
+ def do_delete_parent(self, arg):
+ """
+ Delete a parent of this RPKI entity.
+ """
+
+ try:
+ self.zoo.delete_parent(arg)
+ except rpki.irdb.Parent.DoesNotExist:
+ print "No such parent \"%s\"" % arg
+
+ def complete_delete_parent(self, *args):
+ return self.irdb_handle_complete(rpki.irdb.Parent, *args)
+
+
+ def do_configure_publication_client(self, arg):
+ """
+ Configure publication server to know about a new client, given the
+ client's request-for-service message as input. This command reads
+ the client's request for service, cross-certifies the client's
+ BPKI data, and generates a response message containing the
+ repository's BPKI data and service URI.
+ """
+
+ sia_base = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["sia_base="])
+ for o, a in opts:
+ if o == "--sia_base":
+ sia_base = a
+
+ if len(argv) != 1:
+ raise BadCommandSyntax, "Need to specify filename for client.xml"
+
+ r, client_handle = self.zoo.configure_publication_client(argv[0], sia_base)
+ r.save("%s.repository-response.xml" % client_handle.replace("/", "."), sys.stdout)
+
+
+ def do_delete_publication_client(self, arg):
+ """
+ Delete a publication client of this RPKI entity.
+ """
+
+ try:
+ self.zoo.delete_publication_client(arg).delete()
+ except rpki.irdb.Client.DoesNotExist:
+ print "No such client \"%s\"" % arg
+
+ def complete_delete_publication_client(self, *args):
+ return self.irdb_handle_complete(rpki.irdb.Client, *args)
+
+
+ def do_configure_repository(self, arg):
+ """
+ Configure a publication repository for this RPKI entity, given the
+ repository's response to our request-for-service message as input.
+ This command reads the repository's response, extracts and
+ cross-certifies the BPKI data and service URI, and links the
+ repository data with the corresponding parent data in our local
+ database.
+ """
+
+ parent_handle = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="])
+ for o, a in opts:
+ if o == "--parent_handle":
+ parent_handle = a
+
+ if len(argv) != 1:
+ raise BadCommandSyntax, "Need to specify filename for repository.xml on command line"
+
+ self.zoo.configure_repository(argv[0], parent_handle)
+
+ def do_delete_repository(self, arg):
+ """
+ Delete a repository of this RPKI entity.
+
+ This should check that the XML file it's deleting really is a
+ repository, but doesn't, yet.
+ """
+
+ try:
+ self.zoo.delete_repository(arg)
+ except rpki.irdb.Repository.DoesNotExist:
+ print "No such repository \"%s\"" % arg
+
+ def complete_delete_repository(self, *args):
+ return self.irdb_handle_complete(rpki.irdb.Repository, *args)
+
+
+ def do_delete_self(self, arg):
+ """
+ Delete the current RPKI entity (<self/> object).
+ """
+
+ self.zoo.delete_self()
+
+
+ def do_renew_child(self, arg):
+ """
+ Update validity period for one child entity.
+ """
+
+ valid_until = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["valid_until"])
+ for o, a in opts:
+ if o == "--valid_until":
+ valid_until = a
+
+ if len(argv) != 1:
+ raise BadCommandSyntax, "Need to specify child handle"
+
+ self.zoo.renew_children(argv[0], valid_until)
+
+ def complete_renew_child(self, *args):
+ return self.irdb_handle_complete(rpki.irdb.Child, *args)
+
+
+ def do_renew_all_children(self, arg):
+ """
+ Update validity period for all child entities.
+ """
+
+ valid_until = None
+
+ opts, argv = getopt.getopt(arg.split(), "", ["valid_until"])
+ for o, a in opts:
+ if o == "--valid_until":
+ valid_until = a
+
+ if len(argv) != 0:
+ raise BadCommandSyntax, "Unexpected arguments"
+
+ self.zoo.renew_children(None, valid_until)
+
+
+ def do_load_prefixes(self, arg):
+ """
+ Load prefixes into IRDB from CSV file.
+ """
+
+ argv = arg.split()
+
+ if len(argv) != 1:
+ raise BadCommandSyntax("Need to specify prefixes.csv filename")
+
+ self.zoo.load_prefixes(argv[0])
+
+
+ def do_show_child_resources(self, arg):
+ """
+ Show resources assigned to children.
+ """
+
+ if arg.strip():
+ raise BadCommandSyntax("This command takes no arguments")
+
+ for child in self.zoo.resource_ca.children.all():
+ resources = child.resource_bag
+
+ print "Child:", child.handle
+ if resources.asn:
+ print " ASN:", resources.asn
+ if resources.v4:
+ print " IPv4:", resources.v4
+ if resources.v6:
+ print " IPv6:", resources.v6
+
+
+ def do_load_asns(self, arg):
+ """
+ Load ASNs into IRDB from CSV file.
+ """
+
+ argv = arg.split()
+
+ if len(argv) != 1:
+ raise BadCommandSyntax("Need to specify asns.csv filename")
+
+ self.zoo.load_asns(argv[0])
+
+
+ def do_load_roa_requests(self, arg):
+ """
+ Load ROA requests into IRDB from CSV file.
+ """
+
+ argv = arg.split()
+
+ if len(argv) != 1:
+ raise BadCommandSyntax("Need to specify roa.csv filename")
+
+ self.zoo.load_roa_requests(argv[0])
+
+
+ def do_synchronize(self, arg):
+ """
+ Whack daemons to match IRDB.
+
+ This command may be replaced by implicit synchronization embedded
+ in of other commands, haven't decided yet.
+ """
+
+ if arg:
+ raise BadCommandSyntax("Unexpected argument(s): %r" % arg)
+
+ self.zoo.synchronize()
diff --git a/rpkid/rpki/rpkid.py b/rpkid/rpki/rpkid.py
index 76b3c81a..7501a16a 100644
--- a/rpkid/rpki/rpkid.py
+++ b/rpkid/rpki/rpkid.py
@@ -242,7 +242,7 @@ class main(object):
raise
except Exception, e:
rpki.log.traceback()
- cb(500, reason = "Unhandled exception %s" % e)
+ cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e))
up_down_url_regexp = re.compile("/up-down/([-A-Z0-9_]+)/([-A-Z0-9_]+)$", re.I)
diff --git a/rpkid/rpki/sql_schemas.py b/rpkid/rpki/sql_schemas.py
index 154ab5c1..e7c65299 100644
--- a/rpkid/rpki/sql_schemas.py
+++ b/rpkid/rpki/sql_schemas.py
@@ -239,115 +239,6 @@ CREATE TABLE ghostbuster (
-- End:
'''
-## @var irdbd
-## SQL schema irdbd
-irdbd = '''-- $Id: irdbd.sql 3730 2011-03-21 12:42:43Z sra $
-
--- Copyright (C) 2009--2011 Internet Systems Consortium ("ISC")
---
--- Permission to use, copy, modify, and distribute this software for any
--- purpose with or without fee is hereby granted, provided that the above
--- copyright notice and this permission notice appear in all copies.
---
--- THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
--- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
--- AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
--- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
--- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
--- OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
--- PERFORMANCE OF THIS SOFTWARE.
-
--- Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
---
--- Permission to use, copy, modify, and distribute this software for any
--- purpose with or without fee is hereby granted, provided that the above
--- copyright notice and this permission notice appear in all copies.
---
--- THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
--- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
--- AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
--- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
--- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
--- OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
--- PERFORMANCE OF THIS SOFTWARE.
-
--- SQL objects needed by irdbd.py. You only need this if you're using
--- irdbd.py as your IRDB; if you have a "real" backend you can do
--- anything you like so long as you implement the relevant portion of
--- the left-right protocol.
-
--- DROP TABLE commands must be in correct (reverse dependency) order
--- to satisfy FOREIGN KEY constraints.
-
-DROP TABLE IF EXISTS roa_request_prefix;
-DROP TABLE IF EXISTS roa_request;
-DROP TABLE IF EXISTS registrant_net;
-DROP TABLE IF EXISTS registrant_asn;
-DROP TABLE IF EXISTS registrant;
-DROP TABLE IF EXISTS ghostbuster_request;
-
-CREATE TABLE registrant (
- registrant_id SERIAL NOT NULL,
- registrant_handle VARCHAR(255) NOT NULL,
- registrant_name TEXT,
- registry_handle VARCHAR(255),
- valid_until DATETIME NOT NULL,
- PRIMARY KEY (registrant_id),
- UNIQUE (registry_handle, registrant_handle)
-) ENGINE=InnoDB;
-
-CREATE TABLE registrant_asn (
- registrant_asn_id SERIAL NOT NULL,
- start_as BIGINT UNSIGNED NOT NULL,
- end_as BIGINT UNSIGNED NOT NULL,
- registrant_id BIGINT UNSIGNED NOT NULL,
- PRIMARY KEY (registrant_asn_id),
- CONSTRAINT registrant_asn_registrant_id
- FOREIGN KEY (registrant_id) REFERENCES registrant (registrant_id) ON DELETE CASCADE
-) ENGINE=InnoDB;
-
-CREATE TABLE registrant_net (
- registrant_net_id SERIAL NOT NULL,
- start_ip VARCHAR(40) NOT NULL,
- end_ip VARCHAR(40) NOT NULL,
- version TINYINT UNSIGNED NOT NULL,
- registrant_id BIGINT UNSIGNED NOT NULL,
- PRIMARY KEY (registrant_net_id),
- CONSTRAINT registrant_net_registrant_id
- FOREIGN KEY (registrant_id) REFERENCES registrant (registrant_id) ON DELETE CASCADE
-) ENGINE=InnoDB;
-
-CREATE TABLE roa_request (
- roa_request_id SERIAL NOT NULL,
- roa_request_handle VARCHAR(255) NOT NULL,
- asn BIGINT UNSIGNED NOT NULL,
- PRIMARY KEY (roa_request_id)
-) ENGINE=InnoDB;
-
-CREATE TABLE roa_request_prefix (
- prefix VARCHAR(40) NOT NULL,
- prefixlen TINYINT UNSIGNED NOT NULL,
- max_prefixlen TINYINT UNSIGNED NOT NULL,
- version TINYINT UNSIGNED NOT NULL,
- roa_request_id BIGINT UNSIGNED NOT NULL,
- PRIMARY KEY (roa_request_id, prefix, prefixlen, max_prefixlen),
- CONSTRAINT roa_request_prefix_roa_request_id
- FOREIGN KEY (roa_request_id) REFERENCES roa_request (roa_request_id) ON DELETE CASCADE
-) ENGINE=InnoDB;
-
-CREATE TABLE ghostbuster_request (
- ghostbuster_request_id SERIAL NOT NULL,
- self_handle VARCHAR(40) NOT NULL,
- parent_handle VARCHAR(40),
- vcard LONGBLOB NOT NULL,
- PRIMARY KEY (ghostbuster_request_id)
-) ENGINE=InnoDB;
-
--- Local Variables:
--- indent-tabs-mode: nil
--- End:
-'''
-
## @var pubd
## SQL schema pubd
pubd = '''-- $Id: pubd.sql 3465 2010-10-07 00:59:39Z sra $
diff --git a/rpkid/rpki/x509.py b/rpkid/rpki/x509.py
index b96dec3f..955b8d97 100644
--- a/rpkid/rpki/x509.py
+++ b/rpkid/rpki/x509.py
@@ -47,6 +47,7 @@ import rpki.POW, rpki.POW.pkix, base64, lxml.etree, os, subprocess, sys
import email.mime.application, email.utils, mailbox, time
import rpki.exceptions, rpki.resource_set, rpki.oids, rpki.sundial
import rpki.manifest, rpki.roa, rpki.log, rpki.async, rpki.ghostbuster
+import rpki.relaxng
def base64_with_linebreaks(der):
"""
@@ -120,6 +121,74 @@ def _find_xia_uri(extension, name):
return location[1]
return None
+class X501DN(object):
+ """
+ Class to hold an X.501 Distinguished Name.
+
+ This is nothing like a complete implementation, just enough for our
+ purposes. POW has one interface to this, POW.pkix has another. In
+ terms of completeness in the Python representation, the POW.pkix
+ representation is much closer to right, but the whole thing is a
+ horrible mess.
+
+ See RFC 5280 4.1.2.4 for the ASN.1 details. In brief:
+
+ - A DN is a SEQUENCE of RDNs.
+
+ - A RDN is a set of AttributeAndValues; in practice, multi-value
+ RDNs are rare, so an RDN is almost always a set with a single
+ element.
+
+ - An AttributeAndValue is an OID and a value, where a whole bunch
+ of things including both syntax and semantics of the value are
+ determined by the OID.
+
+ - The value is some kind of ASN.1 string; there are far too many
+ encoding options options, most of which are either strongly
+ discouraged or outright forbidden by the PKIX profile, but which
+ persist for historical reasons. The only ones PKIX actually
+ likes are PrintableString and UTF8String, but there are nuances
+ and special cases where some of the others are required.
+
+ The RPKI profile further restricts DNs to a single mandatory
+ CommonName attribute with a single optional SerialNumber attribute
+ (not to be confused with the certificate serial number).
+
+ BPKI certificates should (we hope) follow the general PKIX guideline
+ but the ones we construct ourselves are likely to be relatively
+ simple.
+
+ The main purpose of this class is to hide as much as possible of
+ this mess from code that has to work with these wretched things.
+ """
+
+ def __init__(self, ini = None, **kwargs):
+ assert ini is None or not kwargs
+ if len(kwargs) == 1 and "CN" in kwargs:
+ ini = kwargs.pop("CN")
+ if isinstance(ini, (str, unicode)):
+ self.dn = (((rpki.oids.name2oid["commonName"], ("printableString", ini)),),)
+ elif isinstance(ini, tuple):
+ self.dn = ini
+ elif kwargs:
+ raise NotImplementedError("Sorry, I haven't implemented keyword arguments yet")
+ elif ini is not None:
+ raise TypeError("Don't know how to interpret %r as an X.501 DN" % (ini,), ini)
+
+ def __str__(self):
+ return "".join("/" + "+".join("%s=%s" % (rpki.oids.oid2name[a[0]], a[1][1])
+ for a in rdn)
+ for rdn in self.dn)
+
+ def __cmp__(self, other):
+ return cmp(self.dn, other.dn)
+
+ def get_POWpkix(self):
+ return self.dn
+
+ def get_POW(self):
+ raise NotImplementedError("Sorry, I haven't written the conversion to POW format yet")
+
class DER_object(object):
"""
Virtual class to hold a generic DER object.
@@ -259,6 +328,8 @@ class DER_object(object):
return -1
elif other is None:
return 1
+ elif isinstance(other, str):
+ return cmp(self.get_DER(), other)
else:
return cmp(self.get_DER(), other.get_DER())
@@ -456,13 +527,13 @@ class X509(DER_object):
"""
Get the issuer of this certificate.
"""
- return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer())
+ return X501DN(self.get_POWpkix().getIssuer())
def getSubject(self):
"""
Get the subject of this certificate.
"""
- return "".join("/%s=%s" % rdn for rdn in self.get_POW().getSubject())
+ return X501DN(self.get_POWpkix().getSubject())
def getNotBefore(self):
"""
@@ -497,11 +568,60 @@ class X509(DER_object):
def issue(self, keypair, subject_key, serial, sia, aia, crldp, notAfter,
cn = None, resources = None, is_ca = True):
"""
- Issue a certificate.
+ Issue an RPKI certificate.
+ """
+
+ assert aia is not None and crldp is not None
+
+ return self._issue(
+ keypair = keypair,
+ subject_key = subject_key,
+ serial = serial,
+ sia = sia,
+ aia = aia,
+ crldp = crldp,
+ notAfter = notAfter,
+ cn = cn,
+ resources = resources,
+ is_ca = is_ca,
+ aki = self.get_SKI(),
+ issuer_name = self.get_POWpkix().getSubject())
+
+
+ @classmethod
+ def self_certify(cls, keypair, subject_key, serial, sia, notAfter,
+ cn = None, resources = None):
+ """
+ Generate a self-certified RPKI certificate.
+ """
+
+ ski = subject_key.get_SKI()
+ if cn is None:
+ cn = "".join(("%02X" % ord(i) for i in ski))
+
+ return cls._issue(
+ keypair = keypair,
+ subject_key = subject_key,
+ serial = serial,
+ sia = sia,
+ aia = None,
+ crldp = None,
+ notAfter = notAfter,
+ cn = cn,
+ resources = resources,
+ is_ca = True,
+ aki = ski,
+ issuer_name = (((rpki.oids.name2oid["commonName"], ("printableString", cn)),),))
+
+
+ @staticmethod
+ def _issue(keypair, subject_key, serial, sia, aia, crldp, notAfter,
+ cn, resources, is_ca, aki, issuer_name):
+ """
+ Common code to issue an RPKI certificate.
"""
now = rpki.sundial.now()
- aki = self.get_SKI()
ski = subject_key.get_SKI()
if cn is None:
@@ -512,7 +632,7 @@ class X509(DER_object):
cert = rpki.POW.pkix.Certificate()
cert.setVersion(2)
cert.setSerial(serial)
- cert.setIssuer(self.get_POWpkix().getSubject())
+ cert.setIssuer(issuer_name)
cert.setSubject((((rpki.oids.name2oid["commonName"], ("printableString", cn)),),))
cert.setNotBefore(now.toASN1tuple())
cert.setNotAfter(notAfter.toASN1tuple())
@@ -520,10 +640,15 @@ class X509(DER_object):
exts = [ ["subjectKeyIdentifier", False, ski],
["authorityKeyIdentifier", False, (aki, (), None)],
- ["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)],
- ["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)],
["certificatePolicies", True, ((rpki.oids.name2oid["id-cp-ipAddr-asNumber"], ()),)] ]
+
+ if crldp is not None:
+ exts.append(["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)])
+
+ if aia is not None:
+ exts.append(["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)])
+
if is_ca:
exts.append(["basicConstraints", True, (1, None)])
exts.append(["keyUsage", True, (0, 0, 0, 0, 0, 1, 1)])
@@ -555,33 +680,96 @@ class X509(DER_object):
return X509(POWpkix = cert)
- def cross_certify(self, keypair, source_cert, serial, notAfter, now = None, pathLenConstraint = 0):
+ def bpki_cross_certify(self, keypair, source_cert, serial, notAfter,
+ now = None, pathLenConstraint = 0):
+ """
+ Issue a BPKI certificate with values taking from an existing certificate.
+ """
+ return self.bpki_certify(
+ keypair = keypair,
+ subject_name = source_cert.getSubject(),
+ subject_key = source_cert.getPublicKey(),
+ serial = serial,
+ notAfter = notAfter,
+ now = now,
+ pathLenConstraint = pathLenConstraint,
+ is_ca = True)
+
+ @classmethod
+ def bpki_self_certify(cls, keypair, subject_name, serial, notAfter,
+ now = None, pathLenConstraint = None):
+ """
+ Issue a self-signed BPKI CA certificate.
+ """
+ return cls._bpki_certify(
+ keypair = keypair,
+ issuer_name = subject_name,
+ subject_name = subject_name,
+ subject_key = keypair.get_RSApublic(),
+ serial = serial,
+ now = now,
+ notAfter = notAfter,
+ pathLenConstraint = pathLenConstraint,
+ is_ca = True)
+
+ def bpki_certify(self, keypair, subject_name, subject_key, serial, notAfter, is_ca,
+ now = None, pathLenConstraint = None):
+ """
+ Issue a normal BPKI certificate.
+ """
+ assert keypair.get_RSApublic() == self.getPublicKey()
+ return self._bpki_certify(
+ keypair = keypair,
+ issuer_name = self.getSubject(),
+ subject_name = subject_name,
+ subject_key = subject_key,
+ serial = serial,
+ now = now,
+ notAfter = notAfter,
+ pathLenConstraint = pathLenConstraint,
+ is_ca = is_ca)
+
+ @classmethod
+ def _bpki_certify(cls, keypair, issuer_name, subject_name, subject_key,
+ serial, now, notAfter, pathLenConstraint, is_ca):
"""
- Issue a certificate with values taking from an existing certificate.
- This is used to construct some kinds oF BPKI certificates.
+ Issue a BPKI certificate. This internal method does the real
+ work, after one of the wrapper methods has extracted the relevant
+ fields.
"""
if now is None:
now = rpki.sundial.now()
- assert isinstance(pathLenConstraint, int) and pathLenConstraint >= 0
+ issuer_key = keypair.get_RSApublic()
+
+ assert (issuer_key == subject_key) == (issuer_name == subject_name)
+ assert is_ca or issuer_name != subject_name
+ assert is_ca or pathLenConstraint is None
+ assert pathLenConstraint is None or (isinstance(pathLenConstraint, (int, long)) and
+ pathLenConstraint >= 0)
+
+ extensions = [
+ (rpki.oids.name2oid["subjectKeyIdentifier" ], False, subject_key.get_SKI())]
+ if issuer_key != subject_key:
+ extensions.append(
+ (rpki.oids.name2oid["authorityKeyIdentifier"], False, (issuer_key.get_SKI(), (), None)))
+ if is_ca:
+ extensions.append(
+ (rpki.oids.name2oid["basicConstraints" ], True, (1, pathLenConstraint)))
cert = rpki.POW.pkix.Certificate()
cert.setVersion(2)
cert.setSerial(serial)
- cert.setIssuer(self.get_POWpkix().getSubject())
- cert.setSubject(source_cert.get_POWpkix().getSubject())
+ cert.setIssuer(issuer_name.get_POWpkix())
+ cert.setSubject(subject_name.get_POWpkix())
cert.setNotBefore(now.toASN1tuple())
cert.setNotAfter(notAfter.toASN1tuple())
- cert.tbs.subjectPublicKeyInfo.set(
- source_cert.get_POWpkix().tbs.subjectPublicKeyInfo.get())
- cert.setExtensions((
- (rpki.oids.name2oid["subjectKeyIdentifier" ], False, source_cert.get_SKI()),
- (rpki.oids.name2oid["authorityKeyIdentifier"], False, (self.get_SKI(), (), None)),
- (rpki.oids.name2oid["basicConstraints" ], True, (1, 0))))
+ cert.tbs.subjectPublicKeyInfo.fromString(subject_key.get_DER())
+ cert.setExtensions(extensions)
cert.sign(keypair.get_POW(), rpki.POW.SHA256_DIGEST)
- return X509(POWpkix = cert)
+ return cls(POWpkix = cert)
@classmethod
def normalize_chain(cls, chain):
@@ -628,6 +816,12 @@ class PKCS10(DER_object):
self.POWpkix = req
return self.POWpkix
+ def getSubject(self):
+ """
+ Extract the subject name from this certification request.
+ """
+ return X501DN(self.get_POWpkix().certificationRequestInfo.subject.get())
+
def getPublicKey(self):
"""
Extract the public key from this certification request.
@@ -1262,7 +1456,10 @@ class XML_CMS_object(CMS_object):
Wrap an XML PDU in CMS and return its DER encoding.
"""
rpki.log.trace()
- self.set_content(msg.toXML())
+ if self.saxify is None:
+ self.set_content(msg)
+ else:
+ self.set_content(msg.toXML())
self.schema_check()
self.sign(keypair, certs, crls)
if self.dump_outbound_cms:
@@ -1277,7 +1474,22 @@ class XML_CMS_object(CMS_object):
self.dump_inbound_cms.dump(self)
self.verify(ta)
self.schema_check()
- return self.saxify(self.get_content())
+ if self.saxify is None:
+ return self.get_content()
+ else:
+ return self.saxify(self.get_content())
+
+ ## @var saxify
+ # SAX handler hook. Subclasses can set this to a SAX handler, in
+ # which case .unwrap() will call it and return the result.
+ # Otherwise, .unwrap() just returns a verified element tree.
+
+ saxify = None
+
+class SignedReferral(XML_CMS_object):
+ encoding = "us-ascii"
+ schema = rpki.relaxng.myrpki
+ saxify = None
class Ghostbuster(CMS_object):
"""
@@ -1373,7 +1585,7 @@ class CRL(DER_object):
"""
Get issuer value of this CRL.
"""
- return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer())
+ return X501DN(self.get_POWpkix().getIssuer())
@classmethod
def generate(cls, keypair, issuer, serial, thisUpdate, nextUpdate, revokedCertificates, version = 1, digestType = "sha256WithRSAEncryption"):