Merge branch 'development' into RTOP-26-Login-RestAPI

This commit is contained in:
lucasbutzke
2025-07-16 14:14:19 -04:00
7 changed files with 181 additions and 42 deletions
+3
View File
@@ -33,8 +33,11 @@ webserver/core/glue_generator
webserver/core/openplc
webserver/iec2c
webserver/st_optimizer
webserver/active_program
utils/libmodbus_src/src/.libs/*
utils/libmodbus_src/tests/.libs/*
webserver/scripts/openplc_driver
webserver/scripts/openplc_platform
# Runtime directories
.venv
+3 -3
View File
@@ -92,9 +92,9 @@ function install_py_deps {
python3 -m venv "$VENV_DIR"
"$VENV_DIR/bin/python3" -m pip install --upgrade pip
if [ "$1" == "neuron" ]; then
"$VENV_DIR/bin/python3" -m pip install flask==2.2.5 werkzeug==2.2.2 flask-login==0.6.2 pyserial pymodbus==2.5.3
"$VENV_DIR/bin/python3" -m pip install flask==2.2.5 werkzeug==2.2.2 flask-login==0.6.2 pyserial pymodbus==2.5.3 cryptography
else
"$VENV_DIR/bin/python3" -m pip install flask==2.3.3 werkzeug==2.3.7 flask-login==0.6.2 pyserial pymodbus==2.5.3
"$VENV_DIR/bin/python3" -m pip install flask==2.3.3 werkzeug==2.3.7 flask-login==0.6.2 pyserial pymodbus==2.5.3 cryptography
fi
python3 -m pip install pymodbus==2.5.3
}
@@ -278,7 +278,7 @@ if [ "$1" == "win" ]; then
#Setting up venv
python3 -m venv "$VENV_DIR"
"$VENV_DIR/bin/python3" get-pip3.py
"$VENV_DIR/bin/python3" -m pip install flask==2.3.3 werkzeug==2.3.7 flask-login==0.6.2 pyserial pymodbus==2.5.3
"$VENV_DIR/bin/python3" -m pip install flask==2.3.3 werkzeug==2.3.7 flask-login==0.6.2 pyserial pymodbus==2.5.3 cryptography
echo ""
echo "[MATIEC COMPILER]"
+1
View File
@@ -2,3 +2,4 @@ Flask==1.0.2
Flask-Login==0.4.1
pyserial==3.4
pymodbus==2.2.0
cryptography
+117
View File
@@ -0,0 +1,117 @@
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
import datetime
import ipaddress
import os
class CertGen():
"""
Generates a self-signed TLS certificate and private key.
Args:
hostname (str): The common name (CN) for the certificate.
ip_addresses (list, optional): A list of IP addresses to include in the SAN extension.
cert_file (str): The filename for the certificate (PEM format).
key_file (str): The filename for the private key (PEM format).
"""
def __init__(self, hostname, ip_addresses=None):
self.hostname = hostname
self.ip_addresses = ip_addresses
# Certificate validity
self.now = datetime.datetime.utcnow()
# Subject and Issuer
self.subject = self.issuer = x509.Name([
# x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"), # TODO get device country
# x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"openplc"),
# x509.NameAttribute(NameOID.LOCALITY_NAME, u"openplc"),
# x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Autonomy"),
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
])
# Subject Alternative Names (SAN)
self.alt_names = [x509.DNSName(hostname)]
if ip_addresses:
for addr in ip_addresses:
self.alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
self.san_extension = x509.SubjectAlternativeName(self.alt_names)
def generate_key(self):
# Generate our key
self.key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
def generate_self_signed_cert(self, cert_file="cert.pem", key_file="key.pem"):
print(f"Generating self-signed certificate for {self.hostname}...")
self.generate_key()
cert = (
x509.CertificateBuilder()
.subject_name(self.subject)
.issuer_name(self.issuer)
.public_key(self.key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(self.now)
.not_valid_after(self.now + datetime.timedelta(days=365)) # Valid for 1 year
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.add_extension(self.san_extension, critical=False)
.sign(self.key, hashes.SHA256(), default_backend())
)
# Write our certificate and key to disk
with open(cert_file, "wb+") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
with open(key_file, "wb+") as f:
f.write(self.key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption()
))
print(f"Certificate saved to {cert_file}")
print(f"Private key saved to {key_file}")
# TODO add a function to update the certificate on the client before expiration
def is_certificate_valid(self, cert_file):
"""
Checks if a certificate is valid (not expired and not yet valid).
Args:
cert_file (str): The path to the certificate file (PEM format).
Returns:
bool: True if the certificate is currently valid, False otherwise.
"""
if not os.path.exists(cert_file):
print(f"Certificate file not found: {cert_file}")
return False
try:
with open(cert_file, "rb") as f:
cert_data = f.read()
cert = x509.load_pem_x509_certificate(cert_data, default_backend())
now = datetime.datetime.utcnow()
if now < cert.not_valid_before_utc:
print(f"Certificate is not yet valid. Valid from: {cert.not_valid_before}")
return False
if now > cert.not_valid_after_utc:
print(f"Certificate has expired. Expired on: {cert.not_valid_after}")
return False
print(f"Certificate is valid. Expires on: {cert.not_valid_after_utc}")
return True
except Exception as e:
print(f"Error loading or parsing certificate: {e}")
return False
+2 -2
View File
@@ -89,7 +89,7 @@ def restapi_plc_get(command):
result = _handler_callback_get(command, data)
return jsonify(result), 200
except Exception as e:
print(f"Error in restapi_start_plc: {e}")
print(f"Error in restapi_plc_get: {e}")
return jsonify({"error": str(e)}), 500
@restapi_bp.route("/<command>", methods=["POST"])
@@ -105,5 +105,5 @@ def restapi_plc_post(command):
result = _handler_callback_post(command, data)
return jsonify(result), 200
except Exception as e:
print(f"Error in restapi_start_plc_post: {e}")
print(f"Error in restapi_plc_post: {e}")
return jsonify({"error": str(e)}), 500
-31
View File
@@ -1,31 +0,0 @@
FROM debian:bullseye-20240722
COPY . /workdir
WORKDIR /workdir
RUN mkdir /docker_persistent
VOLUME /docker_persistent
# setup docker
RUN ./install.sh docker \
&& touch /docker_persistent/mbconfig.cfg \
&& touch /docker_persistent/persistent.file \
&& mkdir /docker_persistent/st_files \
&& cp /workdir/webserver/openplc.db /docker_persistent/openplc.db \
&& mv /workdir/webserver/openplc.db /workdir/webserver/openplc_default.db \
&& cp /workdir/webserver/dnp3.cfg /docker_persistent/dnp3.cfg \
&& mv /workdir/webserver/dnp3.cfg /workdir/webserver/dnp3_default.cfg \
&& cp -r /workdir/webserver/st_files/ /docker_persistent/st_files/ \
&& mv /workdir/webserver/st_files /workdir/webserver/st_files_default \
&& cp /workdir/webserver/active_program /docker_persistent/active_program \
&& mv /workdir/webserver/active_program /workdir/webserver/active_program_default \
&& ln -s /docker_persistent/mbconfig.cfg /workdir/webserver/mbconfig.cfg \
&& ln -s /docker_persistent/persistent.file /workdir/webserver/persistent.file \
&& ln -s /docker_persistent/openplc.db /workdir/webserver/openplc.db \
&& ln -s /docker_persistent/dnp3.cfg /workdir/webserver/dnp3.cfg \
&& ln -s /docker_persistent/st_files /workdir/webserver/st_files \
&& ln -s /docker_persistent/active_program /workdir/webserver/active_program
ENTRYPOINT ["./start_openplc.sh"]
+55 -6
View File
@@ -14,8 +14,10 @@ import sys
import ctypes
import socket
import mimetypes
import ssl
import threading
import flask
import flask
import flask_login
from credentials import CertGen
@@ -931,7 +933,7 @@ def upload_program():
if (prog_file.filename == ''):
return draw_blank_page() + "<h2>Error</h2><p>You need to select a file to be uploaded!<br><br>Use the back-arrow on your browser to return</p></div></div></div></body></html>"
# TODO colocar em outra funçao
# TODO realocate to another function
filename = str(random.randint(1,1000000)) + ".st"
prog_file.save(os.path.join('st_files', filename))
@@ -2579,9 +2581,41 @@ def run_https():
register_callback_get(restapi_callback_get)
register_callback_post(restapi_callback_post)
try:
# CertGen class is used to generate SSL certificates and verify their validity
cert_gen = CertGen(hostname=HOSTNAME, ip_addresses=["127.0.0.1"])
# Generate certificate if it doesn't exist
if not os.path.exists(CERT_FILE) or not os.path.exists(KEY_FILE):
cert_gen.generate_self_signed_cert(cert_file=CERT_FILE, key_file=KEY_FILE)
# Verify expiration date
elif cert_gen.is_certificate_valid(CERT_FILE):
print(cert_gen.generate_self_signed_cert(cert_file=CERT_FILE, key_file=KEY_FILE))
# Credentials already created
else:
print("Credentials already generated!")
try:
context = (CERT_FILE, KEY_FILE)
app_restapi.run(debug=False, host='0.0.0.0', threaded=True, port=8443, ssl_context=context)
except KeyboardInterrupt as e:
print(f"Exiting OpenPLC Webserver...{e}")
openplc_runtime.stop_runtime()
except Exception as e:
print(f"An error occurred: {e}")
openplc_runtime.stop_runtime()
except:
print("An unexpected error occurred.")
# TODO handle file error
except FileNotFoundError as e:
print(f"Could not find SSL credentials! {e}")
except ssl.SSLError as e:
print(f"SSL credentials FAIL! {e}")
def run_http():
#Load information about current program on the openplc_runtime object
file = open("active_program", "r")
st_file = file.read()
with open("active_program", "r") as file:
st_file = file.read()
st_file = st_file.replace('\r','').replace('\n','')
database = "openplc.db"
@@ -2611,10 +2645,25 @@ def run_https():
time.sleep(1)
configure_runtime()
monitor.parse_st(openplc_runtime.project_file)
app.run(debug=False, host='0.0.0.0', threaded=True, port=8080)
try:
app.run(debug=False, host='0.0.0.0', threaded=True, port=8080)
except KeyboardInterrupt as e:
print(f"Exiting OpenPLC Webserver...{e}")
openplc_runtime.stop_runtime()
except Exception as e:
print(f"An error occurred: {e}")
openplc_runtime.stop_runtime()
except:
print("An unexpected error occurred.")
except Error as e:
print("error connecting to the database" + str(e))
else:
print("error connecting to the database")
if __name__ == '__main__':
# Running webserver and RestAPI in separate threads
threading.Thread(target=run_http).start()
threading.Thread(target=run_https).start()