diff --git a/lib/backendDataRest.py b/lib/backendDataRest.py new file mode 100644 index 0000000..35f1162 --- /dev/null +++ b/lib/backendDataRest.py @@ -0,0 +1,115 @@ +from common import * +from utils import * + +import requests_fp +import urllib +import urlparse +import json # Hack for Debian Wheezy compatibility; remove this import when Wheezy is phased out + +# This backend requires non-default modules loaded. +# If not using TLS, you can do this on Fedora with: +# yum install python-requests +# If using TLS (experimental), you can do this on Fedora with: +# yum install python-requests pyOpenSSL python-ndg_httpsclient python-pyasn1 + +# TODO: Finish testing the TLS code. Note that Bitcoin Core is probably removing TLS support soon, so TLS support is solely for things like Nginx proxies. + +class backendData(): + validURL = False + + def __init__(self, conf): + + url = urlparse.urlparse(conf) + if url.scheme == 'http' or url.scheme == 'https': + self.validURL = True + self.scheme = url.scheme + self.tls = (url.scheme == 'https') + self.host = url.hostname + self.port = url.port + + # Sessions let us reuse TCP connections, while keeping unique identities on different TCP connections + self.sessions = {} + + if self.tls: + # Init the TLS security settings + try: + requests_fp.init() + except: + print "ERROR: Failed to load PyOpenSSL." + print "Make sure you have the right packages installed." + print "On Fedora, run:" + print "sudo yum install pyOpenSSL python-ndg_httpsclient python-pyasn1" + print "Other distros/OS's may be similar" + import os + os._exit(-1) + + if app['debug']: + print "WARNING: You are using the experimental REST over TLS feature. This is probably broken and should not be used in production." + + if url.params == '': + self.fprs = {} + else: + self.fprs = self._parseFprOptions(url.params) + + if "sha256" in self.fprs: + requests_fp.add_fingerprint(self.host, self.fprs["sha256"]) + + if self.tls and "sha256" not in self.fprs: + if app['debug']: + print "ERROR: REST SHA256 fingerprint missing in plugin-data.conf; REST lookups will fail." + + if "testTlsConfig" in self.fprs: + testResults = requests_fp.test_tls_config() + print "TLS test result:" + print testResults + import os + os._exit(0) + elif app['debug']: + print "ERROR: Unsupported scheme for REST URL:", url.scheme + + def getAllNames(self): + # The REST API doesn't support enumerating the names. + if app['debug']: + print 'ERROR: REST data backend does not support name enumeration; set import.mode=none or switch to a different import.from backend.' + return (True, None) # TODO: Should this be True rather than False? See the data plugin for usage. + + def getName(self, name, sessionId = ""): + + encoded = urllib.quote_plus(name) + + result = self._queryHttpGet(self.scheme + "://" + self.host + ":" + str(self.port) + "/rest/name/" + encoded + ".json", sessionId) + + try: + resultJson = json.loads(result.text) # Hack for Debian Wheezy compatibility; use the following line instead when Wheezy is phased out + # resultJson = result.json() + except ValueError: + raise Exception("Error parsing REST response. Make sure that Namecoin Core is running with -rest option.") + + return (None, resultJson) + + def _queryHttpGet(self, url, sessionId): + + # set up a session if we haven't yet for this identity (Tor users will use multiple identities) + if sessionId not in self.sessions: + if app['debug']: + print 'Creating new REST identity = "' + sessionId + '"' + self.sessions[sessionId] = requests_fp.Session() + + return self.sessions[sessionId].get(url) + + 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/requests_fp.py b/lib/requests_fp.py new file mode 100644 index 0000000..735d6b3 --- /dev/null +++ b/lib/requests_fp.py @@ -0,0 +1,41 @@ +from common import * +from utils import * + +from requests import * + +# Stores desired fingerprints +fp_sha256 = {} + +# PyOpenSSL callback +def verify_fingerprint(connection, x509, errnum, errdepth, ok): + + try: + host = connection.get_servername() + except AttributeError, e: + raise Exception("ERROR: You appear to be on a broken PyOpenSSL version such as 0.14. Please upgrade PyOpenSSL if you wish to use TLS validation. " + str(e)) + + seen_fp = sanitiseFingerprint(x509.digest("sha256")) + + if app['debug']: + print "Checking TLS cert", seen_fp, "for", host + + # Accept a cert if verification is forced off, or if it's a non-primary CA cert (the main cert will still be verified), or if the SHA256 matches + return (host in fp_sha256 and sanitiseFingerprint("NONE") in fp_sha256[host]) or errdepth > 0 or (host in fp_sha256 and seen_fp in fp_sha256[host]) + +# Add a fingerprint to the whitelist +def add_fingerprint(host, fp): + if host not in fp_sha256: + fp_sha256[host] = [] + + fp_sha256[host].append(sanitiseFingerprint(fp)) + +# Returns HTML analysis from SSLLabs. Output this to a file and view with Javascript disabled. +def test_tls_config(): + return get("https://www.ssllabs.com/ssltest/viewMyClient.html").text + +def init(): + # Set ciphers and enable fingerprint verification via PyOpenSSL + packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST = "EDH+aRSA+AES256:EECDH+aRSA+AES256:!SSLv3" + packages.urllib3.contrib.pyopenssl._verify_callback = verify_fingerprint + packages.urllib3.contrib.pyopenssl.inject_into_urllib3() + 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 6b7d752..89f5bbd 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']}, 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