mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-21 22:31:48 +01:00
clnrest: code refactoring for:
- certificate generation - config options validation - log level from 'error' to 'info' - sending method as None instead "" - added `listclnrest-notifications` for websocket server rune method Changelog-Fixed: websocket server notifications are available with restriction of `readonly` runes
This commit is contained in:
parent
12d8ab6451
commit
baa9a96eb1
5 changed files with 114 additions and 122 deletions
|
@ -60,7 +60,6 @@ def broadcast_from_message_queue():
|
|||
msg = msgq.get()
|
||||
if msg is None:
|
||||
return
|
||||
plugin.log(f"Emitting message: {msg}", "debug")
|
||||
socketio.emit("message", msg)
|
||||
# Wait for a second after processing all items in the queue
|
||||
time.sleep(1)
|
||||
|
@ -81,8 +80,8 @@ def handle_message(message):
|
|||
def ws_connect():
|
||||
try:
|
||||
plugin.log("Client Connecting...", "debug")
|
||||
is_valid_rune = verify_rune(plugin, request)
|
||||
|
||||
rune = request.headers.get("rune", None)
|
||||
is_valid_rune = verify_rune(plugin, rune, "listclnrest-notifications", None)
|
||||
if "error" in is_valid_rune:
|
||||
# Logging as error/warn emits the event for all clients
|
||||
plugin.log(f"Error: {is_valid_rune}", "info")
|
||||
|
@ -137,13 +136,14 @@ def set_application_options(plugin):
|
|||
else:
|
||||
cert_file = Path(f"{CERTS_PATH}/client.pem")
|
||||
key_file = Path(f"{CERTS_PATH}/client-key.pem")
|
||||
if not cert_file.is_file() or not key_file.is_file():
|
||||
plugin.log(f"Certificate not found at {CERTS_PATH}. Generating a new certificate!", "debug")
|
||||
generate_certs(plugin, CERTS_PATH)
|
||||
try:
|
||||
if not cert_file.is_file() or not key_file.is_file():
|
||||
plugin.log(f"Certificate not found at {CERTS_PATH}. Generating a new certificate!", "debug")
|
||||
generate_certs(plugin, REST_HOST, CERTS_PATH)
|
||||
plugin.log(f"Certs Path: {CERTS_PATH}", "debug")
|
||||
except Exception as err:
|
||||
raise Exception(f"{err}: Certificates do not exist at {CERTS_PATH}")
|
||||
|
||||
# Assigning only one worker due to added complexity between gunicorn's multiple worker process forks
|
||||
# and websocket connection's persistance with a single worker.
|
||||
options = {
|
||||
|
@ -164,8 +164,8 @@ class CLNRestApplication(BaseApplication):
|
|||
from utilities.shared import REST_PROTOCOL, REST_HOST, REST_PORT
|
||||
self.application = app
|
||||
self.options = options or {}
|
||||
plugin.log(f"REST server running at {REST_PROTOCOL}://{REST_HOST}:{REST_PORT}", "info")
|
||||
super().__init__()
|
||||
plugin.log(f"REST server running at {REST_PROTOCOL}://{REST_HOST}:{REST_PORT}", "info")
|
||||
|
||||
def load_config(self):
|
||||
config = {key: value for key, value in self.options.items()
|
||||
|
@ -216,7 +216,7 @@ def on_any_notification(request, **kwargs):
|
|||
# A plugin which subscribes to shutdown is expected to exit itself.
|
||||
sys.exit(0)
|
||||
else:
|
||||
msgq.put(str(kwargs))
|
||||
msgq.put(kwargs)
|
||||
|
||||
|
||||
try:
|
||||
|
|
|
@ -5,98 +5,65 @@ from cryptography.x509.oid import NameOID
|
|||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
import datetime
|
||||
from utilities.shared import validate_ip4
|
||||
|
||||
|
||||
def generate_ca_cert(certs_path):
|
||||
# Generate CA Private Key
|
||||
ca_private_key = ec.generate_private_key(ec.SECP256R1())
|
||||
def save_cert(entity_type, cert, private_key, certs_path):
|
||||
"""Serialize and save certificates and keys.
|
||||
`entity_type` is either "ca", "client" or "server"."""
|
||||
with open(os.path.join(certs_path, f"{entity_type}.pem"), "wb") as f:
|
||||
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
||||
with open(os.path.join(certs_path, f"{entity_type}-key.pem"), "wb") as f:
|
||||
f.write(private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()))
|
||||
|
||||
# Generate CA Public Key
|
||||
ca_public_key = ca_private_key.public_key()
|
||||
|
||||
# Generate CA Certificate
|
||||
ca_subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u"cln Root REST CA")])
|
||||
def create_cert_builder(subject_name, issuer_name, public_key, rest_host):
|
||||
list_sans = [x509.DNSName("cln"), x509.DNSName("localhost")]
|
||||
if validate_ip4(rest_host) is True:
|
||||
list_sans.append(x509.IPAddress(ipaddress.IPv4Address(rest_host)))
|
||||
|
||||
ca_cert = (
|
||||
return (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(ca_subject)
|
||||
.issuer_name(ca_subject)
|
||||
.public_key(ca_public_key)
|
||||
.subject_name(subject_name)
|
||||
.issuer_name(issuer_name)
|
||||
.public_key(public_key)
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.datetime.utcnow())
|
||||
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=10 * 365)) # Ten years validity
|
||||
.add_extension(x509.SubjectAlternativeName([x509.DNSName(u"cln"), x509.DNSName(u'localhost'), x509.IPAddress(ipaddress.IPv4Address(u'127.0.0.1'))]), critical=False)
|
||||
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
||||
.sign(ca_private_key, hashes.SHA256())
|
||||
.add_extension(x509.SubjectAlternativeName(list_sans), critical=False)
|
||||
)
|
||||
|
||||
# Create the certs directory if it does not exist
|
||||
os.makedirs(certs_path, exist_ok=True)
|
||||
|
||||
# Serialize CA certificate and write to disk
|
||||
with open(os.path.join(certs_path, "ca.pem"), "wb") as f:
|
||||
f.write(ca_cert.public_bytes(serialization.Encoding.PEM))
|
||||
|
||||
# Serialize and save the private key to a PEM file (CA)
|
||||
with open(os.path.join(certs_path, "ca-key.pem"), "wb") as f:
|
||||
f.write(ca_private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
))
|
||||
|
||||
return ca_subject, ca_private_key
|
||||
|
||||
|
||||
def generate_client_server_certs(certs_path, ca_subject, ca_private_key):
|
||||
# Generate Server and Client Private Keys
|
||||
server_private_key = ec.generate_private_key(ec.SECP256R1())
|
||||
client_private_key = ec.generate_private_key(ec.SECP256R1())
|
||||
|
||||
# Generate Server and Client Public Keys
|
||||
server_public_key = server_private_key.public_key()
|
||||
client_public_key = client_private_key.public_key()
|
||||
|
||||
# Generate Server and Client Certificates
|
||||
for entity_type in ["server", "client"]:
|
||||
public_key = server_public_key if entity_type == "server" else client_public_key
|
||||
def generate_cert(entity_type, ca_subject, ca_private_key, rest_host, certs_path):
|
||||
# Generate Key pair
|
||||
private_key = ec.generate_private_key(ec.SECP256R1())
|
||||
public_key = private_key.public_key()
|
||||
|
||||
# Generate Certificates
|
||||
if isinstance(ca_subject, x509.Name):
|
||||
subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, f"cln rest {entity_type}")])
|
||||
|
||||
cert_builder = create_cert_builder(subject, ca_subject, public_key, rest_host)
|
||||
cert = cert_builder.sign(ca_private_key, hashes.SHA256())
|
||||
else:
|
||||
ca_subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u"cln Root REST CA")])
|
||||
ca_private_key, ca_public_key = private_key, public_key
|
||||
cert_builder = create_cert_builder(ca_subject, ca_subject, ca_public_key, rest_host)
|
||||
cert = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(ca_subject)
|
||||
.public_key(public_key)
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.datetime.utcnow())
|
||||
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=10 * 365)) # Ten years validity
|
||||
.add_extension(x509.SubjectAlternativeName([x509.DNSName(u"cln"), x509.DNSName(u'localhost'), x509.IPAddress(ipaddress.IPv4Address(u'127.0.0.1'))]), critical=False)
|
||||
cert_builder
|
||||
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
||||
.sign(ca_private_key, hashes.SHA256())
|
||||
)
|
||||
|
||||
# Serialize Server and Client certificates and write to disk
|
||||
with open(os.path.join(certs_path, f"{entity_type}.pem"), "wb") as f:
|
||||
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
||||
|
||||
# Serialize Private Keys (Server)
|
||||
with open(os.path.join(certs_path, "server-key.pem"), "wb") as f:
|
||||
f.write(server_private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
))
|
||||
|
||||
# Serialize Private Keys (Client)
|
||||
with open(os.path.join(certs_path, "client-key.pem"), "wb") as f:
|
||||
f.write(client_private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
))
|
||||
os.makedirs(certs_path, exist_ok=True)
|
||||
save_cert(entity_type, cert, private_key, certs_path)
|
||||
return ca_subject, ca_private_key
|
||||
|
||||
|
||||
def generate_certs(plugin, certs_path):
|
||||
ca_subject, ca_private_key = generate_ca_cert(certs_path)
|
||||
generate_client_server_certs(certs_path, ca_subject, ca_private_key)
|
||||
def generate_certs(plugin, rest_host, certs_path):
|
||||
ca_subject, ca_private_key = generate_cert("ca", None, None, rest_host, certs_path)
|
||||
generate_cert("client", ca_subject, ca_private_key, rest_host, certs_path)
|
||||
generate_cert("server", ca_subject, ca_private_key, rest_host, certs_path)
|
||||
plugin.log(f"Certificates Generated!", "debug")
|
||||
|
|
|
@ -8,4 +8,4 @@ plugin.add_option(name="rest-protocol", default="https", description="REST serve
|
|||
plugin.add_option(name="rest-host", default="127.0.0.1", description="REST server host", opt_type="string", deprecated=False)
|
||||
plugin.add_option(name="rest-port", default=None, description="REST server port to listen", opt_type="int", deprecated=False)
|
||||
plugin.add_option(name="rest-cors-origins", default="*", description="Cross origin resource sharing origins", opt_type="string", deprecated=False, multi=True)
|
||||
plugin.add_option(name="rest-csp", default="default-src 'self'; font-src 'self'; img-src 'self' data:; frame-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';", description="Content security policy (CSP) for the server", opt_type="string", deprecated=False, multi=True)
|
||||
plugin.add_option(name="rest-csp", default="default-src 'self'; font-src 'self'; img-src 'self' data:; frame-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';", description="Content security policy (CSP) for the server", opt_type="string", deprecated=False, multi=False)
|
||||
|
|
|
@ -23,7 +23,7 @@ class ListMethodsResource(Resource):
|
|||
return response
|
||||
|
||||
except Exception as err:
|
||||
plugin.log(f"Error: {err}", "error")
|
||||
plugin.log(f"Error: {err}", "info")
|
||||
return json5.loads(str(err)), 500
|
||||
|
||||
|
||||
|
@ -37,25 +37,25 @@ class RpcMethodResource(Resource):
|
|||
def post(self, rpc_method):
|
||||
"""Call any valid core lightning method (check list-methods response)"""
|
||||
try:
|
||||
is_valid_rune = verify_rune(plugin, request)
|
||||
rune = request.headers.get("rune", None)
|
||||
rpc_method = request.view_args.get("rpc_method", None)
|
||||
rpc_params = request.form.to_dict() if not request.is_json else request.get_json() if len(request.data) != 0 else {}
|
||||
|
||||
if "error" in is_valid_rune:
|
||||
plugin.log(f"Error: {is_valid_rune}", "error")
|
||||
raise Exception(is_valid_rune)
|
||||
try:
|
||||
is_valid_rune = verify_rune(plugin, rune, rpc_method, rpc_params)
|
||||
if "error" in is_valid_rune:
|
||||
plugin.log(f"Error: {is_valid_rune}", "error")
|
||||
raise Exception(is_valid_rune)
|
||||
|
||||
except Exception as err:
|
||||
return json5.loads(str(err)), 401
|
||||
|
||||
try:
|
||||
return call_rpc_method(plugin, rpc_method, rpc_params), 201
|
||||
|
||||
except Exception as err:
|
||||
plugin.log(f"Error: {err}", "info")
|
||||
return json5.loads(str(err)), 500
|
||||
|
||||
except Exception as err:
|
||||
return json5.loads(str(err)), 401
|
||||
|
||||
try:
|
||||
if request.is_json:
|
||||
if len(request.data) != 0:
|
||||
payload = request.get_json()
|
||||
else:
|
||||
payload = {}
|
||||
else:
|
||||
payload = request.form.to_dict()
|
||||
return call_rpc_method(plugin, rpc_method, payload), 201
|
||||
|
||||
except Exception as err:
|
||||
plugin.log(f"Error: {err}", "error")
|
||||
return json5.loads(str(err)), 500
|
||||
return f"Unable to parse request: {err}", 500
|
||||
|
|
|
@ -1,18 +1,56 @@
|
|||
import json5
|
||||
import re
|
||||
import json
|
||||
import ipaddress
|
||||
|
||||
|
||||
CERTS_PATH, REST_PROTOCOL, REST_HOST, REST_PORT, REST_CSP, REST_CORS_ORIGINS = "", "", "", "", "", []
|
||||
|
||||
|
||||
def validate_ip4(ip_str):
|
||||
try:
|
||||
# Create an IPv4 address object.
|
||||
ipaddress.IPv4Address(ip_str)
|
||||
return True
|
||||
except ipaddress.AddressValueError:
|
||||
return False
|
||||
|
||||
|
||||
def validate_ip6(ip_str):
|
||||
try:
|
||||
# Create an IPv6 address object.
|
||||
ipaddress.IPv6Address(ip_str)
|
||||
return True
|
||||
except ipaddress.AddressValueError:
|
||||
return False
|
||||
|
||||
|
||||
def validate_port(port):
|
||||
try:
|
||||
# Ports <= 1024 are reserved for system processes
|
||||
return 1024 <= port <= 65535
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def set_config(options):
|
||||
if 'rest-port' not in options:
|
||||
return "`rest-port` option is not configured"
|
||||
global CERTS_PATH, REST_PROTOCOL, REST_HOST, REST_PORT, REST_CSP, REST_CORS_ORIGINS
|
||||
CERTS_PATH = str(options["rest-certs"])
|
||||
REST_PROTOCOL = str(options["rest-protocol"])
|
||||
REST_HOST = str(options["rest-host"])
|
||||
|
||||
REST_PORT = int(options["rest-port"])
|
||||
if validate_port(REST_PORT) is False:
|
||||
return f"`rest-port` {REST_PORT}, should be a valid available port between 1024 and 65535."
|
||||
|
||||
REST_HOST = str(options["rest-host"])
|
||||
if REST_HOST != "localhost" and validate_ip4(REST_HOST) is False and validate_ip6(REST_HOST) is False:
|
||||
return f"`rest-host` should be a valid IP."
|
||||
|
||||
REST_PROTOCOL = str(options["rest-protocol"])
|
||||
if REST_PROTOCOL != "http" and REST_PROTOCOL != "https":
|
||||
return f"`rest-protocol` can either be http or https."
|
||||
|
||||
CERTS_PATH = str(options["rest-certs"])
|
||||
REST_CSP = str(options["rest-csp"])
|
||||
cors_origins = options["rest-cors-origins"]
|
||||
REST_CORS_ORIGINS.clear()
|
||||
|
@ -30,13 +68,13 @@ def call_rpc_method(plugin, rpc_method, payload):
|
|||
else:
|
||||
plugin.log(f"{response}", "debug")
|
||||
if '"result":' in str(response).lower():
|
||||
# Use json5.loads ONLY when necessary, as it increases processing time significantly
|
||||
# Use json5.loads ONLY when necessary, as it increases processing time
|
||||
return json.loads(response)["result"]
|
||||
else:
|
||||
return response
|
||||
|
||||
except Exception as err:
|
||||
plugin.log(f"Error: {err}", "error")
|
||||
plugin.log(f"Error: {err}", "info")
|
||||
if "error" in str(err).lower():
|
||||
match_err_obj = re.search(r'"error":\{.*?\}', str(err))
|
||||
if match_err_obj is not None:
|
||||
|
@ -48,23 +86,10 @@ def call_rpc_method(plugin, rpc_method, payload):
|
|||
raise Exception(err)
|
||||
|
||||
|
||||
def verify_rune(plugin, request):
|
||||
rune = request.headers.get("rune", None)
|
||||
|
||||
def verify_rune(plugin, rune, rpc_method, rpc_params):
|
||||
if rune is None:
|
||||
raise Exception('{ "error": {"code": 403, "message": "Not authorized: Missing rune"} }')
|
||||
|
||||
if request.is_json:
|
||||
if len(request.data) != 0:
|
||||
rpc_params = request.get_json()
|
||||
else:
|
||||
rpc_params = {}
|
||||
else:
|
||||
rpc_params = request.form.to_dict()
|
||||
|
||||
# None, if this isn't present.
|
||||
rpc_method = request.view_args.get("rpc_method")
|
||||
|
||||
return call_rpc_method(plugin, "checkrune",
|
||||
{"rune": rune,
|
||||
"method": rpc_method,
|
||||
|
|
Loading…
Add table
Reference in a new issue