mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
Previously network.py had its own idea of request IDs, and each interface had its own which was sent on the wire. The interface would jump through hoops to translate one to the other. This unifies them so that a message ID is passed when queueing a request, in addition to the method and params. network.py is now solely responsible for message ID management. Apart from being simpler and clearer, this also should be faster as there is much less data structure manipulation and rebuilding happening.
361 lines
12 KiB
Python
361 lines
12 KiB
Python
#!/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/>.
|
|
|
|
|
|
import os
|
|
import re
|
|
import socket
|
|
import ssl
|
|
import sys
|
|
import threading
|
|
import time
|
|
import traceback
|
|
|
|
import requests
|
|
ca_path = requests.certs.where()
|
|
|
|
import util
|
|
import x509
|
|
import pem
|
|
|
|
|
|
def Connection(server, queue, config_path):
|
|
"""Makes asynchronous connections to a remote remote electrum server.
|
|
Returns the running thread that is making the connection.
|
|
|
|
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.
|
|
"""
|
|
host, port, protocol = server.split(':')
|
|
if not protocol in 'st':
|
|
raise Exception('Unknown protocol: %s' % protocol)
|
|
c = TcpConnection(server, queue, config_path)
|
|
c.start()
|
|
return c
|
|
|
|
class TcpConnection(threading.Thread, util.PrintError):
|
|
|
|
def __init__(self, server, queue, config_path):
|
|
threading.Thread.__init__(self)
|
|
self.daemon = True
|
|
self.config_path = config_path
|
|
self.queue = queue
|
|
self.server = server
|
|
self.host, self.port, self.protocol = self.server.split(':')
|
|
self.host = str(self.host)
|
|
self.port = int(self.port)
|
|
self.use_ssl = (self.protocol == 's')
|
|
|
|
def diagnostic_name(self):
|
|
return self.host
|
|
|
|
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
|
|
else:
|
|
# 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
|
|
|
|
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
|
|
except BaseException as e:
|
|
continue
|
|
else:
|
|
self.print_error("failed to connect", str(e))
|
|
|
|
def get_socket(self):
|
|
if self.use_ssl:
|
|
cert_path = os.path.join(self.config_path, 'certs', self.host)
|
|
if not os.path.exists(cert_path):
|
|
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
|
|
if s and self.check_host_name(s.getpeercert(), self.host):
|
|
self.print_error("SSL certificate signed by CA")
|
|
return s
|
|
|
|
# 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)
|
|
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:
|
|
f.write(cert)
|
|
else:
|
|
is_new = False
|
|
|
|
s = self.get_simple_socket()
|
|
if s is None:
|
|
return
|
|
|
|
if self.use_ssl:
|
|
try:
|
|
s = ssl.wrap_socket(s,
|
|
ssl_version=ssl.PROTOCOL_TLSv1,
|
|
cert_reqs=ssl.CERT_REQUIRED,
|
|
ca_certs= (temporary_path if is_new else cert_path),
|
|
do_handshake_on_connect=True)
|
|
except ssl.SSLError, e:
|
|
self.print_error("SSL error:", e)
|
|
if e.errno != 1:
|
|
return
|
|
if is_new:
|
|
rej = cert_path + '.rej'
|
|
if os.path.exists(rej):
|
|
os.unlink(rej)
|
|
os.rename(temporary_path, rej)
|
|
else:
|
|
with open(cert_path) as f:
|
|
cert = f.read()
|
|
try:
|
|
b = pem.dePem(cert, 'CERTIFICATE')
|
|
x = x509.X509(b)
|
|
except:
|
|
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)
|
|
os.unlink(cert_path)
|
|
return
|
|
self.print_error("wrong certificate")
|
|
return
|
|
except BaseException, e:
|
|
self.print_error(e)
|
|
if e.errno == 104:
|
|
return
|
|
traceback.print_exc(file=sys.stderr)
|
|
return
|
|
|
|
if is_new:
|
|
self.print_error("saving certificate")
|
|
os.rename(temporary_path, cert_path)
|
|
|
|
return s
|
|
|
|
def run(self):
|
|
socket = self.get_socket()
|
|
if socket:
|
|
self.print_error("connected")
|
|
self.queue.put((self.server, socket))
|
|
|
|
class Interface(util.PrintError):
|
|
"""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.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 diagnostic_name(self):
|
|
return self.host
|
|
|
|
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, *args): # method, params, _id
|
|
'''Queue a request, later to be send with send_requests when the
|
|
socket is available for writing.
|
|
'''
|
|
self.request_time = time.time()
|
|
self.unsent_requests.append(args)
|
|
|
|
def send_requests(self):
|
|
'''Sends all queued requests. Returns False on failure.'''
|
|
make_dict = lambda (m, p, i): {'method': m, 'params': p, 'id': i}
|
|
wire_requests = map(make_dict, self.unsent_requests)
|
|
try:
|
|
self.pipe.send_all(wire_requests)
|
|
except socket.error, e:
|
|
self.print_error("socket error:", e)
|
|
return False
|
|
for request in self.unsent_requests:
|
|
if self.debug:
|
|
self.print_error("-->", request)
|
|
self.unanswered_requests[request[2]] = 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
|
|
|
|
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
|
|
(request, response) pairs. Notifications are singleton
|
|
unsolicited responses presumably as a result of prior
|
|
subscriptions, so request is None and there is no 'id' member.
|
|
Otherwise it is a response, which has an 'id' member and a
|
|
corresponding request. If the connection was closed remotely
|
|
or the remote server is misbehaving, a (None, None) will appear.
|
|
'''
|
|
responses = []
|
|
while True:
|
|
try:
|
|
response = self.pipe.get()
|
|
except util.timeout:
|
|
break
|
|
if response is None:
|
|
responses.append((None, None))
|
|
self.closed_remotely = True
|
|
self.print_error("connection closed remotely")
|
|
break
|
|
if self.debug:
|
|
self.print_error("<--", response)
|
|
wire_id = response.get('id', None)
|
|
if wire_id is None: # Notification
|
|
responses.append((None, response))
|
|
else:
|
|
request = self.unanswered_requests.pop(wire_id, None)
|
|
if request:
|
|
responses.append((request, response))
|
|
else:
|
|
self.print_error("unknown wire ID", wire_id)
|
|
responses.append(None, None) # Signal
|
|
break
|
|
|
|
return responses
|
|
|
|
|
|
def check_cert(host, cert):
|
|
try:
|
|
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)
|
|
|
|
|
|
# Used by tests
|
|
def _match_hostname(name, val):
|
|
if val == name:
|
|
return True
|
|
|
|
return val.startswith('*.') and name.endswith(val[1:])
|
|
|
|
def test_certificates():
|
|
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()
|