aboutsummaryrefslogtreecommitdiff
path: root/potpourri/rpkidemo
blob: fdb4e1bb63af2059d76396bcb2581e92629242db (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
#!/usr/bin/env python

"""
Hosted GUI client startup script, for workshops, etc.

As of when this is run, we assume that the tarball (contents TBD and
perhaps changing from one workshop to another) have been unpacked,
that we are on some Unix-like machine, and that we are executing in
a Python interpreter.  We have to check anything else we care about.

In what we hope is the most common case, this script should be run
with no options.

$Id$

Copyright (C) 2010  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.
"""

# Check Python version before doing anything else

import sys

python_version = sys.version_info[:2]

have_ssl_module = python_version >= (2, 6)

if python_version == (2, 5):
  print """
        WARNING WARNING WARNING

        You are running Python version 2.5, which does not include
        real SSL support.  This means that sessions created by this
        script will be vulnerable to monkey-in-the-middle attacks.

        Python 2.6 does not have this problem.
        """
  while True:
    answer = raw_input("Are you SURE you want to proceed? (yes/NO) ").strip().lower()
    if answer in ("", "n", "no"):
      sys.exit("You have chosen wisely")
    elif answer in ("y", "yes"):
      print "You have been warned"
      break
    else:
      print 'Please answer "yes" or "no"'

elif have_ssl_module:
  try:
    import ssl
  except ImportError:
    sys.exit("You're running Python 2.6+, but I can't find the ssl module, so you have no SSL support at all, argh!")

else:
  sys.exit("Sorry, this script requires Python 2.6+, I seem to be running in %s" % sys.version)

# Ok, it's safe to import the other stuff we need now

import os, subprocess, webbrowser, urllib2, getpass, re, errno, time, email.utils, httplib, socket, getopt, urllib, cookielib
import tempfile
from xml.etree.ElementTree import fromstring as ElementFromString

def save(filename, data):
  """
  Save data to a file.
  """

  tempname = "%s.%d.tmp" % (filename, os.getpid())
  f = open(tempname, "w")
  f.write(data)
  f.close()
  os.rename(tempname, filename)

def save_error(err):
  """
  Save the data from the file-like object "f" into a temporary file
  and open a web browser to view the result.
  """

  with tempfile.NamedTemporaryFile(prefix = "rpkidemo-error", suffix = ".html", delete = False) as tmpf:
    tmpf.write(err.read())

    # Save filename for use outside the with statement.  This ensures
    # the file is properly flushed prior to invoking the web browser.
    fname = tmpf.name

  sys.stderr.write("errors saved in %s\n" % fname)
  webbrowser.open("file://" + fname)

class CSV_File(object):
  """
  A CSV file that's being maintained by the GUI but being monitored,
  downloaded, and used here.
  """

  def __init__(self, filename, url):
    self.filename = filename
    self.url = url
    try:
      self.timestamp = os.stat(filename).st_mtime
    except:
      self.store(0, "")

  def last_modified(self):
    """
    Return CSV file timestamp formatted for use with HTTP.
    """
    return email.utils.formatdate(self.timestamp, False, True)

  def store(self, timestamp, data):
    """
    Save CSV file, and record new timestamp.
    """
    save(self.filename, data)
    self.timestamp = timestamp
    os.utime(self.filename, (time.time(), timestamp))


class AbstractHTTPSConnection(httplib.HTTPSConnection):
  """
  Customization of httplib.HTTPSConnection to enable certificate
  validation.

  This is an abstract class; subclass must set trust_anchor to the
  filename of a anchor file in the format that the ssl module
  expects.
  """

  trust_anchor = None

  def connect(self):
    assert self.trust_anchor is not None
    sock = socket.create_connection((self.host, self.port), self.timeout)
    if getattr(self, "_tunnel_host", None):
      self.sock = sock
      self._tunnel()
    self.sock = ssl.wrap_socket(sock,
                                keyfile = self.key_file,
                                certfile = self.cert_file,
                                cert_reqs = ssl.CERT_REQUIRED,
                                ssl_version = ssl.PROTOCOL_TLSv1,
                                ca_certs = self.trust_anchor)


class main(object):
  """
  Main program.
  """

  # Environmental parameters

  top = os.path.realpath(os.path.join((sys.path[0] or "."), ".."))
  cwd = os.getcwd()

  # Parameters that we might want to get from a config file.
  # Just wire them all in for the moment.

  base_url = "https://demo.rpki.net/"
  myrpki_url = base_url + "rpki/"
  auth_url = myrpki_url + "demo/login"
  example_myrpki_cfg = "%s/rpkid/examples/rpki.conf" % top
  working_dir = "%s/rpkidemo-data" % cwd
  myrpki_py = "%s/rpkid/myrpki.py" % top
  user_agent = "RPKIDemo"
  delay = 15
  trust_anchor = "%s/scripts/rpkidemo.pem" % top

  openssl = None

  def setup_openssl(self):
    """
    Find a usable version of OpenSSL, or build one if we must.
    """

    def scrape(*args):
      return subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.STDOUT).communicate()[0]

    def usable_openssl(f):
      return f is not None and os.path.exists(f) and "-ss_cert" in scrape(f, "ca", "-?") and "Usage cms" in scrape(f, "cms", "-?")

    for d in os.environ["PATH"].split(":"):
      f = os.path.join(d, "openssl")
      if usable_openssl(f):
        self.openssl = f
        break

    if self.openssl is None:
      print "Couldn't find usable openssl on path, attempting to build one"
      subprocess.check_call(("./configure",), cwd = self.top)
      subprocess.check_call(("make",), cwd = os.path.join(self.top, "openssl"))
      self.openssl = os.path.join(self.top, "openssl", "openssl", "apps", "openssl")
      print "Done building openssl"
      print

    if usable_openssl(self.openssl):
      print "Using", self.openssl
    else:
      sys.exit("Could not find or build usable version of openssl, giving up")

  @staticmethod
  def setup_utc():
    """
    This script thinks in UTC.
    """

    os.environ["TZ"] = "UTC"
    time.tzset()

  def setup_username(self):
    """
    Get username and password for web interface, construct urllib2
    "opener" tailored for our use, perform an initial GET (ignoring
    result, other than exceptions) to test the username and password.
    """

    print "I need to know your username and password on the Django GUI server to proceed"

    while True:

      try:
        self.username = raw_input("Username: ")
        self.password = getpass.getpass()

        handlers = []

        self.cookiejar = cookielib.CookieJar()
        handlers.append(urllib2.HTTPCookieProcessor(self.cookiejar))

        if have_ssl_module:

          class HTTPSConnection(AbstractHTTPSConnection):
            trust_anchor = self.trust_anchor

          class HTTPSHandler(urllib2.HTTPSHandler):
            def https_open(self, req):
              return self.do_open(HTTPSConnection, req)

          handlers.append(HTTPSHandler)

        self.opener = urllib2.build_opener(*handlers)

        # Test login credentials
        resp = self.opener.open(self.auth_url) # GET

        r = self.opener.open(urllib2.Request(
          url = self.auth_url,
          data = urllib.urlencode({ "username"            : self.username,
                                    "password"            : self.password,
                                    "csrfmiddlewaretoken" : self.csrftoken() }),
          headers = { "Referer"    : self.auth_url,
                      "User-Agent" : self.user_agent})) # POST
        return

      except urllib2.URLError, e:
        print "Could not log in to server: %s" % e
        print "Please try again"
        save_error(e)

  def csrftoken(self):
    """
    Pull Django's CSFR token from cookie database.

    Django's login form requires the "csrfmiddlewaretoken."  It turns out
    this is the same value as the "csrftoken" cookie, so we don't need
    to bother parsing the form.
    """

    return [c.value for c in self.cookiejar if c.name == "csrftoken"][0]

  def setup_working_directory(self):
    """
    Create working directory and move to it.
    """

    try:
      print "Creating", self.working_dir
      os.mkdir(self.working_dir)
    except OSError, e:
      if e.errno != errno.EEXIST:
        raise
      print self.working_dir, "already exists, reusing it"
    os.chdir(self.working_dir)

  def setup_config_file(self):
    """
    Generate rpki.conf
    """
    
    if os.path.exists("rpki.conf"):
      print "You already have a rpki.conf file, so I will use it"
      return

    print "Generating rpki.conf"
    section_regexp = re.compile("\s*\[\s*(.+?)\s*\]\s*$")
    variable_regexp = re.compile("\s*([-a-zA-Z0-9_]+)\s*=\s*(.+?)\s*$")
    f = open("rpki.conf", "w")
    f.write("# Automatically generated, do not edit\n")
    section = None
    for line in open(self.example_myrpki_cfg):
      m = section_regexp.match(line)
      if m:
        section = m.group(1)
      m = variable_regexp.match(line)
      option = m.group(1) if m and section == "myrpki" else None
      value  = m.group(2) if option else None
      if option == "handle":
        line = "handle = %s\n" % self.username
      if option == "openssl":
        line = "openssl = %s\n" % self.openssl
      if option in ("run_rpkid", "run_pubd", "run_rootd") and value != "false":
        line = "%s = false\n" % option
      f.write(line)
    f.close()

  def myrpki(self, *cmd):
    """
    Run a myrpki command.
    """
    return subprocess.check_call((sys.executable, self.myrpki_py) + cmd)

  def upload(self, url, filename):
    """
    Upload filename to URL, return result.
    """

    url = "%s%s/%s" % (self.myrpki_url, url, self.username)
    data = open(filename).read()
    print "Uploading", filename, "to", url
    post_data = urllib.urlencode({
      "content"             : data,
      "csrfmiddlewaretoken" : self.csrftoken() }) # POST
    try:
      return self.opener.open(urllib2.Request(url, post_data, {
        "User-Agent"   : self.user_agent,
        "Referer"      : url}))
    except urllib2.HTTPError, e:
      sys.stderr.write("Problem uploading to URL %s\n" % url)
      save_error(e)
      raise

  def update(self):
    """
    Run configure_resources, upload result, download updated result.
    """

    self.myrpki("configure_resources")
    r = self.upload("demo/myrpki-xml", "myrpki.xml")
    save("myrpki.xml", r.read())

  def setup_csv_files(self):
    """
    Create CSV file objects and synchronize timestamps.
    """

    self.csv_files = [
      CSV_File("asns.csv",     "demo/down/asns/%s"     % self.username),
      CSV_File("prefixes.csv", "demo/down/prefixes/%s" % self.username),
      CSV_File("roas.csv",     "demo/down/roas/%s"     % self.username) ]

  def upload_for_response(self, url, path):
    """
    Upload an XML file to the requested URL and wait for for the server
    to signal that a response is ready.
    """

    self.upload(url, path)

    print """
          Waiting for response to upload.  This may require action by a human
          being on the server side, so it may take a while, please be patient.
          """

    while True:
      try:
        return self.opener.open(urllib2.Request(
          "%s%s/%s" % (self.myrpki_url, url, self.username),
          None,
          { "User-Agent" : self.user_agent }))
      except urllib2.HTTPError, e:
        # Portal GUI uses response code 503 to signal "not ready"
        if e.code != 503:
          sys.stderr.write("Problem getting response from %s: %s\n" % (url, e))
          save_error(e)
          raise
      time.sleep(self.delay)

  def setup_parent(self):
    """
    Upload the user's identity.xml and wait for the portal gui to send
    back the parent.xml response.
    """

    r = self.upload_for_response("demo/parent-request", "entitydb/identity.xml")
    parent_data = r.read()
    save("parent.xml", parent_data)
    self.myrpki("configure_parent", "parent.xml")

    # Extract the parent_handle from the xml response and save it for use by
    # setup_repository()
    self.parent_handle = ElementFromString(parent_data).get("parent_handle")

  def setup_repository(self):
    """
    Upload the repository referral to the portal-gui and wait the
    response from the repository operator.
    """

    r = self.upload_for_response("demo/repository-request", "entitydb/repositories/%s.xml" % self.parent_handle)
    save("repository.xml", r.read())
    self.myrpki("configure_repository", "repository.xml")

  def poll(self, csv_file):
    """
    Poll for new version of a CSV file, save if changed, return
    boolean indicating whether file has changed.
    """

    try:
      url = self.myrpki_url + csv_file.url
      r = self.opener.open(urllib2.Request(url, None, {
        "If-Modified-Since" : csv_file.last_modified(),
        "User-Agent"        : self.user_agent }))
      timestamp = time.mktime(r.info().getdate("Last-Modified"))
      csv_file.store(timestamp, r.read())
      return True
    except urllib2.HTTPError, e:
      if e.code == 304:                   # 304 == "Not Modified"
        return False
      else:
        sys.stderr.write("Problem polling URL %s\n" % url)
        save_error(e)
        raise

  def poll_loop(self):
    """
    Loop forever, polling for updates.
    """

    while True:
      changed = False
      for csv_file in self.csv_files:
        if self.poll(csv_file):
          changed = True
      if changed:
        self.update()
      time.sleep(self.delay)

  def getopt(self):
    """
    Parse options.
    """
    opts, argv = getopt.getopt(sys.argv[1:], "hi?", ["help"])
    for o, a in opts:
      if o in ("-h", "--help", "-?"):
        print __doc__
        sys.exit(0)
    if argv:
      sys.exit("Unexpected arguments %r" % (argv,))

  def __init__(self):
    self.getopt()
    self.setup_utc()
    self.setup_openssl()
    self.setup_username()
    self.setup_working_directory()
    self.setup_config_file()
    self.setup_csv_files()
    self.myrpki("initialize")
    self.setup_parent()
    self.setup_repository()
    self.update()
    self.update()

    webbrowser.open(self.myrpki_url)

    self.poll_loop()

main()

# Local Variables:
# mode:python
# End:

# vim:sw=2 ts=8 expandtab