297 lines
8.9 KiB
Python
297 lines
8.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
# vi:si:et:sw=4:sts=4:ts=4
|
|
from __future__ import division
|
|
|
|
from Queue import Queue
|
|
from threading import Thread
|
|
import json
|
|
|
|
from datetime import datetime
|
|
import os
|
|
|
|
import ox
|
|
import ed25519
|
|
import requests
|
|
|
|
import settings
|
|
import user.models
|
|
from changelog import Changelog
|
|
|
|
import directory
|
|
from websocket import trigger_event
|
|
from localnodes import LocalNodes
|
|
|
|
ENCODING='base64'
|
|
|
|
class Node(object):
|
|
online = False
|
|
download_speed = 0
|
|
|
|
def __init__(self, nodes, user):
|
|
self._nodes = nodes
|
|
self._app = nodes._app
|
|
self.user_id = user.id
|
|
key = str(user.id)
|
|
self.vk = ed25519.VerifyingKey(key, encoding=ENCODING)
|
|
self.go_online()
|
|
|
|
@property
|
|
def url(self):
|
|
local = self.get_local()
|
|
if local:
|
|
url = 'http://[%s]:%s' % (local['host'], local['port'])
|
|
print 'using local peer discovery to access node', url
|
|
elif not self.host:
|
|
return None
|
|
else:
|
|
if ':' in self.host:
|
|
url = 'http://[%s]:%s' % (self.host, self.port)
|
|
else:
|
|
url = 'http://%s:%s' % (self.host, self.port)
|
|
return url
|
|
|
|
def resolve(self):
|
|
r = directory.get(self.vk)
|
|
if r:
|
|
self.host = r['host']
|
|
if 'port' in r:
|
|
self.port = r['port']
|
|
else:
|
|
self.host = None
|
|
self.port = 9851
|
|
|
|
def get_local(self):
|
|
if self._nodes and self._nodes._local:
|
|
return self._nodes._local.get(self.user_id)
|
|
return None
|
|
|
|
def request(self, action, *args):
|
|
url = self.url
|
|
if not url:
|
|
self.resolve()
|
|
url = self.url
|
|
if not self.url:
|
|
print 'unable to find host', self.user_id
|
|
self.online = False
|
|
return None
|
|
content = json.dumps([action, args])
|
|
sig = settings.sk.sign(content, encoding=ENCODING)
|
|
headers = {
|
|
'User-Agent': settings.USER_AGENT,
|
|
'Accept': 'text/plain',
|
|
'Accept-Encoding': 'gzip',
|
|
'Content-Type': 'application/json',
|
|
'X-Ed25519-Key': settings.USER_ID,
|
|
'X-Ed25519-Signature': sig,
|
|
}
|
|
r = requests.post(url, data=content, headers=headers)
|
|
if r.status_code == 403:
|
|
print 'REMOTE ENDED PEERING'
|
|
if self.user.peered:
|
|
self.user.update_peering(False)
|
|
self.online = False
|
|
data = r.content
|
|
sig = r.headers.get('X-Ed25519-Signature')
|
|
if sig and self._valid(data, sig):
|
|
response = json.loads(data)
|
|
else:
|
|
print 'invalid signature', data
|
|
response = None
|
|
return response
|
|
|
|
def _valid(self, data, sig):
|
|
try:
|
|
self.vk.verify(sig, data, encoding=ENCODING)
|
|
#except ed25519.BadSignatureError:
|
|
except:
|
|
return False
|
|
return True
|
|
|
|
@property
|
|
def user(self):
|
|
return user.models.User.get_or_create(self.user_id)
|
|
|
|
def go_online(self):
|
|
self.resolve()
|
|
if self.user.peered:
|
|
try:
|
|
self.online = False
|
|
print 'type to connect to', self.user_id
|
|
self.pullChanges()
|
|
print 'connected to', self.user_id
|
|
self.online = True
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
print 'failed to connect to', self.user_id
|
|
self.online = False
|
|
else:
|
|
self.online = False
|
|
trigger_event('status', {
|
|
'id': self.user_id,
|
|
'status': 'online' if self.online else 'offline'
|
|
})
|
|
|
|
def pullChanges(self):
|
|
with self._app.app_context():
|
|
last = Changelog.query.filter_by(user_id=self.user_id).order_by('-revision').first()
|
|
from_revision = last.revision + 1 if last else 0
|
|
changes = self.request('pullChanges', from_revision)
|
|
if not changes:
|
|
return False
|
|
for change in changes:
|
|
if not Changelog.apply_change(self.user, change):
|
|
print 'FAIL', change
|
|
break
|
|
return False
|
|
return True
|
|
|
|
def pushChanges(self, changes):
|
|
print 'pushing changes to', self.user_id, changes
|
|
try:
|
|
r = self.request('pushChanges', changes)
|
|
except:
|
|
self.online = False
|
|
trigger_event('status', {
|
|
'id': self.user_id,
|
|
'status': 'offline'
|
|
})
|
|
r = False
|
|
print r
|
|
|
|
def requestPeering(self, message):
|
|
p = self.user
|
|
p.pending = 'sent'
|
|
p.save()
|
|
r = self.request('requestPeering', settings.preferences['username'], message)
|
|
return True
|
|
|
|
def acceptPeering(self, message):
|
|
r = self.request('acceptPeering', settings.preferences['username'], message)
|
|
p = self.user
|
|
p.update_peering(True)
|
|
self.go_online()
|
|
return True
|
|
|
|
def rejectPeering(self, message):
|
|
print 'reject peering!!!', self.user
|
|
p = self.user
|
|
p.update_peering(False)
|
|
r = self.request('rejectPeering', message)
|
|
print 'reject peering!!!', r
|
|
self.online = False
|
|
return True
|
|
|
|
def removePeering(self, message):
|
|
print 'remove peering!!!', self.user
|
|
p = self.user
|
|
if p.peered:
|
|
p.update_peering(False)
|
|
r = self.request('removePeering', message)
|
|
self.online = False
|
|
return True
|
|
|
|
def cancelPeering(self, message):
|
|
p = self.user
|
|
p.update_peering(False)
|
|
self.online = False
|
|
r = self.request('cancelPeering', message)
|
|
return True
|
|
|
|
def download(self, item):
|
|
url = '%s/get/%s' % (self.url, item.id)
|
|
headers = {
|
|
'User-Agent': settings.USER_AGENT,
|
|
}
|
|
t1 = datetime.now()
|
|
print 'GET', url
|
|
r = requests.get(url, headers=headers)
|
|
if r.status_code == 200:
|
|
t2 = datetime.now()
|
|
duration = (t2-t1).total_seconds()
|
|
if duration:
|
|
self.download_speed = len(r.content) / duration
|
|
print 'SPEED', ox.format_bits(self.download_speed)
|
|
return item.save_file(r.content)
|
|
else:
|
|
print 'FAILED', url
|
|
return False
|
|
|
|
def download_upgrade(self):
|
|
for module in settings.release['modules']:
|
|
path = os.path.join(settings.update_path, settings.release['modules'][module]['name'])
|
|
if not os.path.exists(path):
|
|
url = '%s/oml/%s' % (self.url, settings.release['modules'][module]['name'])
|
|
sha1 = settings.release['modules'][module]['sha1']
|
|
headers = {
|
|
'User-Agent': settings.USER_AGENT,
|
|
}
|
|
r = requests.get(url, headers=headers)
|
|
if r.status_code == 200:
|
|
with open(path, 'w') as fd:
|
|
fd.write(r.content)
|
|
if (ox.sha1sum(path) != sha1):
|
|
print 'invalid update!'
|
|
os.unlink(path)
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
class Nodes(Thread):
|
|
_nodes = {}
|
|
|
|
def __init__(self, app):
|
|
self._app = app
|
|
self._q = Queue()
|
|
self._running = True
|
|
self._local = LocalNodes(app)
|
|
Thread.__init__(self)
|
|
self.daemon = True
|
|
self.start()
|
|
|
|
def queue(self, *args):
|
|
self._q.put(list(args))
|
|
|
|
def check_online(self, id):
|
|
return id in self._nodes and self._nodes[id].online
|
|
|
|
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':
|
|
nodes = self._nodes.values()
|
|
elif target == 'online':
|
|
nodes = [n for n in self._nodes.values() if n.online]
|
|
else:
|
|
nodes = [self._nodes[target]]
|
|
for node in nodes:
|
|
getattr(node, action)(*args)
|
|
|
|
def _add_node(self, user_id):
|
|
if user_id not in self._nodes:
|
|
from user.models import User
|
|
self._nodes[user_id] = Node(self, User.get_or_create(user_id))
|
|
'''
|
|
else:
|
|
self._nodes[user_id].online = True
|
|
trigger_event('status', {
|
|
'id': user_id,
|
|
'status': 'online'
|
|
})
|
|
'''
|
|
|
|
def run(self):
|
|
with self._app.app_context():
|
|
while self._running:
|
|
args = self._q.get()
|
|
if args:
|
|
if args[0] == 'add':
|
|
self._add_node(args[1])
|
|
else:
|
|
self._call(*args)
|
|
|
|
def join(self):
|
|
self._running = False
|
|
self._q.put(None)
|
|
return Thread.join(self)
|