openmedialibrary/oml/nodes.py

522 lines
17 KiB
Python
Raw Normal View History

2014-05-04 17:26:43 +00:00
# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
2014-09-02 22:32:44 +00:00
from queue import Queue
2014-05-04 17:26:43 +00:00
from threading import Thread
import json
2014-05-18 03:01:24 +00:00
import socket
2014-10-31 14:47:54 +00:00
from io import BytesIO
2014-05-18 23:24:04 +00:00
import gzip
2014-09-02 22:32:44 +00:00
import urllib.request, urllib.error, urllib.parse
2014-05-04 17:26:43 +00:00
from datetime import datetime
import os
2014-05-19 15:00:33 +00:00
import time
2014-05-04 17:26:43 +00:00
import ox
import ed25519
2014-05-18 23:24:04 +00:00
from tornado.ioloop import PeriodicCallback
2014-05-04 17:26:43 +00:00
import settings
import user.models
from changelog import Changelog
import directory
from websocket import trigger_event
2014-05-12 12:57:47 +00:00
from localnodes import LocalNodes
from tor_request import get_opener
import state
2014-08-09 16:14:14 +00:00
import db
2014-05-04 17:26:43 +00:00
2014-05-17 14:26:59 +00:00
import logging
2015-11-29 14:56:38 +00:00
logger = logging.getLogger(__name__)
2014-05-17 14:26:59 +00:00
2014-05-04 17:26:43 +00:00
ENCODING='base64'
2014-05-19 15:00:33 +00:00
class Node(Thread):
_running = True
2015-12-01 09:39:47 +00:00
_pulling = False
2014-09-02 23:09:42 +00:00
host = None
2015-12-01 08:59:52 +00:00
local = None
2015-12-02 21:05:23 +00:00
_online = None
2014-05-04 17:26:43 +00:00
download_speed = 0
2014-05-18 23:50:05 +00:00
TIMEOUT = 5
2014-05-04 17:26:43 +00:00
2014-05-12 12:57:47 +00:00
def __init__(self, nodes, user):
self._nodes = nodes
2014-05-04 17:26:43 +00:00
self.user_id = user.id
self._opener = get_opener(self.user_id)
2014-05-19 15:00:33 +00:00
self._q = Queue()
Thread.__init__(self)
self.daemon = True
self.start()
2015-12-01 08:59:52 +00:00
self._pull = PeriodicCallback(self.pull, 60000)
self._pull.start()
2014-05-19 15:00:33 +00:00
self.ping()
def run(self):
2014-08-09 16:14:14 +00:00
while self._running:
action = self._q.get()
if not self._running:
break
2015-12-01 08:59:52 +00:00
if action == 'go_online':
if not self.online:
self._go_online()
elif action == 'ping':
self.online = self.can_connect()
elif action == 'pull':
2015-12-01 09:39:47 +00:00
self._pulling = True
2014-08-09 16:14:14 +00:00
self.online = self.can_connect()
2015-12-01 08:59:52 +00:00
self.pullChanges()
2015-12-01 09:39:47 +00:00
self._pulling = False
2015-12-01 08:59:52 +00:00
else:
logger.debug('unknown action %s', action)
2014-05-19 15:00:33 +00:00
def join(self):
self._running = False
2015-12-01 08:59:52 +00:00
self._q.put('')
2014-08-09 18:32:41 +00:00
#return Thread.join(self)
2014-05-19 15:00:33 +00:00
2015-12-01 08:59:52 +00:00
def pull(self):
2015-12-02 21:05:23 +00:00
if state.online and not self._pulling:
2015-12-01 09:39:47 +00:00
self._q.put('pull')
2015-12-01 08:59:52 +00:00
2014-05-19 15:00:33 +00:00
def ping(self):
2015-12-02 21:05:23 +00:00
if state.online:
self._q.put('ping')
2014-05-19 15:00:33 +00:00
def go_online(self):
self._q.put('go_online')
2014-05-04 17:26:43 +00:00
@property
def url(self):
2015-11-26 11:06:01 +00:00
url = None
if self.local:
if ':' in self.local:
url = 'https://[%s]:%s' % (self.local, self.port)
2014-05-12 12:57:47 +00:00
else:
url = 'https://%s:%s' % (self.local, self.port)
2015-11-26 11:06:01 +00:00
elif len(self.user_id) == 16:
url = 'https://%s.onion:9851' % self.user_id
2014-05-04 17:26:43 +00:00
return url
2015-12-02 21:05:23 +00:00
@property
def online(self):
return self._online
@online.setter
def online(self, online):
if self._online != online:
self._online = online
self.trigger_status()
else:
self._online = online
2014-05-13 10:36:02 +00:00
def resolve(self):
2015-12-01 10:51:58 +00:00
#logger.debug('resolve node %s', self.user_id)
r = self.get_local()
2014-05-04 17:26:43 +00:00
if r:
self.local = r['host']
2014-05-04 17:26:43 +00:00
if 'port' in r:
self.port = r['port']
else:
self.local = None
2014-05-04 17:26:43 +00:00
self.port = 9851
2015-11-26 11:06:01 +00:00
if len(self.user_id) == 43:
self.migrate_id()
def migrate_id(self):
key = self.user_id.encode()
vk = ed25519.VerifyingKey(key, encoding=ENCODING)
try:
r = directory.get(vk)
except:
logger.debug('directory failed', exc_info=1)
r = None
if r and 'id' in r and len(r['id']) == 16:
u = self.user
self.user_id = r['id']
2015-11-26 11:08:22 +00:00
u.migrate_id(self.user_id)
2015-11-26 12:40:39 +00:00
self._opener = get_opener(self.user_id)
2014-05-04 17:26:43 +00:00
2014-05-12 12:57:47 +00:00
def get_local(self):
if self._nodes and self._nodes._local:
return self._nodes._local.get(self.user_id)
2014-05-12 12:57:47 +00:00
return None
2014-05-04 17:26:43 +00:00
def request(self, action, *args):
2015-12-02 15:30:37 +00:00
logger.debug('request[%s] %s%s', self.user_id, action, args)
self.resolve()
2014-05-13 10:36:02 +00:00
url = self.url
2015-12-01 08:59:52 +00:00
if not url:
2014-05-17 14:26:59 +00:00
logger.debug('unable to find host %s', self.user_id)
2014-05-13 10:36:02 +00:00
self.online = False
2014-05-04 17:26:43 +00:00
return None
2015-12-02 15:30:37 +00:00
#logger.debug('url=%s', url)
2014-09-09 10:08:04 +00:00
content = json.dumps([action, args]).encode()
#sig = settings.sk.sign(content, encoding=ENCODING).decode()
2014-05-04 17:26:43 +00:00
headers = {
'User-Agent': settings.USER_AGENT,
'X-Node-Protocol': settings.NODE_PROTOCOL,
2014-05-04 17:26:43 +00:00
'Accept': 'text/plain',
'Accept-Encoding': 'gzip',
'Content-Type': 'application/json',
}
self._opener.addheaders = list(zip(headers.keys(), headers.values()))
2015-12-02 15:30:37 +00:00
#logger.debug('headers: %s', self._opener.addheaders)
2014-05-14 09:57:11 +00:00
try:
2014-09-02 23:09:42 +00:00
self._opener.timeout = self.TIMEOUT
r = self._opener.open(url, data=content)
2014-09-02 22:32:44 +00:00
except urllib.error.HTTPError as e:
2014-05-14 09:57:11 +00:00
if e.code == 403:
2014-05-17 14:26:59 +00:00
logger.debug('REMOTE ENDED PEERING')
2014-08-11 18:10:07 +00:00
with db.session():
u = self.user
if u.peered:
u.update_peering(False)
2014-05-14 09:57:11 +00:00
self.online = False
return
2014-05-17 14:26:59 +00:00
logger.debug('urllib2.HTTPError %s %s', e, e.code)
2014-05-14 09:57:11 +00:00
self.online = False
return None
2014-09-02 22:32:44 +00:00
except urllib.error.URLError as e:
2014-05-17 14:26:59 +00:00
logger.debug('urllib2.URLError %s', e)
2014-05-14 09:57:11 +00:00
self.online = False
return None
except:
2014-05-18 03:01:24 +00:00
logger.debug('unknown url error', exc_info=1)
2014-05-14 09:57:11 +00:00
self.online = False
return None
data = r.read()
2014-05-18 23:24:04 +00:00
if r.headers.get('content-encoding', None) == 'gzip':
2014-10-31 14:47:54 +00:00
data = gzip.GzipFile(fileobj=BytesIO(data)).read()
version = r.headers.get('X-Node-Protocol', None)
if version != settings.NODE_PROTOCOL:
logger.debug('version does not match local: %s remote %s', settings.NODE_PROTOCOL, version)
self.online = False
if version > settings.NODE_PROTOCOL:
state.update_required = True
return None
'''
sig = r.headers.get('X-Node-Signature')
2014-05-04 17:26:43 +00:00
if sig and self._valid(data, sig):
2014-09-02 23:09:42 +00:00
response = json.loads(data.decode('utf-8'))
2014-05-04 17:26:43 +00:00
else:
2014-05-17 14:26:59 +00:00
logger.debug('invalid signature %s', data)
2014-05-04 17:26:43 +00:00
response = None
'''
response = json.loads(data.decode('utf-8'))
2014-05-04 17:26:43 +00:00
return response
def _valid(self, data, sig):
2014-09-08 19:17:35 +00:00
if isinstance(data, str):
2014-09-09 10:08:04 +00:00
data = data.encode()
2014-05-04 17:26:43 +00:00
try:
self.vk.verify(sig, data, encoding=ENCODING)
#except ed25519.BadSignatureError:
except:
return False
return True
@property
def user(self):
2014-08-09 16:14:14 +00:00
with db.session():
return user.models.User.get_or_create(self.user_id)
2014-05-04 17:26:43 +00:00
2014-05-18 03:01:24 +00:00
def can_connect(self):
2015-12-01 08:59:52 +00:00
self.resolve()
url = self.url
2014-05-18 03:01:24 +00:00
try:
if url:
headers = {
'User-Agent': settings.USER_AGENT,
'X-Node-Protocol': settings.NODE_PROTOCOL,
'Accept-Encoding': 'gzip',
}
self._opener.addheaders = list(zip(headers.keys(), headers.values()))
2015-12-01 10:51:58 +00:00
self._opener.timeout = 2
2014-09-02 23:09:42 +00:00
r = self._opener.open(url)
version = r.headers.get('X-Node-Protocol', None)
if version != settings.NODE_PROTOCOL:
logger.debug('version does not match local: %s remote %s', settings.NODE_PROTOCOL, version)
return False
c = r.read()
2015-02-27 10:49:56 +00:00
logger.debug('can connect to: %s (%s)', url, self.user.nickname)
return True
2014-05-18 03:01:24 +00:00
except:
2015-11-26 11:42:52 +00:00
logger.debug('can not connect to: %s (%s)', url, self.user.nickname)
2014-05-18 03:01:24 +00:00
pass
return False
2015-12-01 08:59:52 +00:00
def is_online(self):
return self.online or self.get_local() != None
2014-05-19 15:00:33 +00:00
def _go_online(self):
2014-05-18 03:01:24 +00:00
u = self.user
if u.peered or u.queued:
2015-12-01 08:59:52 +00:00
logger.debug('go_online peered=%s queued=%s %s (%s)', u.peered, u.queued, u.id, u.nickname)
2014-05-04 17:26:43 +00:00
try:
2015-12-01 08:59:52 +00:00
self.online = self.can_connect()
if self.online:
logger.debug('connected to %s', self.url)
2014-05-18 03:01:24 +00:00
if u.queued:
logger.debug('queued peering event pending=%s peered=%s', u.pending, u.peered)
if u.pending == 'sent':
self.peering('requestPeering')
elif u.pending == '' and u.peered:
self.peering('acceptPeering')
else:
#fixme, what about cancel/reject peering here?
self.peering('removePeering')
2014-05-04 17:26:43 +00:00
except:
2015-11-26 11:42:52 +00:00
logger.debug('failed to connect to %s', self.user_id)
2014-05-04 17:26:43 +00:00
self.online = False
else:
self.online = False
2014-05-19 15:00:33 +00:00
def trigger_status(self):
2015-12-02 21:05:23 +00:00
if self.online is not None:
trigger_event('status', {
'id': self.user_id,
'online': self.online
})
2014-05-04 17:26:43 +00:00
def pullChanges(self):
2015-12-01 13:54:10 +00:00
if not self.online or not self.user.peered:
2015-12-01 08:59:52 +00:00
return True
2014-08-09 16:14:14 +00:00
last = Changelog.query.filter_by(user_id=self.user_id).order_by('-revision').first()
from_revision = last.revision + 1 if last else 0
2015-12-01 08:59:52 +00:00
try:
changes = self.request('pullChanges', from_revision)
except:
self.online = False
logger.debug('%s went offline', self.user.name)
return False
2014-08-09 16:14:14 +00:00
if not changes:
return False
with db.session():
r = Changelog.apply_changes(self.user, changes)
2015-12-01 08:59:52 +00:00
return r
2014-05-04 17:26:43 +00:00
def pushChanges(self, changes):
2014-05-17 14:26:59 +00:00
logger.debug('pushing changes to %s %s', self.user_id, changes)
if self.online:
try:
r = self.request('pushChanges', changes)
except:
self.online = False
r = False
logger.debug('pushedChanges %s %s', r, self.user_id)
2014-05-04 17:26:43 +00:00
2014-05-18 03:01:24 +00:00
def peering(self, action):
u = self.user
if action in ('requestPeering', 'acceptPeering'):
r = self.request(action, settings.preferences['username'], u.info.get('message'))
else:
r = self.request(action, u.info.get('message'))
2014-05-18 23:24:04 +00:00
if r != None:
2014-05-18 03:01:24 +00:00
u.queued = False
if 'message' in u.info:
del u.info['message']
u.save()
else:
logger.debug('peering failed? %s %s', action, r)
if action in ('cancelPeering', 'rejectPeering', 'removePeering'):
self.online = False
2014-05-19 15:00:33 +00:00
else:
self.go_online()
trigger_event('peering.%s'%action.replace('Peering', ''), u.json())
2014-05-04 17:26:43 +00:00
return True
def download(self, item):
from item.models import Transfer
2015-12-01 08:59:52 +00:00
self.resolve()
2014-05-04 17:26:43 +00:00
url = '%s/get/%s' % (self.url, item.id)
headers = {
'X-Node-Protocol': settings.NODE_PROTOCOL,
2014-05-04 17:26:43 +00:00
'User-Agent': settings.USER_AGENT,
}
2014-05-20 00:43:54 +00:00
t1 = datetime.utcnow()
2014-05-17 14:26:59 +00:00
logger.debug('download %s', url)
self._opener.addheaders = list(zip(headers.keys(), headers.values()))
try:
r = self._opener.open(url, timeout=self.TIMEOUT*2)
except:
2015-03-08 11:33:27 +00:00
logger.debug('openurl failed %s', url, exc_info=1)
return False
2014-05-14 09:57:11 +00:00
if r.getcode() == 200:
try:
2015-11-30 23:26:35 +00:00
fileobj = r
if r.headers.get('content-encoding', None) == 'gzip':
2015-11-30 23:26:35 +00:00
fileobj = gzip.GzipFile(fileobj=r)
content = b''
ct = datetime.utcnow()
size = 0
for chunk in iter(lambda: fileobj.read(16*1024), b''):
content += chunk
size += len(chunk)
since_ct = (datetime.utcnow() - ct).total_seconds()
if since_ct > 1:
ct = datetime.utcnow()
t = Transfer.get(item.id)
t.progress = len(content) / item.info['size']
t.save()
trigger_event('transfer', {
'id': item.id, 'progress': t.progress
})
if state.bandwidth:
state.bandwidth.download(size/since_ct)
size = 0
'''
content = fileobj.read()
'''
2015-12-01 08:59:52 +00:00
if state.bandwidth:
state.bandwidth.download(size/since_ct)
size = 0
2014-05-18 23:24:04 +00:00
t2 = datetime.utcnow()
duration = (t2-t1).total_seconds()
if duration:
self.download_speed = len(content) / duration
logger.debug('SPEED %s', ox.format_bits(self.download_speed))
return item.save_file(content)
except:
2015-03-08 11:33:27 +00:00
logger.debug('download failed %s', url, exc_info=1)
return False
2014-05-04 17:26:43 +00:00
else:
2014-05-17 14:26:59 +00:00
logger.debug('FAILED %s', url)
2014-05-04 17:26:43 +00:00
return False
2014-05-14 09:57:11 +00:00
def download_upgrade(self, release):
for module in release['modules']:
path = os.path.join(settings.update_path, release['modules'][module]['name'])
2014-05-04 17:26:43 +00:00
if not os.path.exists(path):
2014-05-14 09:57:11 +00:00
url = '%s/oml/%s' % (self.url, release['modules'][module]['name'])
sha1 = release['modules'][module]['sha1']
2014-05-04 17:26:43 +00:00
headers = {
'User-Agent': settings.USER_AGENT,
}
self._opener.addheaders = list(zip(headers.keys(), headers.values()))
2014-05-14 09:57:11 +00:00
r = self._opener.open(url)
if r.getcode() == 200:
2014-05-04 17:26:43 +00:00
with open(path, 'w') as fd:
2014-05-14 09:57:11 +00:00
fd.write(r.read())
2014-05-04 17:26:43 +00:00
if (ox.sha1sum(path) != sha1):
2014-05-17 14:26:59 +00:00
logger.error('invalid update!')
2014-05-04 17:26:43 +00:00
os.unlink(path)
return False
else:
return False
class Nodes(Thread):
_nodes = {}
2014-05-18 03:01:24 +00:00
_local = None
2014-05-04 17:26:43 +00:00
2014-08-09 16:33:59 +00:00
def __init__(self):
2014-05-04 17:26:43 +00:00
self._q = Queue()
self._running = True
2015-12-02 21:05:23 +00:00
with db.session():
for u in user.models.User.query.filter_by(peered=True):
if 'local' in u.info:
del u.info['local']
u.save()
self.queue('add', u.id)
for u in user.models.User.query.filter_by(queued=True):
logger.debug('adding queued node... %s', u.id)
self.queue('add', u.id)
2014-08-09 16:33:59 +00:00
self._local = LocalNodes()
self._cleanup = PeriodicCallback(lambda: self.queue('cleanup'), 120000)
self._cleanup.start()
2014-05-04 17:26:43 +00:00
Thread.__init__(self)
self.daemon = True
self.start()
def cleanup(self):
if self._running and self._local:
self._local.cleanup()
2014-05-04 17:26:43 +00:00
def queue(self, *args):
self._q.put(list(args))
def is_online(self, id):
2015-12-01 08:59:52 +00:00
return id in self._nodes and self._nodes[id].is_online()
2014-05-04 17:26:43 +00:00
def download(self, id, item):
return id in self._nodes and self._nodes[id].download(item)
def _call(self, target, action, *args):
if target == 'all':
2014-09-02 22:32:44 +00:00
nodes = list(self._nodes.values())
2014-05-14 09:57:11 +00:00
elif target == 'peered':
2014-09-02 22:32:44 +00:00
nodes = [n for n in list(self._nodes.values()) if n.user.peered]
2014-05-04 17:26:43 +00:00
elif target == 'online':
2014-09-02 22:32:44 +00:00
nodes = [n for n in list(self._nodes.values()) if n.online]
2014-05-04 17:26:43 +00:00
else:
nodes = [self._nodes[target]]
for node in nodes:
r = getattr(node, action)(*args)
logger.debug('call node api %s->%s%s = %s', node.user_id, action, args, r)
2014-05-04 17:26:43 +00:00
2014-05-18 03:01:24 +00:00
def _add(self, user_id):
2014-05-04 17:26:43 +00:00
if user_id not in self._nodes:
from user.models import User
2014-08-09 16:14:14 +00:00
with db.session():
self._nodes[user_id] = Node(self, User.get_or_create(user_id))
2014-05-04 17:26:43 +00:00
else:
2014-05-18 03:01:24 +00:00
if not self._nodes[user_id].online:
2014-05-19 15:00:33 +00:00
self._nodes[user_id].ping()
2014-05-04 17:26:43 +00:00
def run(self):
2014-08-09 16:14:14 +00:00
while self._running:
args = self._q.get()
if args:
if args[0] == 'cleanup':
self.cleanup()
elif args[0] == 'add':
2014-08-09 16:14:14 +00:00
self._add(args[1])
else:
self._call(*args)
2014-05-04 17:26:43 +00:00
def join(self):
self._running = False
self._q.put(None)
2014-09-02 22:32:44 +00:00
for node in list(self._nodes.values()):
2014-05-19 15:00:33 +00:00
node.join()
if self._local:
self._local.join()
2014-05-04 17:26:43 +00:00
return Thread.join(self)
def publish_node():
update_online()
state.check_nodes = PeriodicCallback(check_nodes, 120000)
state.check_nodes.start()
state._online = PeriodicCallback(update_online, 60000)
state._online.start()
def update_online():
online = state.tor and state.tor.is_online()
if online != state.online:
state.online = online
trigger_event('status', {
'id': settings.USER_ID,
'online': state.online
})
2015-11-26 11:16:09 +00:00
if not settings.server.get('migrated_id', False):
2015-11-26 11:06:01 +00:00
r = directory.put(settings.sk, {
'id': settings.USER_ID,
})
logger.debug('push id to directory %s', r)
2015-11-26 11:16:09 +00:00
if r:
settings.server['migrated_id'] = True
def check_nodes():
if state.online:
with db.session():
for u in user.models.User.query.filter_by(queued=True):
if not state.nodes.is_online(u.id):
logger.debug('queued peering message for %s trying to connect...', u.id)
state.nodes.queue('add', u.id)