mirror of https://github.com/oxen-io/lokinet
Merge branch 'staging' of https://github.com/loki-project/loki-network
commit
2e4ad12a87
@ -0,0 +1,4 @@
|
||||
*.egg-info
|
||||
v
|
||||
__pycache__
|
||||
dist
|
@ -0,0 +1,111 @@
|
||||
#
|
||||
# super freaking dead simple wicked awesome bencode library
|
||||
#
|
||||
from io import BytesIO
|
||||
|
||||
class BCodec:
|
||||
encoding = 'utf-8'
|
||||
def __init__(self, fd):
|
||||
self._fd = fd
|
||||
|
||||
def _write_bytestring(self, bs):
|
||||
self._fd.write('{}:'.format(len(bs)).encode('ascii'))
|
||||
self._fd.write(bs)
|
||||
|
||||
def _write_list(self, l):
|
||||
self._fd.write(b'l')
|
||||
for item in l:
|
||||
self.encode(item)
|
||||
self._fd.write(b'e')
|
||||
|
||||
def _write_dict(self, d):
|
||||
self._fd.write(b'd')
|
||||
keys = list(d.keys())
|
||||
keys.sort()
|
||||
for k in keys:
|
||||
if isinstance(k, str):
|
||||
self._write_bytestring(k.encode(self.encoding))
|
||||
elif isinstance(k, bytes):
|
||||
self._write_bytestring(k)
|
||||
else:
|
||||
self._write_bytestring('{}'.format(k).encode(self.encoding))
|
||||
self.encode(d[k])
|
||||
self._fd.write(b'e')
|
||||
|
||||
def _write_int(self, i):
|
||||
self._fd.write('i{}e'.format(i).encode(self.encoding))
|
||||
|
||||
def encode(self, obj):
|
||||
if isinstance(obj, dict):
|
||||
self._write_dict(obj)
|
||||
elif isinstance(obj, list):
|
||||
self._write_list(obj)
|
||||
elif isinstance(obj, int):
|
||||
self._write_int(obj)
|
||||
elif isinstance(obj, str):
|
||||
self._write_bytestring(obj.encode(self.encoding))
|
||||
elif isinstance(obj, bytes):
|
||||
self._write_bytestring(obj)
|
||||
elif hasattr(obj, bencode):
|
||||
obj.bencode(self._fd)
|
||||
else:
|
||||
raise ValueError("invalid object type")
|
||||
|
||||
def _readuntil(self, delim):
|
||||
b = bytes()
|
||||
while True:
|
||||
ch = self._fd.read(1)
|
||||
if ch == delim:
|
||||
return b
|
||||
b += ch
|
||||
|
||||
def _decode_list(self):
|
||||
l = list()
|
||||
while True:
|
||||
b = self._fd.read(1)
|
||||
if b == b'e':
|
||||
return l
|
||||
l.append(self._decode(b))
|
||||
|
||||
def _decode_dict(self):
|
||||
d = dict()
|
||||
while True:
|
||||
ch = self._fd.read(1)
|
||||
if ch == b'e':
|
||||
return d
|
||||
k = self._decode_bytestring(ch)
|
||||
d[k] = self.decode()
|
||||
|
||||
def _decode_int(self):
|
||||
return int(self._readuntil(b'e'), 10)
|
||||
|
||||
def _decode_bytestring(self, ch):
|
||||
ch += self._readuntil(b':')
|
||||
l = int(ch, base=10)
|
||||
return self._fd.read(l)
|
||||
|
||||
def _decode(self, ch):
|
||||
if ch == b'd':
|
||||
return self._decode_dict()
|
||||
elif ch == b'l':
|
||||
return self._decode_list()
|
||||
elif ch == b'i':
|
||||
return self._decode_int()
|
||||
elif ch in [b'0',b'1',b'2',b'3',b'4',b'5',b'6',b'7',b'8',b'9']:
|
||||
return self._decode_bytestring(ch)
|
||||
else:
|
||||
raise ValueError(ch)
|
||||
|
||||
def decode(self):
|
||||
return self._decode(self._fd.read(1))
|
||||
|
||||
|
||||
def bencode(obj):
|
||||
buf = BytesIO()
|
||||
b = BCodec(buf)
|
||||
b.encode(obj)
|
||||
return buf.getvalue()
|
||||
|
||||
def bdecode(bytestring):
|
||||
buf = BytesIO(bytestring)
|
||||
return BCodec(buf).decode()
|
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# python wsgi application for managing many lokinet instances
|
||||
#
|
||||
|
||||
__doc__ = """lokinet bootserv wsgi app
|
||||
run me with via gunicorn pylokinet.bootserv:app
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from pylokinet import rc
|
||||
import random
|
||||
|
||||
class RCHolder:
|
||||
|
||||
_dir = '/tmp/lokinet_nodedb/'
|
||||
|
||||
_rc_files = list()
|
||||
|
||||
def __init__(self):
|
||||
if os.path.exists(self._dir):
|
||||
for root, _, files in os.walk(self._dir):
|
||||
for f in files:
|
||||
self._add_rc(os.path.join(root, f))
|
||||
else:
|
||||
os.mkdir(self._dir)
|
||||
|
||||
def validate_then_put(self, body):
|
||||
if not rc.validate(body):
|
||||
return False
|
||||
k = rc.get_pubkey(body)
|
||||
print(k)
|
||||
if k is None:
|
||||
return False
|
||||
with open(os.path.join(self._dir, k), "wb") as f:
|
||||
f.write(body)
|
||||
return True
|
||||
|
||||
def _add_rc(self, fpath):
|
||||
self._rc_files.append(fpath)
|
||||
|
||||
def serve_random(self):
|
||||
with open(random.choice(self._rc_files), 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
def empty(self):
|
||||
return len(self._rc_files) == 0
|
||||
|
||||
|
||||
def handle_rc_upload(body, respond):
|
||||
holder = RCHolder()
|
||||
if holder.validate_then_put(body):
|
||||
respond("200 OK", [("Content-Type", "text/plain")])
|
||||
return ["rc accepted".encode('ascii')]
|
||||
else:
|
||||
respond("400 Bad Request", [("Content-Type", "text/plain")])
|
||||
return ["bad rc".encode('ascii')]
|
||||
|
||||
|
||||
def serve_random_rc():
|
||||
holder = RCHolder()
|
||||
if holder.empty():
|
||||
return None
|
||||
else:
|
||||
return holder.serve_random()
|
||||
|
||||
def response(status, msg, respond):
|
||||
respond(status, [("Content-Type", "text/plain"), ("Content-Length", "{}".format(len(msg)))])
|
||||
return [msg.encode("utf-8")]
|
||||
|
||||
def app(environ, start_response):
|
||||
request_body_size = int(environ.get("CONTENT_LENGTH", 0))
|
||||
method = environ.get("REQUEST_METHOD")
|
||||
if method.upper() == "PUT" and request_body_size > 0:
|
||||
rcbody = environ.get("wsgi.input").read(request_body_size)
|
||||
return handle_rc_upload(rcbody, start_response)
|
||||
elif method.upper() == "GET":
|
||||
if environ.get("PATH_INFO") == "/bootstrap.signed":
|
||||
resp = serve_random_rc()
|
||||
if resp is not None:
|
||||
start_response('200 OK', [("Content-Type", "application/octet-stream")])
|
||||
return [resp]
|
||||
else:
|
||||
return response('404 Not Found', 'no RCs', start_response)
|
||||
elif environ.get("PATH_INFO") == "/ping":
|
||||
return response('200 OK', 'pong', start_response)
|
||||
else:
|
||||
return response('400 Bad Request', 'invalid path', start_response)
|
||||
else:
|
||||
return response('405 Method Not Allowed', 'method not allowed', start_response)
|
@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# lokinet runtime wrapper
|
||||
#
|
||||
|
||||
from ctypes import *
|
||||
import configparser
|
||||
import signal
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
|
||||
from pylokinet import rc
|
||||
|
||||
lib_file = os.path.join(os.path.realpath('.'), 'liblokinet-shared.so')
|
||||
|
||||
|
||||
def log(msg):
|
||||
sys.stderr.write("lokinet: {}\n".format(msg))
|
||||
sys.stderr.flush()
|
||||
|
||||
class LokiNET(threading.Thread):
|
||||
|
||||
lib = None
|
||||
ctx = 0
|
||||
failed = False
|
||||
up = False
|
||||
|
||||
asRouter = True
|
||||
|
||||
def configure(self, lib, conf, ip=None, port=None, ifname=None):
|
||||
log("configure lib={} conf={}".format(lib, conf))
|
||||
if not os.path.exists(os.path.dirname(conf)):
|
||||
os.mkdir(os.path.dirname(conf))
|
||||
try:
|
||||
self.lib = CDLL(lib)
|
||||
except OSError as ex:
|
||||
log("failed to load library: {}".format(ex))
|
||||
return False
|
||||
if self.lib.llarp_ensure_config(conf.encode('utf-8'), os.path.dirname(conf).encode('utf-8'), True, self.asRouter):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(conf)
|
||||
log('overwrite ip="{}" port="{}" ifname="{}"'.format(ip, port, ifname))
|
||||
if ip:
|
||||
config['router']['public-address'] = '{}'.format(ip)
|
||||
if port:
|
||||
config['router']['public-port'] = '{}'.format(port)
|
||||
if ifname and port:
|
||||
config['bind'] = {
|
||||
ifname: '{}'.format(port)
|
||||
}
|
||||
with open(conf, "w") as f:
|
||||
config.write(f)
|
||||
self.ctx = self.lib.llarp_main_init(conf.encode('utf-8'))
|
||||
else:
|
||||
return False
|
||||
return self.lib.llarp_main_setup(self.ctx) == 0
|
||||
|
||||
|
||||
def inform_fail(self):
|
||||
"""
|
||||
inform lokinet crashed
|
||||
"""
|
||||
self.failed = True
|
||||
self._inform()
|
||||
|
||||
def inform_up(self):
|
||||
self.up = True
|
||||
self._inform()
|
||||
|
||||
def _inform(self):
|
||||
"""
|
||||
inform waiter
|
||||
"""
|
||||
|
||||
def wait_for_up(self, timeout):
|
||||
"""
|
||||
wait for lokinet to go up for :timeout: seconds
|
||||
:return True if we are up and running otherwise False:
|
||||
"""
|
||||
# return self._up.wait(timeout)
|
||||
|
||||
def signal(self, sig):
|
||||
if self.ctx and self.lib:
|
||||
self.lib.llarp_main_signal(self.ctx, int(sig))
|
||||
|
||||
def run(self):
|
||||
# self._up.acquire()
|
||||
self.up = True
|
||||
code = self.lib.llarp_main_run(self.ctx)
|
||||
log("llarp_main_run exited with status {}".format(code))
|
||||
if code:
|
||||
self.inform_fail()
|
||||
self.up = False
|
||||
# self._up.release()
|
||||
|
||||
def close(self):
|
||||
if self.lib and self.ctx:
|
||||
self.lib.llarp_main_free(self.ctx)
|
||||
|
||||
def getconf(name, fallback=None):
|
||||
return name in os.environ and os.environ[name] or fallback
|
||||
|
||||
def main(args):
|
||||
log("going up")
|
||||
root = getconf("LOKINET_ROOT")
|
||||
if root is None:
|
||||
print("LOKINET_ROOT was not set")
|
||||
return
|
||||
|
||||
rc_callback = getconf("LOKINET_SUBMIT_URL")
|
||||
if rc_callback is None:
|
||||
print("LOKINET_SUBMIT_URL was not set")
|
||||
return
|
||||
|
||||
bootstrap = getconf("LOKINET_BOOTSTRAP_URL")
|
||||
if bootstrap is None:
|
||||
print("LOKINET_BOOTSTRAP_URL was not set")
|
||||
|
||||
lib = getconf("LOKINET_LIB", lib_file)
|
||||
timeout = int(getconf("LOKINET_TIMEOUT", "5"))
|
||||
ping_interval = int(getconf("LOKINET_PING_INTERVAL", "60"))
|
||||
ping_callback = getconf("LOKINET_PING_URL")
|
||||
ip = getconf("LOKINET_IP")
|
||||
port = getconf("LOKINET_PORT")
|
||||
ifname = getconf("LOKINET_IFNAME")
|
||||
if ping_callback is None:
|
||||
print("LOKINET_PING_URL was not set")
|
||||
return
|
||||
conf = os.path.join(root, "daemon.ini")
|
||||
loki = LokiNET()
|
||||
log("bootstrapping...")
|
||||
try:
|
||||
r = requests.get(bootstrap)
|
||||
if r.status_code == 404:
|
||||
log("bootstrap gave no RCs, we are the seed node")
|
||||
elif r.status_code != 200:
|
||||
raise Exception("http {}".format(r.status_code))
|
||||
else:
|
||||
data = r.content
|
||||
if rc.validate(data):
|
||||
log("valid RC obtained")
|
||||
with open(os.path.join(root, "bootstrap.signed"), "wb") as f:
|
||||
f.write(data)
|
||||
else:
|
||||
raise Exception("invalid RC")
|
||||
except Exception as ex:
|
||||
log("failed to bootstrap: {}".format(ex))
|
||||
loki.close()
|
||||
return
|
||||
if loki.configure(lib, conf, ip, port, ifname):
|
||||
log("configured")
|
||||
|
||||
loki.start()
|
||||
try:
|
||||
log("waiting for spawn")
|
||||
while timeout > 0:
|
||||
time.sleep(1)
|
||||
if loki.failed:
|
||||
log("failed")
|
||||
break
|
||||
log("waiting {}".format(timeout))
|
||||
timeout -= 1
|
||||
if loki.up:
|
||||
log("submitting rc")
|
||||
try:
|
||||
with open(os.path.join(root, 'self.signed'), 'rb') as f:
|
||||
r = requests.put(rc_callback, data=f.read(), headers={"content-type": "application/octect-stream"})
|
||||
log('submit rc reply: HTTP {}'.format(r.status_code))
|
||||
except Exception as ex:
|
||||
log("failed to submit rc: {}".format(ex))
|
||||
loki.signal(signal.SIGINT)
|
||||
time.sleep(2)
|
||||
else:
|
||||
while loki.up:
|
||||
time.sleep(ping_interval)
|
||||
try:
|
||||
r = requests.get(ping_callback)
|
||||
log("ping reply: HTTP {}".format(r.status_code))
|
||||
except Exception as ex:
|
||||
log("failed to submit ping: {}".format(ex))
|
||||
else:
|
||||
log("failed to go up")
|
||||
loki.signal(signal.SIGINT)
|
||||
except KeyboardInterrupt:
|
||||
loki.signal(signal.SIGINT)
|
||||
time.sleep(2)
|
||||
finally:
|
||||
loki.close()
|
||||
else:
|
||||
loki.close()
|
||||
|
||||
main(sys.argv[1:])
|
@ -0,0 +1,23 @@
|
||||
from pylokinet import bencode
|
||||
import pysodium
|
||||
import binascii
|
||||
|
||||
def validate(data):
|
||||
rc = bencode.bdecode(data)
|
||||
if b'z' not in rc or b'k' not in rc:
|
||||
return False
|
||||
sig = rc[b'z']
|
||||
rc[b'z'] = b'\x00' * 64
|
||||
buf = bencode.bencode(rc)
|
||||
try:
|
||||
k = rc[b'k']
|
||||
pysodium.crypto_sign_verify_detached(sig, buf, k)
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def get_pubkey(data):
|
||||
rc = bencode.bdecode(data)
|
||||
if b'k' in rc:
|
||||
return binascii.hexlify(rc[b'k']).decode('ascii')
|
@ -0,0 +1,27 @@
|
||||
# lokinet cluster setup
|
||||
|
||||
clustering lokinet with python 3
|
||||
|
||||
# python3 setup.py install
|
||||
|
||||
## bootserv
|
||||
|
||||
bootserv is a bootstrap server for accepting and serving RCs
|
||||
|
||||
$ gunicorn -b 0.0.0.0:8000 pylokinet.bootserv:app
|
||||
|
||||
## pylokinet instance
|
||||
|
||||
obtain `liblokinet-shared.so` from a lokinet build
|
||||
|
||||
run (root):
|
||||
|
||||
# export LOKINET_ROOT=/tmp/lokinet-instance/
|
||||
# export LOKINET_LIB=/path/to/liblokinet-shared.so
|
||||
# export LOKINET_BOOTSTRAP_URL=http://bootserv.ip.address.here:8000/bootstrap.signed
|
||||
# export LOKINET_PING_URL=http://bootserv.ip.address.here:8000/ping
|
||||
# export LOKINET_SUBMIT_URL=http://bootserv.ip.address.here:8000/
|
||||
# export LOKINET_IP=public.ip.goes.here
|
||||
# export LOKINET_PORT=1090
|
||||
# export LOKINET_IFNAME=eth0
|
||||
# python3 -m pylokinet.instance
|
@ -0,0 +1,14 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
|
||||
setup(
|
||||
name="pylokinet",
|
||||
version="0.0.1",
|
||||
license="ZLIB",
|
||||
author="jeff",
|
||||
author_email="jeff@i2p.rocks",
|
||||
description="lokinet python bindings",
|
||||
url="https://github.com/loki-project/loki-network",
|
||||
install_requires=["pysodium", "requests"],
|
||||
packages=find_packages())
|
Loading…
Reference in New Issue