namecoin.py
1 """ 2 Namecoin queries 3 """ 4 # pylint: disable=too-many-branches,protected-access 5 6 import base64 7 import json 8 import os 9 import socket 10 import sys 11 12 from six.moves import http_client as httplib 13 14 from . import defaults 15 from .addresses import decodeAddress 16 from .bmconfigparser import config 17 from .debug import logger 18 from .tr import _translate # translate 19 20 configSection = "bitmessagesettings" 21 22 23 class RPCError(Exception): 24 """Error thrown when the RPC call returns an error.""" 25 26 error = None 27 28 def __init__(self, data): 29 super(RPCError, self).__init__() 30 self.error = data 31 32 def __str__(self): 33 return "{0}: {1}".format(type(self).__name__, self.error) 34 35 36 class namecoinConnection(object): 37 """This class handles the Namecoin identity integration.""" 38 39 user = None 40 password = None 41 host = None 42 port = None 43 nmctype = None 44 bufsize = 4096 45 queryid = 1 46 con = None 47 48 def __init__(self, options=None): 49 """ 50 Initialise. If options are given, take the connection settings from 51 them instead of loading from the configs. This can be used to test 52 currently entered connection settings in the config dialog without 53 actually changing the values (yet). 54 """ 55 if options is None: 56 self.nmctype = config.get( 57 configSection, "namecoinrpctype") 58 self.host = config.get( 59 configSection, "namecoinrpchost") 60 self.port = int(config.get( 61 configSection, "namecoinrpcport")) 62 self.user = config.get( 63 configSection, "namecoinrpcuser") 64 self.password = config.get( 65 configSection, "namecoinrpcpassword") 66 else: 67 self.nmctype = options["type"] 68 self.host = options["host"] 69 self.port = int(options["port"]) 70 self.user = options["user"] 71 self.password = options["password"] 72 73 assert self.nmctype == "namecoind" or self.nmctype == "nmcontrol" 74 if self.nmctype == "namecoind": 75 self.con = httplib.HTTPConnection(self.host, self.port, timeout=3) 76 77 def query(self, identity): 78 """ 79 Query for the bitmessage address corresponding to the given identity 80 string. If it doesn't contain a slash, id/ is prepended. We return 81 the result as (Error, Address) pair, where the Error is an error 82 message to display or None in case of success. 83 """ 84 slashPos = identity.find("/") 85 if slashPos < 0: 86 display_name = identity 87 identity = "id/" + identity 88 else: 89 display_name = identity.split("/")[1] 90 91 try: 92 if self.nmctype == "namecoind": 93 res = self.callRPC("name_show", [identity]) 94 res = res["value"] 95 elif self.nmctype == "nmcontrol": 96 res = self.callRPC("data", ["getValue", identity]) 97 res = res["reply"] 98 if not res: 99 return (_translate( 100 "MainWindow", "The name %1 was not found." 101 ).arg(identity.decode("utf-8", "ignore")), None) 102 else: 103 assert False 104 except RPCError as exc: 105 logger.exception("Namecoin query RPC exception") 106 if isinstance(exc.error, dict): 107 errmsg = exc.error["message"] 108 else: 109 errmsg = exc.error 110 return (_translate( 111 "MainWindow", "The namecoin query failed (%1)" 112 ).arg(errmsg.decode("utf-8", "ignore")), None) 113 except AssertionError: 114 return (_translate( 115 "MainWindow", "Unknown namecoin interface type: %1" 116 ).arg(self.nmctype.decode("utf-8", "ignore")), None) 117 except Exception: 118 logger.exception("Namecoin query exception") 119 return (_translate( 120 "MainWindow", "The namecoin query failed."), None) 121 122 try: 123 res = json.loads(res) 124 except ValueError: 125 pass 126 else: 127 try: 128 display_name = res["name"] 129 except KeyError: 130 pass 131 res = res.get("bitmessage") 132 133 valid = decodeAddress(res)[0] == "success" 134 return ( 135 None, "%s <%s>" % (display_name, res) 136 ) if valid else ( 137 _translate( 138 "MainWindow", 139 "The name %1 has no associated Bitmessage address." 140 ).arg(identity.decode("utf-8", "ignore")), None) 141 142 def test(self): 143 """ 144 Test the connection settings. This routine tries to query a "getinfo" 145 command, and builds either an error message or a success message with 146 some info from it. 147 """ 148 try: 149 if self.nmctype == "namecoind": 150 try: 151 vers = self.callRPC("getinfo", [])["version"] 152 except RPCError: 153 vers = self.callRPC("getnetworkinfo", [])["version"] 154 155 v3 = vers % 100 156 vers = vers / 100 157 v2 = vers % 100 158 vers = vers / 100 159 v1 = vers 160 if v3 == 0: 161 versStr = "0.%d.%d" % (v1, v2) 162 else: 163 versStr = "0.%d.%d.%d" % (v1, v2, v3) 164 message = ( 165 "success", 166 _translate( 167 "MainWindow", 168 "Success! Namecoind version %1 running.").arg( 169 versStr.decode("utf-8", "ignore"))) 170 171 elif self.nmctype == "nmcontrol": 172 res = self.callRPC("data", ["status"]) 173 prefix = "Plugin data running" 174 if ("reply" in res) and res["reply"][:len(prefix)] == prefix: 175 return ( 176 "success", 177 _translate( 178 "MainWindow", 179 "Success! NMControll is up and running." 180 ) 181 ) 182 183 logger.error("Unexpected nmcontrol reply: %s", res) 184 message = ( 185 "failed", 186 _translate( 187 "MainWindow", 188 "Couldn\'t understand NMControl." 189 ) 190 ) 191 192 else: 193 sys.exit("Unsupported Namecoin type") 194 195 return message 196 197 except Exception: 198 logger.info("Namecoin connection test failure") 199 return ( 200 "failed", 201 _translate( 202 "MainWindow", "The connection to namecoin failed.") 203 ) 204 205 def callRPC(self, method, params): 206 """Helper routine that actually performs an JSON RPC call.""" 207 208 data = {"method": method, "params": params, "id": self.queryid} 209 if self.nmctype == "namecoind": 210 resp = self.queryHTTP(json.dumps(data)) 211 elif self.nmctype == "nmcontrol": 212 resp = self.queryServer(json.dumps(data)) 213 else: 214 assert False 215 val = json.loads(resp) 216 217 if val["id"] != self.queryid: 218 raise Exception("ID mismatch in JSON RPC answer.") 219 220 if self.nmctype == "namecoind": 221 self.queryid = self.queryid + 1 222 223 error = val["error"] 224 if error is None: 225 return val["result"] 226 227 if isinstance(error, bool): 228 raise RPCError(val["result"]) 229 raise RPCError(error) 230 231 def queryHTTP(self, data): 232 """Query the server via HTTP.""" 233 234 result = None 235 236 try: 237 self.con.putrequest("POST", "/") 238 self.con.putheader("Connection", "Keep-Alive") 239 self.con.putheader("User-Agent", "bitmessage") 240 self.con.putheader("Host", self.host) 241 self.con.putheader("Content-Type", "application/json") 242 self.con.putheader("Content-Length", str(len(data))) 243 self.con.putheader("Accept", "application/json") 244 authstr = "%s:%s" % (self.user, self.password) 245 self.con.putheader( 246 "Authorization", "Basic %s" % base64.b64encode(authstr)) 247 self.con.endheaders() 248 self.con.send(data) 249 except: # noqa:E722 250 logger.info("HTTP connection error") 251 return None 252 253 try: 254 resp = self.con.getresponse() 255 result = resp.read() 256 if resp.status != 200: 257 raise Exception( 258 "Namecoin returned status" 259 " %i: %s" % (resp.status, resp.reason)) 260 except: # noqa:E722 261 logger.info("HTTP receive error") 262 return None 263 264 return result 265 266 def queryServer(self, data): 267 """Helper routine sending data to the RPC " 268 "server and returning the result.""" 269 270 try: 271 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 272 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 273 s.settimeout(3) 274 s.connect((self.host, self.port)) 275 s.sendall(data) 276 result = "" 277 278 while True: 279 tmp = s.recv(self.bufsize) 280 if not tmp: 281 break 282 result += tmp 283 284 s.close() 285 286 return result 287 288 except socket.error as exc: 289 raise Exception("Socket error in RPC connection: %s" % exc) 290 291 292 def lookupNamecoinFolder(): 293 """ 294 Look up the namecoin data folder. 295 296 .. todo:: Check whether this works on other platforms as well! 297 """ 298 299 app = "namecoin" 300 from os import environ, path 301 if sys.platform == "darwin": 302 if "HOME" in environ: 303 dataFolder = path.join(os.environ["HOME"], 304 "Library/Application Support/", app) + "/" 305 else: 306 sys.exit( 307 "Could not find home folder, please report this message" 308 " and your OS X version to the BitMessage Github." 309 ) 310 311 elif "win32" in sys.platform or "win64" in sys.platform: 312 dataFolder = path.join(environ["APPDATA"], app) + "\\" 313 else: 314 dataFolder = path.join(environ["HOME"], ".%s" % app) + "/" 315 316 return dataFolder 317 318 319 def ensureNamecoinOptions(): 320 """ 321 Ensure all namecoin options are set, by setting those to default values 322 that aren't there. 323 """ 324 325 if not config.has_option(configSection, "namecoinrpctype"): 326 config.set(configSection, "namecoinrpctype", "namecoind") 327 if not config.has_option(configSection, "namecoinrpchost"): 328 config.set(configSection, "namecoinrpchost", "localhost") 329 330 hasUser = config.has_option(configSection, "namecoinrpcuser") 331 hasPass = config.has_option(configSection, "namecoinrpcpassword") 332 hasPort = config.has_option(configSection, "namecoinrpcport") 333 334 # Try to read user/password from .namecoin configuration file. 335 defaultUser = "" 336 defaultPass = "" 337 nmcFolder = lookupNamecoinFolder() 338 nmcConfig = nmcFolder + "namecoin.conf" 339 try: 340 nmc = open(nmcConfig, "r") 341 342 while True: 343 line = nmc.readline() 344 if line == "": 345 break 346 parts = line.split("=") 347 if len(parts) == 2: 348 key = parts[0] 349 val = parts[1].rstrip() 350 351 if key == "rpcuser" and not hasUser: 352 defaultUser = val 353 if key == "rpcpassword" and not hasPass: 354 defaultPass = val 355 if key == "rpcport": 356 defaults.namecoinDefaultRpcPort = val 357 358 nmc.close() 359 except IOError: 360 logger.warning( 361 "%s unreadable or missing, Namecoin support deactivated", 362 nmcConfig) 363 except Exception: 364 logger.warning("Error processing namecoin.conf", exc_info=True) 365 366 # If still nothing found, set empty at least. 367 if not hasUser: 368 config.set(configSection, "namecoinrpcuser", defaultUser) 369 if not hasPass: 370 config.set(configSection, "namecoinrpcpassword", defaultPass) 371 372 # Set default port now, possibly to found value. 373 if not hasPort: 374 config.set(configSection, "namecoinrpcport", defaults.namecoinDefaultRpcPort)