From cb91319a8116f1583451099ede7dabf32e932acb Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Wed, 10 Dec 2014 20:32:11 +0100 Subject: [PATCH 1/2] Implement REST API as backend for NMControl. --- lib/backendDataRest.py | 48 ++++++++++++++++++++++++++++++++++++++++++ plugin/pluginData.py | 3 ++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 lib/backendDataRest.py diff --git a/lib/backendDataRest.py b/lib/backendDataRest.py new file mode 100644 index 0000000..ab20485 --- /dev/null +++ b/lib/backendDataRest.py @@ -0,0 +1,48 @@ +from common import * + +import httplib +import json +import urllib +import urlparse + +class backendData(): + validURL = False + + def __init__(self, conf): + url = urlparse.urlparse(conf) + if url.scheme == 'http': + self.validURL = True + self.host = url.hostname + self.port = url.port + elif app['debug']: + print "Unsupported scheme for REST URL:", url.scheme + + def getAllNames(self): + # The REST API doesn't support enumerating the names. + return False + + def getName(self, name): + if not self.validURL: + return "invalid REST URL", None + + encoded = urllib.quote_plus(name) + data = self._queryHttpGet("/rest/name/" + encoded + ".json") + + if data is None: + return "query failed", None + return None, json.loads(data) + + def _queryHttpGet(self, path): + assert self.validURL + conn = httplib.HTTPConnection(self.host, self.port) + conn.request('GET', path) + + res = conn.getresponse() + if res.status != 200: + if app['debug']: + print "REST returned error code:", res.status + print res.read() + return None + + return res.read() + diff --git a/plugin/pluginData.py b/plugin/pluginData.py index 6b7d752..4a5635b 100644 --- a/plugin/pluginData.py +++ b/plugin/pluginData.py @@ -17,10 +17,11 @@ class pluginData(plugin.PluginThread): {'import.namecoin': ['Path of namecoin.conf', platformDep.getNamecoinDir() + os.sep + 'namecoin.conf']}, {'update.mode': ['Update mode', 'ondemand', '']}, - {'update.from': ['Update data from', 'namecoin', '']}, + {'update.from': ['Update data from', 'namecoin', '']}, {'update.freq': ['Update data if older than', '30m', '[h|m|s]']}, {'update.file': ['Update data from file ', 'data' + os.sep + 'namecoin.dat']}, {'update.namecoin': ['Path of namecoin.conf', platformDep.getNamecoinDir() + os.sep + 'namecoin.conf']}, + {'update.rest': ['REST API to query', 'http://localhost:8336']}, {'export.mode': ['Export mode', 'none', '']}, {'export.to': ['Export data to', 'file']}, From 276c26c59500687a0535588b145f5912c72b818f Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Thu, 11 Dec 2014 21:58:35 +0100 Subject: [PATCH 2/2] Support REST over TLS. --- lib/backendDataRest.py | 92 +++++++++++++++++++++++++++++++++++++++++- lib/utils.py | 13 ++++++ plugin/pluginData.py | 2 +- plugin/pluginDns.py | 15 ++----- 4 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 lib/utils.py diff --git a/lib/backendDataRest.py b/lib/backendDataRest.py index ab20485..baaa68c 100644 --- a/lib/backendDataRest.py +++ b/lib/backendDataRest.py @@ -1,19 +1,84 @@ from common import * +from utils import * +import hashlib import httplib import json +import socket +import ssl import urllib import urlparse +class MyHTTPSConnection(httplib.HTTPConnection): + """ + Make a HTTPS connection, but support checking the + server's certificate fingerprint. + """ + + def __init__(self, host, port): + httplib.HTTPConnection.__init__(self, host, port) + + def connect(self): + sock = socket.create_connection((self.host, self.port)) + self.sock = ssl.wrap_socket(sock) + + def verifyCert(self, fprs): + """ + Check whether the server's certificate fingerprint + matches the given one. 'fprs' should be a dict with + keys corresponding to digest methods and values being + the digest value. + """ + + hasher = None + fpr = None + + if 'sha256' in fprs: + hasher = hashlib.sha256() + fpr = fprs['sha256'] + elif 'sha1' in fprs: + hasher = hashlib.sha1() + fpr = fprs['sha1'] + + # Be strict here. If this routine is called, it means that at least + # some parameters were given in the REST URI. If we can't verify the + # fingerprint because the parameters were invalid, fail to make sure + # that the user does not expect security but doesn't get it due + # to an operational mistake. + if hasher is None: + if app['debug']: + print "no recognised fingerprint given, failing check" + return False + assert fpr is not None + + cert = self.sock.getpeercert(True) + hasher.update(cert) + digest = hasher.hexdigest() + + if sanitiseFingerprint(digest) != sanitiseFingerprint(fpr): + if app['debug']: + print "Fingerprint mismatch:" + print " expected:", fpr + print " got:", digest + return False + + return True + class backendData(): validURL = False def __init__(self, conf): url = urlparse.urlparse(conf) - if url.scheme == 'http': + if url.scheme == 'http' or url.scheme == 'https': self.validURL = True + self.tls = (url.scheme == 'https') self.host = url.hostname self.port = url.port + + if url.params == '': + self.fprs = None + else: + self.fprs = self._parseFprOptions(url.params) elif app['debug']: print "Unsupported scheme for REST URL:", url.scheme @@ -34,9 +99,16 @@ def getName(self, name): def _queryHttpGet(self, path): assert self.validURL - conn = httplib.HTTPConnection(self.host, self.port) + if self.tls: + conn = MyHTTPSConnection(self.host, self.port) + else: + conn = httplib.HTTPConnection(self.host, self.port) conn.request('GET', path) + if self.tls and (self.fprs is not None): + if not conn.verifyCert(self.fprs): + return "TLS fingerprint wrong", None + res = conn.getresponse() if res.status != 200: if app['debug']: @@ -46,3 +118,19 @@ def _queryHttpGet(self, path): return res.read() + def _parseFprOptions(self, s): + """ + Parse the REST URI params string that includes (optionally) + the TLS certificate fingerprints. + """ + + pieces = s.split(',') + + res = {} + for p in pieces: + parts = p.split('=', 1) + assert len (parts) <= 2 + if len (parts) == 2: + res[parts[0]] = parts[1] + + return res diff --git a/lib/utils.py b/lib/utils.py new file mode 100644 index 0000000..ee8f6fe --- /dev/null +++ b/lib/utils.py @@ -0,0 +1,13 @@ +def sanitiseFingerprint(fpr): + """ + Sanitise a fingerprint (of a TLS certificate, for instance) for + comparison. This removes colons, spaces and makes the string + upper case. + """ + + #fpr = fpr.translate (None, ': ') + fpr = fpr.replace (":", "") + fpr = fpr.replace (" ", "") + fpr = fpr.upper () + + return fpr diff --git a/plugin/pluginData.py b/plugin/pluginData.py index 4a5635b..89f5bbd 100644 --- a/plugin/pluginData.py +++ b/plugin/pluginData.py @@ -21,7 +21,7 @@ class pluginData(plugin.PluginThread): {'update.freq': ['Update data if older than', '30m', '[h|m|s]']}, {'update.file': ['Update data from file ', 'data' + os.sep + 'namecoin.dat']}, {'update.namecoin': ['Path of namecoin.conf', platformDep.getNamecoinDir() + os.sep + 'namecoin.conf']}, - {'update.rest': ['REST API to query', 'http://localhost:8336']}, + {'update.rest': ['REST API to query', 'http://localhost:8336/']}, {'export.mode': ['Export mode', 'none', '']}, {'export.to': ['Export data to', 'file']}, diff --git a/plugin/pluginDns.py b/plugin/pluginDns.py index 814a5cf..b687379 100644 --- a/plugin/pluginDns.py +++ b/plugin/pluginDns.py @@ -1,4 +1,5 @@ from common import * +from utils import * import plugin #import DNS #import json, base64, types, random, traceback @@ -184,9 +185,9 @@ def verifyFingerprint (self, domain, fpr): "is not a list" return False - fpr = self._sanitiseFingerprint (fpr) + fpr = sanitiseFingerprint (fpr) for a in allowable: - if self._sanitiseFingerprint (a) == fpr: + if sanitiseFingerprint (a) == fpr: return True if app['debug']: @@ -289,13 +290,3 @@ def _getSubDomainTlsFingerprint(self,domain,protocol,port): return tls except: continue - - # Sanitise a fingerprint for comparison. This makes it - # all upper-case and removes colons and spaces. - def _sanitiseFingerprint (self, fpr): - #fpr = fpr.translate (None, ': ') - fpr = fpr.replace (":", "") - fpr = fpr.replace (" ", "") - fpr = fpr.upper () - - return fpr