mirror of
https://github.com/thiagoralves/OpenPLC_v3.git
synced 2026-09-23 03:14:20 +08:00
Merge branch 'development' into RTOP-26-Login-RestAPI
This commit is contained in:
@@ -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
|
||||
@@ -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]"
|
||||
|
||||
@@ -2,3 +2,4 @@ Flask==1.0.2
|
||||
Flask-Login==0.4.1
|
||||
pyserial==3.4
|
||||
pymodbus==2.2.0
|
||||
cryptography
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user