Skip to content
115 changes: 115 additions & 0 deletions lib/backendDataRest.py
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions lib/requests_fp.py
Original file line number Diff line number Diff line change
@@ -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()

13 changes: 13 additions & 0 deletions lib/utils.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion plugin/pluginData.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', '<none|all|ondemand>']},
{'update.from': ['Update data from', 'namecoin', '<namecoin|url|file>']},
{'update.from': ['Update data from', 'namecoin', '<namecoin|rest|file>']},
{'update.freq': ['Update data if older than', '30m', '<number>[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', '<none|all>']},
{'export.to': ['Export data to', 'file']},
Expand Down
15 changes: 3 additions & 12 deletions plugin/pluginDns.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from common import *
from utils import *
import plugin
#import DNS
#import json, base64, types, random, traceback
Expand Down Expand Up @@ -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']:
Expand Down Expand Up @@ -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