mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-09-30 20:46:32 +00:00
- Update app.py with improvements - Update requirements.txt dependencies - Add .env.example template
369 lines
11 KiB
Python
369 lines
11 KiB
Python
"""
|
|
LEGACY BOOTNODE APPLICATION
|
|
===========================
|
|
|
|
⚠️ SECURITY NOTICE: This is the legacy Bootnode application.
|
|
|
|
✅ FIXED: Hardcoded credentials have been removed and replaced with environment variables.
|
|
|
|
🔄 MIGRATION RECOMMENDED: Consider migrating to the modern FastAPI version in /api/bootnode/
|
|
- Better security with JWT authentication
|
|
- Modern async/await patterns
|
|
- Comprehensive API documentation
|
|
- Support for 11+ blockchains
|
|
- Rate limiting and monitoring
|
|
|
|
To run this legacy app safely:
|
|
1. Copy .env.example to .env
|
|
2. Set secure values for all environment variables
|
|
3. Use strong passwords and random tokens
|
|
|
|
For new development, use the modern API in /api/ instead.
|
|
"""
|
|
|
|
from threading import Thread
|
|
from functools import wraps
|
|
from quart import Quart, request
|
|
from quart_cors import cors
|
|
from bootnode import Bootnode
|
|
from util import to_nodes, jsonify
|
|
from pymongo import MongoClient
|
|
import datetime
|
|
import asyncio
|
|
import httpx
|
|
import datetime
|
|
import os
|
|
|
|
# Load environment variables
|
|
from dotenv import load_dotenv
|
|
load_dotenv()
|
|
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.get_child_watcher().attach_loop(loop)
|
|
|
|
app = Quart(__name__)
|
|
cors(app)
|
|
|
|
SUPPORTED_PROVIDERS = ['private-cloud', 'google']
|
|
SUPPORTED_ZONES = {
|
|
'google': ['us-central1-a', 'europe-west6-a', 'asia-east2-a'],
|
|
'private-cloud': ['london1', 'munich1', 'oslo1', 'tokyo1'],
|
|
}
|
|
|
|
# connect to mongo and set up database vars
|
|
mongo_client = MongoClient()
|
|
bootnode_db = mongo_client.bootnode
|
|
nodes_collection = bootnode_db.nodes
|
|
updates_collection = bootnode_db.updates
|
|
node_statuses = bootnode_db.node_statuses
|
|
|
|
# set up system update loop
|
|
async def update_nodes_lambda(date, zone, provider):
|
|
print('updating', date, zone, provider)
|
|
print('-------- Getting ' + provider + ' nodes in zone: ' + zone + ' --------')
|
|
bootnode = Bootnode('casper', 'testnet', provider, zone)
|
|
|
|
deployments = [d.to_dict() for d in await bootnode.list_deployments()]
|
|
services = [s.to_dict() for s in await bootnode.list_services()]
|
|
pods = [p.to_dict() for p in await bootnode.list_pods()]
|
|
|
|
nodes = to_nodes(deployments, services, pods, zone)
|
|
|
|
for node in nodes:
|
|
node['lastUpdated'] = date
|
|
|
|
if node.get('blockchain', None) == 'casper' and node.get('ip', None) is not None:
|
|
try:
|
|
ip = node['ip']
|
|
port = 9001
|
|
if provider == 'private-cloud':
|
|
for p in node['ports']:
|
|
if p['port'] == 9001:
|
|
port = p['nodePort']
|
|
|
|
node['provider'] = provider
|
|
|
|
print('Pod', zone + ' ' + node['id'] + ' ' + node['ip'])
|
|
|
|
start = datetime.datetime.now()
|
|
|
|
async with httpx.AsyncClient(verify=False) as client:
|
|
reqs = [
|
|
client.put(f'https://{ip}:{port}/show/blocks',
|
|
json={'depth': 1}),
|
|
client.put(f'https://{ip}:{port}/show/dag',
|
|
json={'depth': 10,
|
|
'showJustifications': True})
|
|
]
|
|
|
|
ress = await asyncio.gather(*reqs)
|
|
|
|
blockdata = ress[0]
|
|
dag = ress[1]
|
|
|
|
end = datetime.datetime.now()
|
|
|
|
node['metadata'] = {
|
|
'block': blockdata.json()[0],
|
|
'dag': dag.json(),
|
|
}
|
|
node['latencyMillis'] = (end - start).microseconds / 1000
|
|
|
|
|
|
except Exception as e:
|
|
print('cannot get metadata for ' + node['id'] + ': ' +
|
|
str(e))
|
|
|
|
nodes_collection.insert_one(node)
|
|
# except Exception as e:
|
|
# print('update nodes loop error: ' + str(e))
|
|
# finally:
|
|
|
|
# function to spin off thread
|
|
async def update_nodes_loop():
|
|
while True:
|
|
try:
|
|
date = datetime.datetime.utcnow()
|
|
|
|
updates = []
|
|
for provider in SUPPORTED_PROVIDERS:
|
|
zones = SUPPORTED_ZONES[provider]
|
|
|
|
for zone in zones:
|
|
updates.append(update_nodes_lambda(date, zone, provider))
|
|
|
|
await asyncio.gather(*updates)
|
|
|
|
updates_collection.update_one(
|
|
{
|
|
'name': 'nodes',
|
|
},
|
|
{
|
|
'$set': {
|
|
'date': date
|
|
},
|
|
},
|
|
True
|
|
)
|
|
|
|
await asyncio.sleep(1)
|
|
except Exception as e:
|
|
print('update_nodes_loop error' + str(e))
|
|
|
|
def update_nodes_thread():
|
|
print('starting update thread')
|
|
|
|
asyncio.set_event_loop(loop)
|
|
loop.run_until_complete(update_nodes_loop())
|
|
|
|
Thread(target=update_nodes_thread).start()
|
|
|
|
def auth_required(fn):
|
|
@wraps(fn)
|
|
async def wrapped_fn(*args, **kwargs):
|
|
# SECURITY: Check for environment variable instead of hardcoded token
|
|
import os
|
|
expected_token = os.environ.get('LEGACY_API_TOKEN')
|
|
if not expected_token:
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'API token not configured',
|
|
})
|
|
|
|
auth_header = request.headers.get('Authorization')
|
|
if not auth_header or auth_header != f'Bearer {expected_token}':
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'authorization required',
|
|
})
|
|
return await fn(*args, **kwargs)
|
|
return wrapped_fn
|
|
|
|
@app.route('/login', methods=['POST'])
|
|
async def login():
|
|
try:
|
|
json = await request.get_json()
|
|
|
|
# SECURITY: Use environment variables for credentials
|
|
import os
|
|
admin_email = os.environ.get('LEGACY_ADMIN_EMAIL')
|
|
admin_password = os.environ.get('LEGACY_ADMIN_PASSWORD')
|
|
api_token = os.environ.get('LEGACY_API_TOKEN')
|
|
|
|
if not all([admin_email, admin_password, api_token]):
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'Authentication not properly configured'
|
|
})
|
|
|
|
print('login attempt for:', json.get('email', 'unknown'))
|
|
|
|
if json.get('email') == admin_email and json.get('password') == admin_password:
|
|
return jsonify({'token': api_token})
|
|
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'incorrect username or password'
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'could not get login: ' + str(e),
|
|
})
|
|
|
|
@app.route('/nodes', methods=['GET'])
|
|
@auth_required
|
|
async def get_nodes():
|
|
try:
|
|
update = updates_collection.find_one({ 'name': 'nodes' })
|
|
# print(update)
|
|
# print('getting node data as of ' + str(update['date']))
|
|
nodes = nodes_collection.find({'lastUpdated': update['date']})
|
|
|
|
ns = []
|
|
for node in nodes:
|
|
node.pop('_id')
|
|
ns.append(node)
|
|
|
|
return jsonify(ns)
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'could not get nodes: ' + str(e),
|
|
})
|
|
|
|
@app.route('/nodes', methods=['DELETE'])
|
|
@auth_required
|
|
async def delete_nodes():
|
|
try:
|
|
print('deleting everything!')
|
|
|
|
update = updates_collection.find_one({ 'name': 'nodes' })
|
|
nodes = nodes_collection.find({'lastUpdated': update['date']})
|
|
|
|
dns = []
|
|
for node in nodes:
|
|
dns.append(delete_node(node['id'], node['provider'], node['zone']))
|
|
|
|
await asyncio.gather(*dns)
|
|
|
|
print('deleted ' + str(len(dns)) + ' nodes')
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'could not delete a nodes: ' + str(e)
|
|
})
|
|
|
|
@app.route('/nodes', methods=['PUT'])
|
|
@auth_required
|
|
async def put_node():
|
|
try:
|
|
json = await request.get_json()
|
|
|
|
print('launching ' + str(json['number']) + ' nodes in ' + str(json['zone']))
|
|
|
|
provider = json['provider']
|
|
if provider not in SUPPORTED_PROVIDERS:
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': provider + ' is not a valid provider',
|
|
})
|
|
|
|
zone = json['zone']
|
|
if zone not in SUPPORTED_ZONES[provider]:
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': zone + ' is not a valid zone',
|
|
})
|
|
|
|
bootnode = Bootnode('casper', 'testnet', provider, zone)
|
|
|
|
number = 1
|
|
if json['number'] is not None:
|
|
number = int(json['number'])
|
|
|
|
nodes = []
|
|
|
|
ds = []
|
|
for i in range(number):
|
|
|
|
async def create_deployment():
|
|
data = await bootnode.create_deployment()
|
|
# print('deployment created', data.deployment)
|
|
|
|
ds.append(create_deployment())
|
|
|
|
await asyncio.gather(*ds)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'nodes-starting': number,
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'could not create a node: ' + str(e)
|
|
})
|
|
|
|
@app.route('/nodes/<node_id>', methods=['GET'])
|
|
@auth_required
|
|
async def get_node(node_id, provider=None, zone=None):
|
|
try:
|
|
json = await request.get_json()
|
|
|
|
if provider is None:
|
|
provider = json['provider']
|
|
if zone is None:
|
|
zone = json['zone']
|
|
|
|
bootnode = Bootnode('casper', 'testnet', provider, zone)
|
|
|
|
deployment = await bootnode.get_deployment(node_id)
|
|
service = await bootnode.get_service(node_id)
|
|
pods = [p.to_dict() for p in await bootnode.list_pods(label_selector='app=' + node_id)]
|
|
|
|
return jsonify(to_nodes([deployment.to_dict()],
|
|
[service.to_dict()], pods, zone))
|
|
except Exception as e:
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'node not found: ' + str(e)
|
|
})
|
|
|
|
@app.route('/nodes/<node_id>', methods=['DELETE'])
|
|
@auth_required
|
|
async def delete_node(node_id, provider=None, zone=None):
|
|
is_called = provider is not None
|
|
|
|
try:
|
|
print('deleting ' + str(node_id) + ' from provider ' + str(provider) + ' and zone ' + str(zone))
|
|
|
|
if provider is None:
|
|
json = await request.get_json()
|
|
|
|
provider = json['provider']
|
|
zone = json['zone']
|
|
|
|
bootnode = Bootnode('casper', 'testnet', provider, zone)
|
|
await bootnode.delete_deployment(node_id)
|
|
|
|
return jsonify({
|
|
'status': 'ok',
|
|
})
|
|
except Exception as e:
|
|
print('delete_node error', e)
|
|
|
|
return jsonify({
|
|
'status': 'failed',
|
|
'error': 'could not delete: ' + str(e)
|
|
})
|
|
|
|
if __name__ == '__main__':
|
|
app.run(debug=True, host='0.0.0.0', port='4000')
|