electrum-bitcoinprivate/lib/interface.py

367 lines
12 KiB
Python
Raw Normal View History

#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2015-06-02 08:03:33 -07:00
import os
import re
import socket
import ssl
2015-06-02 08:03:33 -07:00
import sys
import threading
import time
import traceback
2012-02-03 06:45:41 -08:00
import requests
ca_path = requests.certs.where()
import util
import x509
2015-08-07 02:58:59 -07:00
import pem
2015-06-02 08:03:33 -07:00
def Connection(server, queue, config_path):
"""Makes asynchronous connections to a remote remote electrum server.
Returns the running thread that is making the connection.
2015-06-02 08:03:33 -07:00
Once the thread has connected, it finishes, placing a tuple on the
queue of the form (server, socket), where socket is None if
connection failed.
"""
2014-07-29 00:28:27 -07:00
host, port, protocol = server.split(':')
2015-06-02 08:03:33 -07:00
if not protocol in 'st':
raise Exception('Unknown protocol: %s' % protocol)
c = TcpConnection(server, queue, config_path)
c.start()
return c
2015-06-02 08:03:33 -07:00
class TcpConnection(threading.Thread):
2015-06-02 08:03:33 -07:00
def __init__(self, server, queue, config_path):
threading.Thread.__init__(self)
self.daemon = True
2015-06-02 08:03:33 -07:00
self.config_path = config_path
self.queue = queue
2013-10-06 03:28:45 -07:00
self.server = server
2014-07-29 00:28:27 -07:00
self.host, self.port, self.protocol = self.server.split(':')
self.host = str(self.host)
2014-07-29 00:28:27 -07:00
self.port = int(self.port)
self.use_ssl = (self.protocol == 's')
def print_error(self, *msg):
2015-06-02 08:03:33 -07:00
util.print_error("[%s]" % self.host, *msg)
def check_host_name(self, peercert, name):
"""Simple certificate/host name checker. Returns True if the
certificate matches, False otherwise. Does not support
wildcards."""
# Check that the peer has supplied a certificate.
# None/{} is not acceptable.
if not peercert:
return False
if peercert.has_key("subjectAltName"):
for typ, val in peercert["subjectAltName"]:
if typ == "DNS" and val == name:
return True
2012-03-17 15:32:36 -07:00
else:
2015-06-02 08:03:33 -07:00
# Only check the subject DN if there is no subject alternative
# name.
cn = None
for attr, val in peercert["subject"]:
# Use most-specific (last) commonName attribute.
if attr == "commonName":
cn = val
if cn is not None:
return cn == name
return False
2012-03-19 07:57:47 -07:00
def get_simple_socket(self):
try:
l = socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM)
except socket.gaierror:
self.print_error("cannot resolve hostname")
return
for res in l:
try:
s = socket.socket(res[0], socket.SOCK_STREAM)
s.connect(res[4])
s.settimeout(2)
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
return s
2015-02-28 10:45:10 -08:00
except BaseException as e:
continue
else:
self.print_error("failed to connect", str(e))
2013-09-30 05:01:49 -07:00
def get_socket(self):
2013-09-30 05:01:49 -07:00
if self.use_ssl:
2015-06-02 08:03:33 -07:00
cert_path = os.path.join(self.config_path, 'certs', self.host)
2013-09-30 05:01:49 -07:00
if not os.path.exists(cert_path):
2013-10-01 18:24:14 -07:00
is_new = True
s = self.get_simple_socket()
if s is None:
return
# try with CA first
try:
s = ssl.wrap_socket(s, ssl_version=ssl.PROTOCOL_TLSv1, cert_reqs=ssl.CERT_REQUIRED, ca_certs=ca_path, do_handshake_on_connect=True)
except ssl.SSLError, e:
s = None
2015-06-02 08:03:33 -07:00
if s and self.check_host_name(s.getpeercert(), self.host):
self.print_error("SSL certificate signed by CA")
return s
2013-11-13 11:35:35 -08:00
# get server certificate.
# Do not use ssl.get_server_certificate because it does not work with proxy
s = self.get_simple_socket()
if s is None:
return
try:
s = ssl.wrap_socket(s, ssl_version=ssl.PROTOCOL_TLSv1, cert_reqs=ssl.CERT_NONE, ca_certs=None)
except ssl.SSLError, e:
self.print_error("SSL error retrieving SSL certificate:", e)
2013-09-30 05:01:49 -07:00
return
dercert = s.getpeercert(True)
s.close()
cert = ssl.DER_cert_to_PEM_cert(dercert)
# workaround android bug
cert = re.sub("([^\n])-----END CERTIFICATE-----","\\1\n-----END CERTIFICATE-----",cert)
temporary_path = cert_path + '.temp'
with open(temporary_path,"w") as f:
2013-09-30 05:01:49 -07:00
f.write(cert)
2013-10-01 18:24:14 -07:00
else:
is_new = False
2013-09-30 05:01:49 -07:00
s = self.get_simple_socket()
if s is None:
return
2013-09-30 05:01:49 -07:00
2012-10-17 02:35:24 -07:00
if self.use_ssl:
2013-09-30 05:01:49 -07:00
try:
s = ssl.wrap_socket(s,
ssl_version=ssl.PROTOCOL_TLSv1,
2013-09-30 05:01:49 -07:00
cert_reqs=ssl.CERT_REQUIRED,
ca_certs= (temporary_path if is_new else cert_path),
2013-09-30 05:01:49 -07:00
do_handshake_on_connect=True)
except ssl.SSLError, e:
self.print_error("SSL error:", e)
2013-10-02 01:43:02 -07:00
if e.errno != 1:
return
2013-10-01 18:33:45 -07:00
if is_new:
2013-11-11 07:59:36 -08:00
rej = cert_path + '.rej'
if os.path.exists(rej):
os.unlink(rej)
os.rename(temporary_path, rej)
2013-10-02 01:36:29 -07:00
else:
with open(cert_path) as f:
cert = f.read()
try:
2015-08-07 02:58:59 -07:00
b = pem.dePem(cert, 'CERTIFICATE')
x = x509.X509(b)
except:
2014-07-29 00:28:27 -07:00
traceback.print_exc(file=sys.stderr)
self.print_error("wrong certificate")
return
try:
x.check_date()
except:
self.print_error("certificate has expired:", cert_path)
2013-10-02 01:36:29 -07:00
os.unlink(cert_path)
return
self.print_error("wrong certificate")
return
except BaseException, e:
self.print_error(e)
if e.errno == 104:
return
2014-07-29 00:28:27 -07:00
traceback.print_exc(file=sys.stderr)
2013-09-30 05:01:49 -07:00
return
if is_new:
self.print_error("saving certificate")
os.rename(temporary_path, cert_path)
2014-08-22 01:33:13 -07:00
return s
2015-06-02 08:03:33 -07:00
def run(self):
socket = self.get_socket()
if socket:
self.print_error("connected")
self.queue.put((self.server, socket))
class Interface:
"""The Interface class handles a socket connected to a single remote
electrum server. It's exposed API is:
- Member functions close(), fileno(), get_responses(), has_timed_out(),
ping_required(), queue_request(), send_requests()
- Member variable server.
"""
def __init__(self, server, socket):
self.server = server
self.host, _, _ = server.split(':')
self.socket = socket
self.pipe = util.SocketPipe(socket)
self.pipe.set_timeout(0.0) # Don't wait for data
# Dump network messages. Set at runtime from the console.
self.debug = False
self.message_id = 0
self.unsent_requests = []
self.unanswered_requests = {}
# Set last ping to zero to ensure immediate ping
self.last_request = time.time()
self.last_ping = 0
self.closed_remotely = False
def print_error(self, *msg):
util.print_error("[%s]" % self.host, *msg)
def fileno(self):
# Needed for select
return self.socket.fileno()
def close(self):
if not self.closed_remotely:
self.socket.shutdown(socket.SHUT_RDWR)
self.socket.close()
def queue_request(self, request):
'''Queue a request.'''
self.request_time = time.time()
2015-06-02 08:03:33 -07:00
self.unsent_requests.append(request)
def send_requests(self):
2015-06-02 08:03:33 -07:00
'''Sends all queued requests. Returns False on failure.'''
def copy_request(orig):
# Replace ID after making copy - mustn't change caller's copy
request = orig.copy()
request['id'] = self.message_id
2012-03-17 08:02:28 -07:00
self.message_id += 1
2015-06-02 08:03:33 -07:00
if self.debug:
self.print_error("-->", request, orig.get('id'))
return request
requests_as_sent = map(copy_request, self.unsent_requests)
try:
self.pipe.send_all(requests_as_sent)
except socket.error, e:
self.print_error("socket error:", e)
return False
# unanswered_requests stores the original unmodified user
# request, keyed by wire ID
for n, request in enumerate(self.unsent_requests):
self.unanswered_requests[requests_as_sent[n]['id']] = request
self.unsent_requests = []
return True
def ping_required(self):
'''Maintains time since last ping. Returns True if a ping should
be sent.
'''
now = time.time()
if now - self.last_ping > 60:
self.last_ping = now
return True
return False
2015-06-02 08:03:33 -07:00
def has_timed_out(self):
'''Returns True if the interface has timed out.'''
if (self.unanswered_requests and time.time() - self.request_time > 10
and self.pipe.idle_time() > 10):
self.print_error("timeout", len(self.unanswered_requests))
return True
return False
def get_responses(self):
'''Call if there is data available on the socket. Returns a list of
notifications and a list of responses. The notifications are
singleton unsolicited responses presumably as a result of
prior subscriptions. The responses are (request, response)
pairs. If the connection was closed remotely or the remote
server is misbehaving, the last notification will be None.
'''
notifications, responses = [], []
while True:
try:
response = self.pipe.get()
except util.timeout:
2015-06-02 08:03:33 -07:00
break
if response is None:
2015-06-02 08:03:33 -07:00
notifications.append(None)
self.closed_remotely = True
self.print_error("connection closed remotely")
2015-06-02 08:03:33 -07:00
break
if self.debug:
self.print_error("<--", response)
wire_id = response.pop('id', None)
if wire_id is None:
notifications.append(response)
elif wire_id in self.unanswered_requests:
request = self.unanswered_requests.pop(wire_id)
responses.append((request, response))
else:
2015-06-02 08:03:33 -07:00
notifications.append(None)
self.print_error("unknown wire ID", wire_id)
break
2015-06-02 08:03:33 -07:00
return notifications, responses
2013-09-30 05:01:49 -07:00
2014-07-27 15:13:40 -07:00
def check_cert(host, cert):
try:
2015-08-07 02:58:59 -07:00
b = pem.dePem(cert, 'CERTIFICATE')
x = x509.X509(b)
except:
traceback.print_exc(file=sys.stdout)
return
try:
x.check_date()
expired = False
except:
expired = True
m = "host: %s\n"%host
m += "has_expired: %s\n"% expired
util.print_msg(m)
2015-06-02 08:03:33 -07:00
# Used by tests
def _match_hostname(name, val):
if val == name:
return True
return val.startswith('*.') and name.endswith(val[1:])
def test_certificates():
2015-06-02 08:03:33 -07:00
from simple_config import SimpleConfig
config = SimpleConfig()
mydir = os.path.join(config.path, "certs")
certs = os.listdir(mydir)
for c in certs:
print c
p = os.path.join(mydir,c)
with open(p) as f:
cert = f.read()
check_cert(c, cert)
if __name__ == "__main__":
test_certificates()