-
Notifications
You must be signed in to change notification settings - Fork 206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] API Redesign #1607
[WIP] API Redesign #1607
Changes from all commits
a690940
00181f9
110e5c8
ba61877
893c86e
c1bf32b
ad47e52
3d434cf
a222bb8
a0d07f4
4e6cc2d
840633e
6e75205
b275944
6f4ffeb
3ed4ba7
83bd420
38fb161
092b312
46ebe88
f8cc62c
313b747
d699408
7d956f6
d890979
1e3a226
c506c0a
7cb5a40
71cfb0c
a2878c6
981d3a5
91e69d1
9fb4df0
b1b86ac
590b965
bc30b79
7a403e4
d8b4b63
861411c
366a060
7916b62
9ca45d6
f27531c
4b0a7a6
a4b4fd3
45998bd
d607602
2945b07
4a5b02e
69c522d
3e72e4d
d3d62ac
9cc97c1
68e7f1e
07f933c
3cc5406
7474f5b
c2dea42
c22ee5f
1c3b049
d89d5f3
d989d7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import argparse | ||
import logging | ||
import multiprocessing | ||
import signal | ||
from multiprocessing import Process | ||
from threading import Timer | ||
|
||
import flask | ||
from counterpartycore import server | ||
from counterpartycore.lib import ( | ||
blocks, | ||
config, | ||
database, | ||
ledger, | ||
) | ||
from counterpartycore.lib.api.routes import ROUTES | ||
from counterpartycore.lib.api.util import get_backend_height, init_api_access_log, remove_rowids | ||
from flask import Flask, request | ||
from flask import g as flask_globals | ||
from flask_cors import CORS | ||
from flask_httpauth import HTTPBasicAuth | ||
|
||
multiprocessing.set_start_method("spawn", force=True) | ||
|
||
logger = logging.getLogger(config.LOGGER_NAME) | ||
auth = HTTPBasicAuth() | ||
|
||
BACKEND_HEIGHT = 0 | ||
REFRESH_BACKEND_HEIGHT_INTERVAL = 10 | ||
BACKEND_HEIGHT_TIMER = None | ||
|
||
|
||
def get_db(): | ||
"""Get the database connection.""" | ||
if not hasattr(flask_globals, "db"): | ||
flask_globals.db = database.get_connection(read_only=True) | ||
return flask_globals.db | ||
|
||
|
||
@auth.verify_password | ||
def verify_password(username, password): | ||
return username == config.API_USER and password == config.API_PASSWORD | ||
|
||
|
||
def api_root(): | ||
counterparty_height = blocks.last_db_index(get_db()) | ||
routes = [] | ||
for path in ROUTES: | ||
Check warning Code scanning / pylint Consider iterating with .items(). Warning
Consider iterating with .items().
|
||
route = ROUTES[path] | ||
routes.append( | ||
{ | ||
"path": path, | ||
"args": route.get("args", []), | ||
"description": route.get("description", ""), | ||
} | ||
) | ||
network = "mainnet" | ||
if config.TESTNET: | ||
network = "testnet" | ||
elif config.REGTEST: | ||
network = "regtest" | ||
elif config.TESTCOIN: | ||
network = "testcoin" | ||
return { | ||
"server_ready": counterparty_height >= BACKEND_HEIGHT, | ||
"network": network, | ||
"version": config.VERSION_STRING, | ||
"backend_height": BACKEND_HEIGHT, | ||
"counterparty_height": counterparty_height, | ||
"routes": routes, | ||
} | ||
|
||
|
||
def inject_headers(result, return_code=None): | ||
server_ready = ledger.CURRENT_BLOCK_INDEX >= BACKEND_HEIGHT | ||
http_code = 200 | ||
if return_code: | ||
http_code = return_code | ||
elif not server_ready: | ||
http_code = config.API_NOT_READY_HTTP_CODE | ||
if isinstance(result, flask.Response): | ||
response = result | ||
else: | ||
response = flask.make_response(flask.jsonify(result), http_code) | ||
response.headers["X-COUNTERPARTY-HEIGHT"] = ledger.CURRENT_BLOCK_INDEX | ||
response.headers["X-COUNTERPARTY-READY"] = ledger.CURRENT_BLOCK_INDEX >= BACKEND_HEIGHT | ||
response.headers["X-BACKEND-HEIGHT"] = BACKEND_HEIGHT | ||
return response | ||
|
||
|
||
def prepare_args(route, **kwargs): | ||
function_args = dict(kwargs) | ||
if "pass_all_args" in route and route["pass_all_args"]: | ||
function_args = request.args | function_args | ||
elif "args" in route: | ||
for arg in route["args"]: | ||
arg_name = arg["name"] | ||
if arg_name in function_args: | ||
continue | ||
str_arg = request.args.get(arg_name) | ||
if str_arg is None and arg["required"]: | ||
raise ValueError(f"Missing required parameter: {arg_name}") | ||
if str_arg is None: | ||
function_args[arg_name] = arg["default"] | ||
elif arg["type"] == "bool": | ||
function_args[arg_name] = str_arg.lower() in ["true", "1"] | ||
elif arg["type"] == "int": | ||
try: | ||
function_args[arg_name] = int(str_arg) | ||
except ValueError as e: | ||
raise ValueError(f"Invalid integer: {arg_name}") from e | ||
else: | ||
function_args[arg_name] = str_arg | ||
return function_args | ||
|
||
|
||
@auth.login_required | ||
def handle_route(**kwargs): | ||
db = get_db() | ||
# update the current block index | ||
ledger.CURRENT_BLOCK_INDEX = blocks.last_db_index(db) | ||
rule = str(request.url_rule.rule) | ||
if rule == "/": | ||
result = api_root() | ||
else: | ||
route = ROUTES.get(rule) | ||
try: | ||
function_args = prepare_args(route, **kwargs) | ||
except ValueError as e: | ||
return inject_headers({"error": str(e)}, return_code=400) | ||
result = route["function"](db, **function_args) | ||
result = remove_rowids(result) | ||
return inject_headers(result) | ||
|
||
|
||
def run_api_server(args): | ||
# default signal handlers | ||
signal.signal(signal.SIGTERM, signal.SIG_DFL) | ||
signal.signal(signal.SIGINT, signal.default_int_handler) | ||
|
||
app = Flask(config.APP_NAME) | ||
# Initialise log and config | ||
server.initialise_log_and_config(argparse.Namespace(**args)) | ||
with app.app_context(): | ||
if not config.API_NO_ALLOW_CORS: | ||
CORS(app) | ||
# Initialise the API access log | ||
init_api_access_log(app) | ||
# Get the last block index | ||
ledger.CURRENT_BLOCK_INDEX = blocks.last_db_index(get_db()) | ||
# Add routes | ||
app.add_url_rule("/", view_func=handle_route) | ||
for path in ROUTES: | ||
app.add_url_rule(path, view_func=handle_route) | ||
# run the scheduler to refresh the backend height | ||
# `no_refresh_backend_height` used only for testing. TODO: find a way to mock it | ||
if "no_refresh_backend_height" not in args or not args["no_refresh_backend_height"]: | ||
refresh_backend_height() | ||
try: | ||
# Start the API server | ||
app.run(host=config.API_HOST, port=config.API_PORT, debug=False) | ||
finally: | ||
# ensure timer is cancelled | ||
if BACKEND_HEIGHT_TIMER: | ||
BACKEND_HEIGHT_TIMER.cancel() | ||
|
||
|
||
def refresh_backend_height(): | ||
global BACKEND_HEIGHT, BACKEND_HEIGHT_TIMER # noqa F811 | ||
Check warning Code scanning / pylint Using the global statement. Warning
Using the global statement.
|
||
BACKEND_HEIGHT = get_backend_height() | ||
if BACKEND_HEIGHT_TIMER: | ||
BACKEND_HEIGHT_TIMER.cancel() | ||
BACKEND_HEIGHT_TIMER = Timer(REFRESH_BACKEND_HEIGHT_INTERVAL, refresh_backend_height) | ||
BACKEND_HEIGHT_TIMER.start() | ||
|
||
|
||
class APIServer(object): | ||
Check warning Code scanning / pylint Class 'APIServer' inherits from object, can be safely removed from bases in python3. Warning
Class 'APIServer' inherits from object, can be safely removed from bases in python3.
|
||
def __init__(self): | ||
self.process = None | ||
|
||
def start(self, args): | ||
if self.process is not None: | ||
raise Exception("API server is already running") | ||
Check warning Code scanning / pylint Raising too general exception: Exception. Warning
Raising too general exception: Exception.
|
||
self.process = Process(target=run_api_server, args=(vars(args),)) | ||
self.process.start() | ||
return self.process | ||
|
||
def stop(self): | ||
logger.info("Stopping API server v2...") | ||
if self.process and self.process.is_alive(): | ||
self.process.terminate() | ||
self.process = None |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium