diff options
author | Michael Elkins <melkins@tislabs.com> | 2011-06-09 20:07:11 +0000 |
---|---|---|
committer | Michael Elkins <melkins@tislabs.com> | 2011-06-09 20:07:11 +0000 |
commit | 949ff01f80f57cac773ec543d13fbf412ce27780 (patch) | |
tree | e0261799e20dc4106057777437cb29219b1b2e0e | |
parent | 48ee451dad61a7e3b4222f85037db7a8b63d6fa9 (diff) |
add support for browing the rcynic cache
svn path=/rpkid/portal-gui/scripts/rpkigui-rcynic.py; revision=3859
24 files changed, 951 insertions, 4 deletions
diff --git a/rpkid/portal-gui/scripts/rpkigui-rcynic.py b/rpkid/portal-gui/scripts/rpkigui-rcynic.py new file mode 100644 index 00000000..2069443d --- /dev/null +++ b/rpkid/portal-gui/scripts/rpkigui-rcynic.py @@ -0,0 +1,216 @@ +# $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. +# + +import os, sys, time +os.environ['DJANGO_SETTINGS_MODULE'] = 'rpki.gui.settings' + +from rpki.gui.cacheview import models +from rpki.rcynic import rcynic_xml_iterator +from rpki.sundial import datetime +import vobject + +debug = True + +def process_object(obj, model_class): + """ + do initial processing on a rcynic_object instance. + + return value is a tuple: first element is a boolean value indicating whether + the object is changed/new since the last time we processed it. second + element is the db instance. + """ + if debug: + print 'processing %s at %s' % (obj.__class__.__name__, obj.uri) + + q = model_class.objects.filter(uri=obj.uri) + if not q: + if debug: + print 'creating new db instance' + inst = model_class(uri=obj.uri) + else: + inst = q[0] + + inst.ok = obj.ok + inst.status = obj.status + inst.timestamp = datetime.fromXMLtime(obj.timestamp).to_sql() + + mtime = os.stat(obj.filename)[8] + if mtime != inst.mtime: + inst.mtime = mtime + inst.not_before = obj.notBefore.to_sql() + inst.not_after = obj.notAfter.to_sql() + + # look up signing cert + q = models.Cert.objects.filter(keyid=obj.aki) + if q: + inst.issuer = q[0] + else: + sys.stderr.write('warning: unable to find signing cert with ski=%s (%s)\n' % (obj.aki, obj.issuer)) + + return True, inst + elif debug: + print 'object is unchanged' + + inst.save() + + return False, inst + +def process_rescert(cert): + """ + Process a RPKI resource certificate. + """ + + refresh, obj = process_object(cert, models.Cert) + + if refresh: + obj.name = cert.subject + obj.keyid = cert.ski + obj.save() + + # resources can change when a cert is updated + obj.asns.clear() + obj.addresses.clear() + + for asr in cert.resources.asn: + if debug: + sys.stderr.write('processing %s\n' % asr) + + q = models.ASRange.objects.filter(min=asr.min, max=asr.max) + if not q: + obj.asns.create(min=asr.min, max=asr.max) + else: + obj.asns.add(q[0]) + + for family, addrset in (4, cert.resources.v4), (6, cert.resources.v6): + for rng in addrset: + if debug: + sys.stderr.write('processing %s\n' % rng) + + minaddr = str(rng.min) + maxaddr = str(rng.max) + q = models.AddressRange.objects.filter(family=family, min=minaddr, max=maxaddr) + if not q: + obj.addresses.create(family=family, min=minaddr, max=maxaddr) + else: + obj.addresses.add(q[0]) + + if debug: + print 'finished processing rescert at %s' % cert.uri + + return obj + +def process_ghostbuster(gbr): + refresh, obj = process_object(gbr, models.Ghostbuster) + + if True: + #if refresh: + vcard = vobject.readOne(gbr.vcard) + if debug: + vcard.prettyPrint() + if hasattr(vcard, 'fn'): + obj.full_name = vcard.fn.value + if hasattr(vcard, 'email'): + obj.email_address = vcard.email.value + if hasattr(vcard, 'tel'): + obj.telephone = vcard.tel.value + if hasattr(vcard, 'org'): + obj.organization = vcard.org.value[0] + obj.save() + +fam_map = { 'roa_prefix_set_ipv6': 6, 'roa_prefix_set_ipv4': 4 } + +def process_roa(roa): + refresh, obj = process_object(roa, models.ROA) + + if refresh: + obj.asid = roa.asID + obj.save() + obj.prefixes.clear() + for pfxset in roa.prefix_sets: + family = fam_map[pfxset.__class__.__name__] + for pfx in pfxset: + attrs = { 'family' : family, + 'prefix': str(pfx.prefix), + 'bits' : pfx.prefixlen, + 'max_length': pfx.max_prefixlen } + q = models.ROAPrefix.objects.filter(**attrs) + if not q: + obj.prefixes.create(**attrs) + else: + obj.prefixes.add(q[0]) + else: + obj.save() + + return obj + +def trydelete(seq): + """ + iterate over a sequence and attempt to delete each item. safely + ignore IntegrityError since the object may be referenced elsewhere. + """ + for o in seq: + try: + o.delete() + except IntegrityError: + pass + +def garbage_collect(ts): + """ + rcynic's XML output file tells us what is currently in the cache, + but not what has been removed. we save the timestamp from the first + entry in the XML file, and remove all objects which are older. + """ + if debug: + print 'doing garbage collection' + + for roa in models.ROA.objects.filter(timestamp__lt=ts): + trydelete(roa.prefixes) + roa.delete() + + for cert in models.Cert.objects.filter(timestamp__lt=ts): + trydelete(cert.asns) + trydelete(cert.addresses) + cert.delete() + + for gbr in models.Ghostbuster.objects.filter(timestamp__lt=ts): + gbr.delete() + +dispatch = { + 'rcynic_certificate': process_rescert, + 'rcynic_roa' : process_roa, + 'rcynic_ghostbuster': process_ghostbuster +} + +def process_cache(root='/var/rcynic/data', xml_file='/var/rcynic/data/rcynic.xml'): + start = time.time() + + first = True + ts = datetime.now() + for obj in rcynic_xml_iterator(root, xml_file): + r = dispatch[obj.__class__.__name__](obj) + if first: + first = False + ts = r.timestamp + garbage_collect(ts) + + if debug: + stop = time.time() + sys.stdout.write('elapsed time %d seconds.\n' % (stop - start)) + +if __name__ == '__main__': + process_cache() + +# vim:sw=4 ts=8 diff --git a/rpkid/rpki/gui/app/admin.py b/rpkid/rpki/gui/app/admin.py index 8b7cd24a..52dc2c87 100644 --- a/rpkid/rpki/gui/app/admin.py +++ b/rpkid/rpki/gui/app/admin.py @@ -58,3 +58,5 @@ 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/urls.py b/rpkid/rpki/gui/app/urls.py index 967baeca..050a03a7 100644 --- a/rpkid/rpki/gui/app/urls.py +++ b/rpkid/rpki/gui/app/urls.py @@ -16,7 +16,6 @@ PERFORMANCE OF THIS SOFTWARE. """ from django.conf.urls.defaults import * -from django.views.generic.list_detail import object_list from rpki.gui.app import views urlpatterns = patterns('', diff --git a/rpkid/rpki/gui/cacheview/__init__.py b/rpkid/rpki/gui/cacheview/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rpkid/rpki/gui/cacheview/__init__.py diff --git a/rpkid/rpki/gui/cacheview/admin.py b/rpkid/rpki/gui/cacheview/admin.py new file mode 100644 index 00000000..af8b31da --- /dev/null +++ b/rpkid/rpki/gui/cacheview/admin.py @@ -0,0 +1,47 @@ +""" +$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 + +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) + +# vim:sw=4 ts=8 diff --git a/rpkid/rpki/gui/cacheview/forms.py b/rpkid/rpki/gui/cacheview/forms.py new file mode 100644 index 00000000..cc33016e --- /dev/null +++ b/rpkid/rpki/gui/cacheview/forms.py @@ -0,0 +1,46 @@ +# $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 import forms + +from rpki.gui.cacheview.misc import parse_ipaddr +from rpki.exceptions import BadIPResource +from rpki.resource_set import resource_range_as + +class SearchForm(forms.Form): + asn = forms.CharField(required=False, help_text='AS or range', label='AS') + addr = forms.CharField(required=False, max_length=40, help_text='range/CIDR', label='IP Address') + + def clean(self): + asn = self.cleaned_data.get('asn') + addr = self.cleaned_data.get('addr') + if (asn and addr) or ((not asn) and (not addr)): + raise forms.ValidationError, 'Please specify either an AS or IP range, not both' + + if asn: + try: + resource_range_as.parse_str(asn) + except ValueError: + raise forms.ValidationError, 'invalid AS range' + + if addr: + #try: + parse_ipaddr(addr) + #except BadIPResource: + # raise forms.ValidationError, 'invalid IP address range/prefix' + + return self.cleaned_data diff --git a/rpkid/rpki/gui/cacheview/misc.py b/rpkid/rpki/gui/cacheview/misc.py new file mode 100644 index 00000000..393055e2 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/misc.py @@ -0,0 +1,33 @@ +""" +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 rpki.resource_set import resource_range_ipv4, resource_range_ipv6 +from rpki.exceptions import BadIPResource + +def parse_ipaddr(s): + # resource_set functions only accept str + if isinstance(s, unicode): + s = s.encode() + s = s.strip() + r = resource_range_ipv4.parse_str(s) + try: + r = resource_range_ipv4.parse_str(s) + return 4, r + except BadIPResource: + r = resource_range_ipv6.parse_str(s) + return 6, r + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/cacheview/models.py b/rpkid/rpki/gui/cacheview/models.py new file mode 100644 index 00000000..76314760 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/models.py @@ -0,0 +1,177 @@ +""" +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 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 + +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() + max = models.IPAddressField() + + class Meta: + ordering = ('family', 'min', 'max') + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.cacheview.views.address_view', [str(self.pk)]) + + def __unicode__(self): + if self.min == self.max: + return u'%s' % self.min + + try: + r = resource_range_ipv4.from_strings(self.min, self.max) + except ValueError: + 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') + + def __unicode__(self): + if self.min == self.max: + return u'AS%d' % self.min + else: + return u'%s-%s' % (self.min, self.max) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.cacheview.views.as_view', [str(self.pk)]) + +class SignedObject(models.Model): + """ + Abstract class to hold common metadata for all signed objects. + 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) + timestamp = models.DateTimeField() + ok = models.BooleanField() + status = models.CharField(max_length=255) + + mtime = models.PositiveIntegerField(default=0) + + # validity period from EE cert which signed object + not_before = models.DateTimeField() + not_after = models.DateTimeField() + + class Meta: + abstract = True + + def mtime_as_datetime(self): + """ + convert the local timestamp to UTC and convert to a datetime object + """ + return datetime.utcfromtimestamp(self.mtime + time.timezone) + +class Cert(SignedObject): + """ + Object representing a resource certificate. + """ + # SubjectName + name = models.CharField(max_length=255) + + # value from the SKI extension + keyid = models.CharField(max_length=50, db_index=True) + + 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) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.cacheview.views.cert_detail', [str(self.pk)]) + + def __unicode__(self): + return u'%s' % self.name + +class ROAPrefix(models.Model): + family = models.PositiveIntegerField() + prefix = models.IPAddressField() + bits = models.PositiveIntegerField() + max_length = models.PositiveIntegerField() + + class Meta: + ordering = ['family', 'prefix', 'bits', '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) + +class ROA(SignedObject): + asid = models.PositiveIntegerField() + prefixes = models.ManyToManyField(ROAPrefix, 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'] + + 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 Ghostbuster(SignedObject): + 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') + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.cacheview.views.ghostbuster_detail', [str(self.pk)]) + + def __unicode__(self): + if self.full_name: + return self.full_name + if self.organization: + return self.organization + if self.email_address: + return self.email_address + return self.telephone + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/address_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/address_detail.html new file mode 100644 index 00000000..069f4270 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/address_detail.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} + +<p> +Range: {{ object.min }} - {{ object.max }} +</p> + +<p>Covered by the following resource certs:</p> + +<ul> +{% for cert in object.certs.all %} +<li><a href="{{ cert.get_absolute_url }}">{{ cert.subject }}</a></li> +{% endfor %} +</ul> + +{% endblock %} diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/cacheview_base.html b/rpkid/rpki/gui/cacheview/templates/cacheview/cacheview_base.html new file mode 100644 index 00000000..1947401c --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/cacheview_base.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block sidebar %} +<ul class='compact'> + <li><a href="/cacheview/search">resource search</a> + <li><a href="/cacheview/query">roa search</a> + <li><a href="/rpki/">rpki dashboard</a> +</ul> +{% endblock %} + diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/cert_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/cert_detail.html new file mode 100644 index 00000000..80921ac0 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/cert_detail.html @@ -0,0 +1,84 @@ +{% extends "cacheview/signedobject_detail.html" %} + +{% block title %} +Resource Certificate Detail +{% endblock %} + +{% block detail %} + +<p> +<table> + <tr><td>Subject</td><td>{{ object.name }}</td></tr> + <tr><td>SKI</td><td>{{ object.keyid }}</td></tr> +</table> + +<h2>RFC3779 Resources</h2> + +<ul> + +<li> + +<h3>AS</h3> +{% if object.asns.all %} +<ul> +{% for asn in object.asns.all %} +<li>{{ asn }} +{% endfor %} +</ul> +{% else %} +<p>none</p> +{% endif %} + +<li> +<h2>IP Ranges</h2> + +{% if object.addresses.all %} +<ul> +{% for rng in object.addresses.all %} +<li>{{ rng }} +{% endfor %} +</ul> +{% else %} +<p>none</p> +{% endif %} +</ul><!--resources--> + +<h2>Issued Objects</h2> +<ul> + + <li> +<h3>Ghostbusters</h3> +{% if object.ghostbusters.all %} +<ul> +{% for g in object.ghostbusters.all %} +<li><a href="{{ g.get_absolute_url }}">{{ g }}</a> +{% endfor %} +</ul> +{% endif %} + + <li> +<h3>ROAs</h3> +{% if object.roas.all %} +<table> + <tr><th>Prefix</th><th>AS</th></tr> + {% for roa in object.roas.all %} + {% for pfx in roa.prefixes.all %} + <tr><td>{{ pfx }}</td><td>{{ roa.asid }}</td></tr> + {% endfor %} + {% endfor %} +</table> +{% endif %} + +<li> +<h3>Children</h3> +{% if object.children.all %} +<ul> + {% for child in object.children.all %} + <li><a href="{{ child.get_absolute_url }}">{{ child.name }}</a> + {% endfor %} +</ul> + +{% endif %} +</ul><!--issued objects--> + +{% endblock %} diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html new file mode 100644 index 00000000..d179e7c2 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html @@ -0,0 +1,13 @@ +{% extends "cacheview/signedobject_detail.html" %} + +{% block title %}Ghostbuster Detail{% endblock %} + +{% block detail %} +<p> +<table> + <tr><td>Full Name</td><td>{{ object.full_name }}</td></tr> + <tr><td>Organization</td><td>{{ object.organization }}</td></tr> + <tr><td>Email</td><td>{{ object.email_address }}</td></tr> + <tr><td>Telephone</td><td>{{ object.telephone }}</td></tr> +</table> +{% endblock %} diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/query_result.html b/rpkid/rpki/gui/cacheview/templates/cacheview/query_result.html new file mode 100644 index 00000000..3bc7e259 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/query_result.html @@ -0,0 +1,21 @@ +{% extends "cacheview/cacheview_base.html" %} + +{% block content %} + +<h1>{% block title %}Query Results{% endblock %}</h1> + +<table> + <tr><th>Prefix</th><th>AS</th><th>Valid</th><th>Until</th></tr> + {% for object in object_list %} + <tr> + <td>{{ object.0 }}</td> + <td>{{ object.1.asid }}</td> + <td><a href="{{ object.1.get_absolute_url }}">{{ object.1.ok }}</a></td> + <td>{{ object.1.not_after }}</td> + </tr> + {% endfor %} +</table> + +<p><a href="{% url rpki.gui.cacheview.views.query_view %}">new query</a></p> + +{% endblock %} diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/resourcecert_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/resourcecert_detail.html new file mode 100644 index 00000000..01c9f7bf --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/resourcecert_detail.html @@ -0,0 +1 @@ +{% extends "cacheview/signedobject_detail.html" %} diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/roa_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/roa_detail.html new file mode 100644 index 00000000..0ae91dbb --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/roa_detail.html @@ -0,0 +1,22 @@ +{% extends "cacheview/signedobject_detail.html" %} + +{% block title %} +<h1>ROA Detail</h1> +{% endblock %} + +{% block detail %} + +<p> +<table> + <tr><td>AS</td><td>{{ object.asid }}</td></tr> +</table> + +<h2>Prefixes</h2> + +<ul> +{% for pfx in object.prefixes.all %} +<li>{{ pfx }} +{% endfor %} +</ul> + +{% endblock %} diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/search_form.html b/rpkid/rpki/gui/cacheview/templates/cacheview/search_form.html new file mode 100644 index 00000000..1141615d --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/search_form.html @@ -0,0 +1,17 @@ +{% extends "cacheview/cacheview_base.html" %} + +{% block title %} +{{ search_type }} Search +{% endblock %} + +{% block content %} + +<h1>{{search_type}} Search</h1> + +<form method='post' action='{{ request.url }}'> + {% csrf_token %} + {{ form.as_p }} + <input type='submit' name='Search'> +</form> + +{% endblock %} diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/search_result.html b/rpkid/rpki/gui/cacheview/templates/cacheview/search_result.html new file mode 100644 index 00000000..3756047f --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/search_result.html @@ -0,0 +1,31 @@ +{% extends "cacheview/cacheview_base.html" %} + +{% block content %} + +<h1>Search Results</h1> + +<h2>Matching Resource Certificates</h2> +{% if certs %} +<ul> +{% for cert in certs %} +<li><a href="{{ cert.get_absolute_url }}">{{ cert }}</a> +{% endfor %} +</ul> +{% else %} +<p>none</p> +{% endif %} + +<h2>Matching ROAs</h2> +{% if roas %} +<ul> +{% for roa in roas %} +<li><a href="{{ roa.get_absolute_url }}">{{ roa }}</a> +{% endfor %} +</ul> +{% else %} +<p>none</p> +{% endif %} + +<p><a href="{% url rpki.gui.cacheview.views.search_view %}">new search</a> + +{% endblock %} diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html new file mode 100644 index 00000000..aa2ec444 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html @@ -0,0 +1,25 @@ +{% extends "cacheview/cacheview_base.html" %} + +{% block content %} +<h1>{% block title %}Signed Object Detail{% endblock %}</h1> + +<h2>Metadata</h2> + +<table> + <tr><td>URI</td><td>{{ object.uri }}</td></tr> + <tr><td>Last Modified</td><td>{{ object.mtime_as_datetime|date:"DATETIME_FORMAT" }}</td></tr> + <tr><td>Timestamp</td><td>{{ object.timestamp }}</td></tr> + <tr><td>Status</td><td>{{ object.status }}</td></tr> + <tr><td>Valid</td><td>{{ object.ok }}</td></tr> +</table> + +<h2>Auth Info</h2> + +<table> + <tr><td>Validity</td><td>{{ object.not_before }} - {{ object.not_after }}</td></tr> + <tr><td>Issuer</td><td><a href='{{object.issuer.get_absolute_url}}'>{{ object.issuer.name }}</a></td></tr> +</table> + +{% block detail %}{% endblock %} + +{% endblock %} diff --git a/rpkid/rpki/gui/cacheview/tests.py b/rpkid/rpki/gui/cacheview/tests.py new file mode 100644 index 00000000..2247054b --- /dev/null +++ b/rpkid/rpki/gui/cacheview/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/rpkid/rpki/gui/cacheview/urls.py b/rpkid/rpki/gui/cacheview/urls.py new file mode 100644 index 00000000..bbc14b35 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/urls.py @@ -0,0 +1,28 @@ +# $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.conf.urls.defaults import * + +urlpatterns = patterns('', + (r'^query$', 'rpki.gui.cacheview.views.query_view'), + (r'^search$', 'rpki.gui.cacheview.views.search_view'), + (r'^cert/(?P<pk>[^/]+)$', 'rpki.gui.cacheview.views.cert_detail'), + (r'^roa/(?P<pk>[^/]+)$', 'rpki.gui.cacheview.views.roa_detail'), + (r'^gbr/(?P<pk>[^/]+)$', 'rpki.gui.cacheview.views.ghostbuster_detail'), +) + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/cacheview/views.py b/rpkid/rpki/gui/cacheview/views.py new file mode 100644 index 00000000..733dc5b1 --- /dev/null +++ b/rpkid/rpki/gui/cacheview/views.py @@ -0,0 +1,132 @@ +""" +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.views.generic import list_detail +from django.shortcuts import get_object_or_404, redirect + +from rpki.gui.cacheview import models, forms, misc +from rpki.gui.app.views import render +from rpki.resource_set import resource_range_as +from rpki.ipaddrs import v4addr, v6addr + +# Create your views here. + +def address_detail(request, pk): + return list_detail.object_detail(request, models.AddressRange.objects.all(), pk) + +def as_detail(request, pk): + return list_detail.object_detail(request, models.ASRange.objects.all(), pk) + +def roa_detail(request, pk): + return list_detail.object_detail(request, models.ROA.objects.all(), pk) + +def cert_detail(request, pk): + return list_detail.object_detail(request, models.Cert.objects.all(), pk) + +def ghostbuster_detail(request, pk): + return list_detail.object_detail(request, models.Ghostbuster.objects.all(), pk) + +def search_view(request): + if request.method == 'POST': + form = forms.SearchForm(request.POST, request.FILES) + if form.is_valid(): + certs = None + roas = None + + addr = form.cleaned_data.get('addr') + asn = form.cleaned_data.get('asn') + + if addr: + family, r = misc.parse_ipaddr(addr) + certs = models.Cert.objects.filter(addresses__family=family, addresses__min__gte=r.min, addresses__max__lte=r.max).distinct() + roas = models.ROA.objects.filter(prefixes__family=family, prefixes__prefix=str(r.min)).distinct() + elif asn: + r = resource_range_as.parse_str(asn) + certs = models.Cert.objects.filter(asns__min__gte=r.min, asns__max__lte=r.max) + roas = models.ROA.objects.filter(asid__gte=r.min, asid__lte=r.max) + + return render('cacheview/search_result.html', { 'certs': certs, 'roas': roas }, request) + else: + form = forms.SearchForm() + + return render('cacheview/search_form.html', { 'form': form, 'search_type': 'Resource' }, request) + +def cmp_prefix(x,y): + r = cmp(x[0].family, y[0].family) + if r == 0: + r = cmp(x[2], y[2]) # integer address + if r == 0: + r = cmp(x[0].bits, y[0].bits) + if r == 0: + r = cmp(x[0].max_length, y[0].max_length) + if r == 0: + r = cmp(x[1].asid, y[1].asid) + return r + +#def cmp_prefix(x,y): +# for attr in ('family', 'prefix', 'bits', 'max_length'): +# r = cmp(getattr(x[0], attr), getattr(y[0], attr)) +# if r: +# return r +# return cmp(x[1].asid, y[1].asid) + +def query_view(request): + """ + Allow the user to search for an AS or prefix, and show all published ROA + information. + """ + + if request.method == 'POST': + form = forms.SearchForm(request.POST, request.FILES) + if form.is_valid(): + certs = None + roas = None + + addr = form.cleaned_data.get('addr') + asn = form.cleaned_data.get('asn') + + if addr: + family, r = misc.parse_ipaddr(addr) + prefixes = models.ROAPrefix.objects.filter(family=family, prefix=str(r.min)) + + prefix_list = [] + for pfx in prefixes: + for roa in pfx.roas.all(): + prefix_list.append((pfx, roa)) + elif asn: + r = resource_range_as.parse_str(asn) + roas = models.ROA.objects.filter(asid__gte=r.min, asid__lte=r.max) + + # display the results sorted by prefix + prefix_list = [] + for roa in roas: + for pfx in roa.prefixes.all(): + if pfx.family == 4: + addr = v4addr(pfx.prefix.encode()) + elif pfx.family == 6: + addr = v6addr(pfx.prefix.encode()) + + prefix_list.append((pfx, roa, addr)) + prefix_list.sort(cmp=cmp_prefix) + + return render('cacheview/query_result.html', + { 'object_list': prefix_list }, request) + else: + form = forms.SearchForm() + + return render('cacheview/search_form.html', { 'form':form, 'search_type': 'ROA ' }, request) + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/settings.py.in b/rpkid/rpki/gui/settings.py.in index 04d6aea8..898f7d31 100644 --- a/rpkid/rpki/gui/settings.py.in +++ b/rpkid/rpki/gui/settings.py.in @@ -58,7 +58,8 @@ INSTALLED_APPS = ( 'django.contrib.admindocs', 'django.contrib.contenttypes', 'django.contrib.sessions', - 'rpki.gui.app' + 'rpki.gui.app', + 'rpki.gui.cacheview' ) TEMPLATE_CONTEXT_PROCESSORS = ( diff --git a/rpkid/rpki/gui/urls.py b/rpkid/rpki/gui/urls.py index 919a812e..70ea4056 100644 --- a/rpkid/rpki/gui/urls.py +++ b/rpkid/rpki/gui/urls.py @@ -32,6 +32,7 @@ urlpatterns = patterns('', (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', diff --git a/rpkid/setup.py b/rpkid/setup.py index 679c7e7f..75d6069c 100644 --- a/rpkid/setup.py +++ b/rpkid/setup.py @@ -63,7 +63,8 @@ setup(name = "rpkitoolkit", description = "RPKI Toolkit", license = "BSD", url = "http://www.rpki.net/", - packages = ["rpki", "rpki.POW", "rpki.gui", "rpki.gui.app" ], + packages = ["rpki", "rpki.POW", "rpki.gui", "rpki.gui.app", "rpki.gui.cacheview" ], ext_modules = [pow], - package_data = { 'rpki.gui.app' : ['templates/*.html', 'templates/*/*.html'] }, + package_data = { 'rpki.gui.app' : ['templates/*.html', 'templates/*/*.html'], + 'rpki.gui.cacheview' : [ 'templates/*/*.html' ] }, data_files = data_files) |