LBRY-Vault/electrum/interface.py
2018-09-06 14:11:35 +02:00

197 lines
6.9 KiB
Python

#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import re
import socket
import ssl
import sys
import threading
import traceback
import aiorpcx
import asyncio
import requests
from .util import PrintError
ca_path = requests.certs.where()
from . import util
from . import x509
from . import pem
from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
class Interface(PrintError):
def __init__(self, server, config_path, connecting):
self.connecting = connecting
self.server = server
self.host, self.port, self.protocol = self.server.split(':')
self.config_path = config_path
self.cert_path = os.path.join(self.config_path, 'certs', self.host)
self.fut = asyncio.get_event_loop().create_task(self.run())
def diagnostic_name(self):
return self.host
async def is_server_ca_signed(self, sslc):
try:
await self.open_session(sslc, do_sleep=False)
except ssl.SSLError as e:
assert e.reason == 'CERTIFICATE_VERIFY_FAILED'
return False
return True
@util.aiosafe
async def run(self):
if self.protocol != 's':
await self.open_session(None, execute_after_connect=lambda: self.connecting.remove(self.server))
return
ca_sslc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
exists = os.path.exists(self.cert_path)
if exists:
with open(self.cert_path, 'r') as f:
contents = f.read()
if contents != '': # if not CA signed
try:
b = pem.dePem(contents, 'CERTIFICATE')
except SyntaxError:
exists = False
else:
x = x509.X509(b)
try:
x.check_date()
except x509.CertificateError:
self.print_error("certificate has expired:", self.cert_path)
os.unlink(self.cert_path)
exists = False
if not exists:
ca_signed = await self.is_server_ca_signed(ca_sslc)
if ca_signed:
with open(self.cert_path, 'w') as f:
# empty file means this is CA signed, not self-signed
f.write('')
else:
await self.save_certificate()
siz = os.stat(self.cert_path).st_size
if siz == 0: # if CA signed
sslc = ca_sslc
else:
sslc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=self.cert_path)
sslc.check_hostname = 0
await self.open_session(sslc, execute_after_connect=lambda: self.connecting.remove(self.server))
async def save_certificate(self):
if not os.path.exists(self.cert_path):
# we may need to retry this a few times, in case the handshake hasn't completed
for _ in range(10):
dercert = await self.get_certificate()
if dercert:
self.print_error("succeeded in getting cert")
with open(self.cert_path, 'w') as f:
cert = ssl.DER_cert_to_PEM_cert(dercert)
# workaround android bug
cert = re.sub("([^\n])-----END CERTIFICATE-----","\\1\n-----END CERTIFICATE-----",cert)
f.write(cert)
# even though close flushes we can't fsync when closed.
# and we must flush before fsyncing, cause flush flushes to OS buffer
# fsync writes to OS buffer to disk
f.flush()
os.fsync(f.fileno())
break
await asyncio.sleep(1)
else:
assert False, "could not get certificate"
async def get_certificate(self):
sslc = ssl.SSLContext()
try:
async with aiorpcx.ClientSession(self.host, self.port, ssl=sslc) as session:
return session.transport._ssl_protocol._sslpipe._sslobj.getpeercert(True)
except ValueError:
return None
async def open_session(self, sslc, do_sleep=True, execute_after_connect=lambda: None):
async with aiorpcx.ClientSession(self.host, self.port, ssl=sslc) as session:
ver = await session.send_request('server.version', [ELECTRUM_VERSION, PROTOCOL_VERSION])
print(ver)
connect_hook_executed = False
while do_sleep:
if not connect_hook_executed:
connect_hook_executed = True
execute_after_connect()
await asyncio.wait_for(session.send_request('server.ping'), 5)
await asyncio.sleep(300)
def has_timed_out(self):
return self.fut.done()
def queue_request(self, method, params, msg_id):
pass
def close(self):
self.fut.cancel()
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:
p = os.path.join(mydir,c)
with open(p, encoding='utf-8') as f:
cert = f.read()
check_cert(c, cert)
if __name__ == "__main__":
test_certificates()