Skip to content
121 changes: 121 additions & 0 deletions lib/backendDataRest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from common import *
from utils import *

import requests
import urllib
import urlparse

# 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.

fp_sha256 = ""

def assert_fingerprint(connection, x509, errnum, errdepth, ok):
# 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 fp_sha256.lower() == "none" or errdepth > 0 or sanitiseFingerprint(x509.digest("sha256")) == sanitiseFingerprint(fp_sha256)

class backendData():
validURL = False

def __init__(self, conf):

global fp_sha256

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:
try:
# Set ciphers and enable fingerprint verification via PyOpenSSL
requests.packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST = "EDH+aRSA+AES256:EECDH+aRSA+AES256:!SSLv3"
requests.packages.urllib3.contrib.pyopenssl._verify_callback = assert_fingerprint
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
except:
if app['debug']:
print "ERROR: Failed to load PyOpenSSL; 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"

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:
fp_sha256 = self.fprs["sha256"]

if self.tls and fp_sha256 == "":
if app['debug']:
print "ERROR: REST SHA256 fingerprint missing in plugin-data.conf; REST lookups will fail."

if "testTlsConfig" in self.fprs:
testResults = self._queryHttpGet("https://www.ssllabs.com/ssltest/viewMyClient.html", "").text
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 = result.json()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line did not work for me. I had to remove the () at the end, since result.json is a member variable and not function. With the change, it worked.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@domob1812 with your change I get the following upon calling getIp4 via RPC:

Traceback (most recent call last):
File "/home/jeremy/Downloads/NMControl/nmcontrol/plugin/pluginRpc.py", line 170, in computeData
result = methodRpc(method, _params)
File "/home/jeremy/Downloads/NMControl/nmcontrol/lib/plugin.py", line 203, in _rpc
return func(_args)
File "/home/jeremy/Downloads/NMControl/nmcontrol/plugin/pluginDns.py", line 130, in getIp4
result = self._getRecordForRPC(domain, 'getIp4')
File "/home/jeremy/Downloads/NMControl/nmcontrol/plugin/pluginDns.py", line 125, in _getRecordForRPC
self._resolve(domain, recType, result)
File "/home/jeremy/Downloads/NMControl/nmcontrol/plugin/pluginDns.py", line 87, in _resolve
handler._resolve(domain, recType, result)
File "/home/jeremy/Downloads/NMControl/nmcontrol/plugin/pluginNamespaceDomain.py", line 53, in _resolve
nameData = app['plugins']['data'].getValueProcessed(name)
File "/home/jeremy/Downloads/NMControl/nmcontrol/plugin/pluginData.py", line 133, in getValueProcessed
data = self.getValue(name)
File "/home/jeremy/Downloads/NMControl/nmcontrol/plugin/pluginData.py", line 121, in getValue
data = self.getData(name)
File "/home/jeremy/Downloads/NMControl/nmcontrol/plugin/pluginData.py", line 107, in getData
if 'expired' in data and data['expired']:
TypeError: argument of type 'instancemethod' is not iterable

Without your change it works fine for me. Not sure why you're getting different results. FWIW the example code on the front page of http://www.python-requests.org/en/latest/ agrees with my results.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also do not know. Maybe json changed from a member variable to a member function with the version of requests? I'm running the one in Debian Wheezy, so already at least two years old.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://stackoverflow.com/questions/6386308/http-requests-and-json-parsing-in-python/17517598#17517598 , Wheezy is indeed on an old version of requests which has json as a variable. I will push a workaround shortly. Thanks for pointing this out.

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.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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit pick: Please add a new line at the end.

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