summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
m---------Mosquitto_CA_and_Certs0
-rw-r--r--Readme.md18
-rwxr-xr-xca_maker_erg.sh355
-rwxr-xr-xclient_maker.sh6
-rwxr-xr-xclient_maker_erg.sh316
-rw-r--r--config.py.example21
-rw-r--r--main.py298
-rw-r--r--sequence_diagram.pngbin0 -> 12924 bytes
-rw-r--r--sequence_diagram.txt9
-rw-r--r--state_diagram.pngbin0 -> 23760 bytes
-rw-r--r--state_diagram.txt27
-rw-r--r--umqttsimple.py432
12 files changed, 1150 insertions, 332 deletions
diff --git a/Mosquitto_CA_and_Certs b/Mosquitto_CA_and_Certs
new file mode 160000
+Subproject c0dd180b62fd30fd5fa94118b169f6282862cd6
diff --git a/Readme.md b/Readme.md
index 5bd405f..0c0f5c7 100644
--- a/Readme.md
+++ b/Readme.md
@@ -4,9 +4,9 @@ and feeds it to MQTT on a wireless connection.
Before you can use it you need to:
- connect onewire sensor to Pico,
-- set up MQTT broker on a server uneder your control,
+- set up MQTT broker on a server,
- optionally, but highly advisable, setup ntpd server in local network,
-- set up certificates,
+- set up certificates (check out the MQTT_for_pie repo),
- provide settings in config.py. Skeleton in config.py.example
Notes:
@@ -14,8 +14,16 @@ Notes:
In order to test Pico, best use Thonny.
At the time of writing this (Jun 2024), you won't be able to see live
interpreter in PyCharm
-Device path is /dev/ttyACM0
-To connect to it, add your user to dialout group.
+You will need kernel config options enabled:
+ (Most likely:)
+ CONFIG_USB_SERIAL_GENERIC
+ CONFIG_USB_SERIAL_SIMPLE
+ (Definitely:)
+ CONFIG_USB_ACM
+
+Device path should be /dev/ttyACM0
+
+To connect to it, add your user to dialout group.
Alternatively change permissions on that device to something like:
chmod o+rw /dev/ttyACM0
Or change owner on that device to your user
@@ -27,7 +35,7 @@ led = Pin("LED", Pin.OUT)
Before using tls (mosquitto, requests) you'll need to set time, for instance:
import ntptime
ntptime.settime()
-Be aware that default timeout on a socket there is 1 second, which may not be enough.
+Be aware that default timeout on a socket is 1 second, which may not be enough.
I'm using usocket directly so that I can set timeout to a larger value.
The hard part is to deal with timeout from ntp server.
I managed to cut down timeouts drastically by running my own ntp server on local network.
diff --git a/ca_maker_erg.sh b/ca_maker_erg.sh
new file mode 100755
index 0000000..db53278
--- /dev/null
+++ b/ca_maker_erg.sh
@@ -0,0 +1,355 @@
+#!/bin/bash
+usage() {
+ echo "Script name: $0";
+ echo "";
+ echo "Usage: $0 --key-type [ RSA | EC | EX25519 ]";
+ echo " --curve [ P-256 | P-384 | P512 ]";
+ echo " --rsa-bits [ 2048 | 4096 ]";
+ echo " --days <n>";
+ echo "";
+ echo "Options: ";
+ echo " --key-type [ RSA | EC | EC25519 ]";
+ echo " Note: Pico W can use EC or RSA keys only, not EC25519,";
+ echo " and Tasmota can only use 2048 bit RSA keys.";
+ echo " --curve [ P-256 | P-384 | P512 ]";
+ echo " --rsa-bits [ 2048 | 4096 ]";
+ echo " --days [ <integer> ]";
+ echo " --dir [ <mosquitto directory> ]";
+ echo " --output-format [ PEM | DER ]";
+ echo " --server_only [ True | False ]";
+ echo " --subjectAltName <subjectAltName>";
+ echo " Example subjectAltName: DNS.1:example.com,DNS.2:192.168.1.167,IP.1:192.168.1.167'";
+}
+
+# This creates a very slimline CA and Server cert with a full SAN multihost server.
+# The Mosquitto server requires keys and certs in PEM format, but it also exports
+# the CA Cert in DER for use in clients such as the Pico W
+TEMP="$(getopt -o h --long help,key-type:,curve:,rsa-bits:,ca_valid_for:,days:,dir:,output-format:,server_only:,subjectAltName: -- "$@")"
+
+eval set -- "$TEMP"
+
+if [ $# == 0 ] ; then
+ usage;
+ exit 1;
+fi
+
+mosquitto_dir="/home/erg/cert_test_1"
+key_type='EC'
+curve='P-256'
+rsa_bits='2048'
+# Choice to encrypt the CA Key or not? either BLANK or cypher (preferably aes-256-cbc)
+encryption=''
+# encryption='-aes-256-cbc'
+output_format='der'
+# Multiple DNS names and IP Addresses. Note mbedtls can't currently use data from IP fields,
+# but can read an IP from a DNS field, to the same effect.
+# Use as many unique names as you need, in the format DNS.1, DNS.2, DNS.3, IP.1, IP.2 etc.
+# subjectAltName='DNS.1:server_name,DNS.2:server_name.example.com,DNS.3:192.168.1.1,IP.1:192.168.1.1'
+subjectAltName='DNS.1:example.com,DNS.2:192.168.0.167,IP.1:192.168.0.167'
+ca_valid_for=3650
+certs_valid_for=365
+server_only="True"
+
+while true
+do
+ case "$1" in
+ -h|--help)
+ usage
+ exit 0;
+ ;;
+ # Note: Pico W can use EC or RSA keys only, not EC25519, and Tasmota can only use 2048 bit RSA keys.
+ # Default choice of Key Types: RSA || EC || ED25519
+ --key-type) key_type="$2"; shift 2;;
+ # Choice of NIST Curves for EC Keys: P-256 || P-384 || P-521
+ --curve) curve="$2"; shift 2;;
+ # Choice of Bits for RSA Keys: 2048 || 4096
+ --rsa-bits) rsa_bits="$2"; shift 2;;
+ # How many days is the CA Cert valid for:
+ --ca_valid_for) ca_valid_for="$2"; shift;;
+ # How many days is the Cert valid for:
+ --days) certs_valid_for="$2"; shift 2;;
+ # Mosquitto directory:
+ --dir) mosquitto_dir="$2"; shift 2;;
+ # Output format: PEM || DER
+ --output-format) output_format="$2"; shift 2;;
+ # Only generate server certificate and key, not the CA. Defaults to True.
+ --server_only) server_only="$2";shift 2;;
+ --subjectAltName) subjectAltName="$2";shift 2;;
+ -- ) shift; break;;
+ * ) break;;
+ esac
+done
+
+echo "subjectAltName you specified: $subjectAltName"
+
+if [ "$server_only" == "True" ] || [ "$server_only" == "true" ]|| [ "$server_only" == "Yes" ] || [ "$server_only" == 'yes' ]; then
+ server_only='True'
+else
+ server_only='False'
+fi
+
+# Mosquitto subdirectories:
+mosquitto_dir_ca="${mosquitto_dir}/certs/CA"
+mosquitto_dir_server="${mosquitto_dir}/certs/server"
+mosquitto_dir_csr="${mosquitto_dir}/certs/csr_files"
+ssl_ca_crt="${mosquitto_dir_ca}/ca_crt.${output_format}"
+ssl_ca_key="${mosquitto_dir_ca}/ca_key.${output_format}"
+
+# NOTE: If you need to create one or more client Keys and Certs in either PEM or DER format,
+# you can call the 'client_maker' script one or more times at the bottom of this script, so
+# just scroll right to the end to add the details. You can call 'client_maker' independantly
+# whenever you need more clients.
+
+#############################################################
+# Check passed option values sane #
+#############################################################
+echo "Checking if CA dir exists: $mosquitto_dir_ca ..."
+if [ ! -d $mosquitto_dir_ca ];then
+ echo "Certificate authority folder does not exist, exiting"
+ exit 1
+fi
+
+# Check CA key and cert exist:
+echo "Checking CA key and cert exist: ..."
+if [ ! -f ${ssl_ca_crt} ]; then # I: Double quote to prevent globbing and word splitting.
+ echo "CA certificate does not exist, exiting!"
+ exit 1
+fi
+if [ ! -f "${mosquitto_dir_ca}/ca_key.pem" ] && [ ! -f "${mosquitto_dir_ca}/ca_key.der" ]; then
+ echo "CA key does not exist, exiting!"
+ exit 1
+fi
+# Check CA certificate is still valid:
+# Date format we get from openssl:
+# Nov 18 13:47:56 2034 GMT
+# %b %d %H:%M:%S %Y %Z
+# compare dates from the CA cert and current one:
+echo "Checking CA certificate valid ..."
+cert_valid_until=$(openssl x509 -enddate -noout -in "${ssl_ca_crt}" | cut -d'=' -f2)
+echo "CA certificate valid until: ${cert_valid_until}"
+if [ ! $(($(date -d "$cert_valid_until" +"%s") > $(date +"%s"))) ];then
+ echo "CA certificate expired, exiting!"
+ exit 1
+fi
+# Check key type:
+if [ "$key_type" != 'EC' ] && [ "$key_type" != 'RSA' ] && [ "$key_type" != 'EC25519' ]; then
+ echo "Wrong key type specified: '$key_type'"
+ echo "Valid keys: [ EC | RSA | EC25519 ]"
+ usage
+ exit 1;
+fi
+
+# Check curve:
+if [ "$curve" != 'P-256' ] && [ "$curve" != 'P-384' ] && [ "$curve" != 'P-521' ]; then
+ echo "Wrong NIST curve specified: '$curve'"
+ echo "Curve must be one of: [ P-256 | P-384 | P-521 ]"
+ usage
+ exit 1;
+fi
+
+# Check RSA bits:
+if [ "$rsa_bits" != '2048' ] && [ "$rsa_bits" != '4096' ]; then
+ echo "Wrong RSA bits specified: '$rsa_bits'"
+ echo "Must be one of: [ 2048 | 4096 ]"
+ usage
+ exit 1;
+fi
+
+# Check days parameter is a number:
+if [ -n "$certs_valid_for" ] && [ "$certs_valid_for" -eq "$certs_valid_for" ] 2>/dev/null; then
+ echo "Certificate will be valid for: '$certs_valid_for' days"
+else
+ echo "Not a number: $certs_valid_for"
+ usage
+ exit 1;
+fi
+
+# Check output format:
+if [ ${output_format} == 'PEM' ] || [ ${output_format} == 'pem' ]; then # I: Double quote to prevent globbing and word splitting.
+ output_format="pem"
+elif [ $output_format == 'DER' ] || [ $output_format == 'der' ]; then # I: Double quote to prevent globbing and word splitting.
+ output_format="der"
+else echo "Incorrect certificate format type specified: '$output_format'";
+ usage
+ exit 1;
+fi
+
+
+############################################################
+# End of user defined variables
+############################################################
+
+
+# Set the algorithm
+algorithm="-algorithm ${key_type}"
+
+# Set the specific pkeyopt for the chosen algorithm (BLANK for ED25519)
+if [ "${key_type}" == "EC" ]; then
+ echo 'Creating EC Key ...'
+ pkeyopt="-pkeyopt ec_paramgen_curve:${curve}"
+elif [ "${key_type}" == "RSA" ]; then
+ echo 'Creating RSA Key ...'
+ pkeyopt="-pkeyopt rsa_keygen_bits:${rsa_bits}"
+elif [ "${key_type}" == "ED25519" ]; then
+ echo 'Creating ED25519 Key'
+ pkeyopt=""
+else
+ echo 'Key Type not found!'
+ exit 1
+fi
+
+############################################################
+# Backup existing certs and create dir structure
+############################################################
+
+# if certs dir already exists, rename it so we don't overwrite anything important
+# but if it doesn't, then redirect the 'No such file or directory' error to null
+time_stamp=$(date +"%Y-%m-%d_%H-%M")
+if [ ! -d "${mosquitto_dir_ca}" ]; then
+ mkdir -p "${mosquitto_dir_ca}-${time_stamp}" 2>/dev/null
+else
+ cp "${mosquitto_dir_ca}" "${mosquitto_dir_ca}-${time_stamp}" 2>/dev/null
+fi
+
+if [ ! -d "${mosquitto_dir_server}" ]; then
+ mkdir -p "${mosquitto_dir_server}-${time_stamp}" 2>/dev/null
+else
+ cp "${mosquitto_dir_server}" "${mosquitto_dir_server}-$time_stamp" 2>/dev/null
+fi
+
+# create a sensible directory structure to store everything if not exists:
+mkdir -p "${mosquitto_dir}"/certs/{DH,CA,server,clients,csr_files}
+
+
+###########################################################
+# dhparamfile Creation
+###########################################################
+
+# Output DH parameters for safe prime group ffdhe2048
+if [ "$server_only" == "True" ]; then
+openssl genpkey \
+ -genparam \
+ -algorithm DH \
+ -pkeyopt group:ffdhe2048 \
+ -out "$mosquitto_dir/certs/DH/dhp_ffdhe2048.pem"
+fi
+
+###########################################################
+# CA Creation
+###########################################################
+
+if [ "${server_only}" == 'True' ]; then
+openssl genpkey \
+ $algorithm $pkeyopt \
+ -outform pem \
+ -out "$mosquitto_dir_ca/ca_key.pem" "$encryption"
+fi
+
+
+# Self sign the CA cert
+
+if [ "${server_only}" == 'True' ]; then
+openssl req \
+ -x509 \
+ -new \
+ -key "$mosquitto_dir_ca/ca_key.pem" \
+ -days $ca_valid_for\
+ -subj '/CN=MQTT CA' \
+ -outform pem \
+ -out "$mosquitto_dir_ca/ca_crt.pem"
+fi
+
+###########################################################
+# Server Creation
+###########################################################
+
+# Create the Key
+openssl genpkey \
+ $algorithm $pkeyopt \
+ -outform pem \
+ -out "$mosquitto_dir_server/server_key.pem"
+
+# Create the certificate signing request (CSR)
+openssl req \
+ -new \
+ -subj "/CN=MQTT Server" \
+ -addext "subjectAltName = ${subjectAltName}" \
+ -nodes \
+ -key "$mosquitto_dir_server/server_key.pem" \
+ -out "$mosquitto_dir_server/server_req.csr"
+
+# Sign and authenticate it with the CA
+# We use copy_extensions to include the subjectAltNames from the CSR in the Server Cert
+echo "Sign and authenticate CSR with the CA ..."
+openssl x509 \
+ -req \
+ -in "$mosquitto_dir_server/server_req.csr" \
+ -copy_extensions copy \
+ -CA "${mosquitto_dir_ca}/ca_crt.pem" \
+ -CAkey "${mosquitto_dir_ca}/ca_key.pem" \
+ -CAcreateserial \
+ -days "${certs_valid_for}" \
+ -outform pem \
+ -out "$mosquitto_dir_server/server_crt.pem"
+
+
+###########################################################
+# Key and Cert Checking
+###########################################################
+
+# Examine the key to check that it looks OK
+openssl pkey \
+ -in "$mosquitto_dir_server/server_key.pem" \
+ -text \
+ -noout
+
+# Examine the CSR
+openssl req \
+ -in "$mosquitto_dir_server/server_req.csr" \
+ -noout \
+ -text
+
+# Validate the CA certificate
+openssl x509 \
+ -in "$mosquitto_dir_server/server_crt.pem" \
+ -text \
+ -noout
+
+#clean up after the server cert creation
+mv "$mosquitto_dir_server/server_req.csr" "$mosquitto_dir_csr"
+
+###########################################################
+# Key Export and Read Permissions fix
+###########################################################
+
+#We need to export a copy of our CA Certificate in DER format for micropython.
+openssl x509 \
+ -in "$mosquitto_dir_ca/ca_crt.pem" \
+ -out "$mosquitto_dir_ca/ca_crt.der" \
+ -outform "$output_format"
+
+# and save a copy of it somewhere useful
+cp \
+ "$mosquitto_dir_ca/ca_crt.der" \
+ "$mosquitto_dir/certs/clients"
+
+# but pem based clients need the pem version of it too
+cp \
+ "$mosquitto_dir_ca/ca_crt.pem" \
+ "$mosquitto_dir/certs/clients"
+
+# We also need to give read access to server_key.pem so it can be used by Mosquitto
+chmod 644 "$mosquitto_dir_server/server_key.pem"
+
+
+# move the ca_maker script to certs/CA and remove its execute permissions
+# to reduce the probability of running it by accident again
+# and overwriting everything in your certs directory
+#mv $mosquitto_dir/ca_maker $mosquitto_dir/certs/CA/ca_maker
+#chmod -x $mosquitto_dir/certs/CA/ca_maker
+
+# auto run the client_maker pem/der username
+# ./client_maker der pico
+#client_maker pem user2
+#client_maker pem user3
+#client_maker der user4
diff --git a/client_maker.sh b/client_maker.sh
index 983bf1a..67dfa98 100755
--- a/client_maker.sh
+++ b/client_maker.sh
@@ -6,7 +6,7 @@
# The script WILL NOT DELETE ANYTHING - it just renames any certs dir that it finds
# You will probably need to change this for your system
-mosquitto_dir="/etc/mosquitto"
+mosquitto_dir="/home/erg/cert_test"
# Note: Pico W can use EC or RSA keys only, not EC25519, and Tasmota can only use 2048 bit RSA keys.
@@ -47,7 +47,7 @@ fi
algorithm="-algorithm ${key_type}"
# Set the specific pkeyopt for the chosen algorithm (BLANK for ED25519)
-if [ "${key_type}" == "EC" ]; then
+if [ "${key_type}" == "EC" ]; then
echo 'Create EC Key'
pkeyopt="-pkeyopt ec_paramgen_curve:${curve}"
elif [ "${key_type}" == "RSA" ]; then
@@ -56,7 +56,7 @@ elif [ "${key_type}" == "RSA" ]; then
elif [ "${key_type}" == "ED25519" ]; then
echo 'Create ED25519 Key'
pkeyopt=""
-else
+else
echo 'Key Type not found'
fi
diff --git a/client_maker_erg.sh b/client_maker_erg.sh
new file mode 100755
index 0000000..ed153f1
--- /dev/null
+++ b/client_maker_erg.sh
@@ -0,0 +1,316 @@
+#!/usr/bin/env bash
+##############################################################################
+# HEADER #
+##############################################################################
+# Script for generating SSL certificates.
+##############################################################################
+# END_OF_HEADER #
+##############################################################################
+
+usage() {
+ echo "Script name: $0";
+ echo "";
+ echo "Usage: $0 --key-type [ RSA | EC | EX25519 ]";
+ echo " --curve [ P-256 | P-384 | P512 ]";
+ echo " --rsa-bits [ 2048 | 4096 ]";
+ echo " --days <n>";
+ echo "";
+ echo "Options: ";
+ echo " --key-type [ RSA | EC | EC25519 ]";
+ echo " Note: Pico W can use EC or RSA keys only, not EC25519,";
+ echo " and Tasmota can only use 2048 bit RSA keys.";
+ echo " --curve [ P-256 | P-384 | P512 ]";
+ echo " --rsa-bits [ 2048 | 4096 ]";
+ echo " --days [ <integer> ]";
+ echo " --dir [ <mosquitto directory> ]";
+ echo " --output-format [ PEM | DER ]";
+ echo " --user <string>";
+ echo " --config <path-to-cnf-file>";
+ echo " --ca_key <path-to-CA_key>";
+ echo " --ca_cert <path-to-CA_cert>";
+ }
+
+TEMP="$(getopt -o hkcrd --long help,key-type:,curve:,rsa-bits:,days:,dir:,output-format:,user:,config:,ca_key:,ca_cert: -- "$@")"
+
+eval set -- "$TEMP"
+
+if [ $# == 0 ] ; then
+ usage;
+ exit 1;
+fi
+
+KEY_TYPE="EC"
+CURVE="P-256"
+RSA_BITS=2048
+DAYS=365
+MOSQUITTO_DIR="/home/erg/cert_test"
+OUTPUT_FORMAT=
+MOSQUITTO_USER=
+PKEYOPT=
+SSL_CNF=
+SSL_CA_KEY=
+SSL_CA_CRT=
+
+while true
+do
+ case "$1" in
+ -h|--help)
+ usage
+ exit 0;
+ ;;
+ --key-type) KEY_TYPE="$2"; echo "Key type: $KEY_TYPE"; shift 2;;
+ # Note: Pico W can use EC or RSA keys only, not EC25519, and Tasmota can only use 2048 bit RSA keys.
+ # Default choice of Key Types: RSA, EC, or ED25519
+ --curve) CURVE="$2"; echo "Curve: $CURVE"; shift 2;;
+ # Choice of NIST Curves for EC Keys: P-256, P-384 or P-521
+ --rsa-bits) RSA_BITS="$2"; echo "RSA bits: $RSA_BITS"; shift 2;;
+ # Choice of Bits for RSA Keys: 2048 or 4096
+ --days) DAYS="$2"; echo "Valid for: $DAYS"; shift 2;;
+ # How many days is the Cert valid for?
+ --dir) MOSQUITTO_DIR="$2"; echo "Working directory: $MOSQUITTO_DIR"; shift 2;;
+ --output-format) OUTPUT_FORMAT="$2"; echo "Output format: $OUTPUT_FORMAT"; shift 2;;
+ --user) MOSQUITTO_USER="$2"; echo "Mosquitto user: $MOSQUITTO_USER"; shift 2;;
+ --config) SSL_CNF="$2"; shift 2;;
+ --ca_key) SSL_CA_KEY="$2"; shift 2;;
+ --ca_cert) SSL_CA_CRT="$2"; shift 2;;
+ -- ) shift; break;;
+ * ) break;;
+ esac
+done
+
+##############################################################################
+# Checks that sane arguments passed: #
+##############################################################################
+
+# Check key type:
+if [ "$KEY_TYPE" != 'EC' ] && [ "$KEY_TYPE" != 'RSA' ] && [ "$KEY_TYPE" != 'EC25519' ]; then
+ echo "Wrong key type specified: '$KEY_TYPE'"
+ echo "Valid keys: [ EC | RSA | EC25519 ]"
+ usage
+ exit 1;
+fi
+
+# Set the specific PKEYOPT for the chosen algorithm (BLANK for ED25519)
+if [ "${KEY_TYPE}" == "EC" ]; then
+ echo 'Create EC Key'
+ PKEYOPT="-pkeyopt ec_paramgen_curve:${CURVE}"
+elif [ "${KEY_TYPE}" == "RSA" ]; then
+ echo 'Create RSA Key'
+ PKEYOPT="-pkeyopt rsa_keygen_bits:${RSA_BITS}"
+elif [ "${KEY_TYPE}" == "ED25519" ]; then
+ echo 'Create ED25519 Key'
+ PKEYOPT=""
+else
+ echo "Setting -PKEYOPT to defaults: -pkeyopt ec_paramgen_curve:${CURVE}"
+fi
+
+# Check curve:
+if [ "$CURVE" != 'P-256' ] && [ "$CURVE" != 'P-384' ] && [ "$CURVE" != 'P-521' ]; then
+ echo "Wrong NIST curve specified: '$CURVE'"
+ echo "Curve must be one of: [ P-256 | P-384 | P-521 ]"
+ usage
+ exit 1;
+fi
+
+# Check RSA bits:
+if [ "$RSA_BITS" != '2048' ] && [ "$RSA_BITS" != '4096' ]; then
+ echo "Wrong RSA bits specified: '$RSA_BITS'"
+ echo "Must be one of: [ 2048 | 4096 ]"
+ usage
+ exit 1;
+fi
+
+# Check days parameter is a number:
+if [ -n "$DAYS" ] && [ "$DAYS" -eq "$DAYS" ] 2>/dev/null; then
+ echo "Certificate will be valid for: '$DAYS' days"
+else
+ echo "Not a number: $DAYS"
+ usage
+ exit 1;
+fi
+
+# Check output format:
+if [ "${OUTPUT_FORMAT}" == 'PEM' ] || [ "${OUTPUT_FORMAT}" == 'pem' ]; then # I: Double quote to prevent globbing and word splitting.
+ OUTPUT_FORMAT="pem"
+elif [ "$OUTPUT_FORMAT" == 'DER' ] || [ "$OUTPUT_FORMAT" == 'der' ]; then # I: Double quote to prevent globbing and word splitting.
+ OUTPUT_FORMAT="der"
+else echo "Incorrect certificate format type specified: $OUTPUT_FORMAT";
+ usage
+ exit 1;
+fi
+
+# Check user supplied:
+if [ -z "$MOSQUITTO_USER" ] && [ "$MOSQUITTO_USER" != " " ]; then
+ echo "Specified: '$MOSQUITTO_USER' as user."
+ echo "Need to specify a non-empty username for the certificate!";
+ usage
+ exit 1;
+fi
+
+# Check CA key and cert exist:
+if [ ! -f ${SSL_CA_CRT} ] || [ ! -f ${SSL_CA_KEY} ]; then
+ echo "CA certificate does not exist, exiting!"
+ exit 1
+fi
+# Check CA certificate is still valid:
+# Date format we get from openssl:
+# Nov 18 13:47:56 2034 GMT
+# %b %d %H:%M:%S %Y %Z
+# compare dates from the CA cert and current one:
+CERT_VALID_UNTIL=$(openssl x509 -enddate -noout -in "${SSL_CA_CRT}" | cut -d'=' -f2)
+echo "CA certificate valid until: ${CERT_VALID_UNTIL}"
+if [ ! $(($(date -d "$CERT_VALID_UNTIL" +"%s") > $(date +"%s"))) ];then
+ echo "CA certificate expired, exiting!"
+ exit 1
+fi
+
+
+##############################################################################
+# Set up variables #
+##############################################################################
+
+# Set the algorithm
+ALGORITHM="-algorithm ${KEY_TYPE}"
+
+# Output client key, CSR request and certificate
+CLIENT_KEY="${MOSQUITTO_DIR}/certs/clients/${MOSQUITTO_USER}/${MOSQUITTO_USER}_key.${OUTPUT_FORMAT}"
+CLIENT_CSR="${MOSQUITTO_DIR}/certs/clients/${MOSQUITTO_USER}/${MOSQUITTO_USER}_req.csr"
+CLIENT_CERT="${MOSQUITTO_DIR}/certs/clients/${MOSQUITTO_USER}/${MOSQUITTO_USER}_crt.${OUTPUT_FORMAT}"
+
+##############################################################################
+# Locks #
+##############################################################################
+
+LOCK_FILE=/tmp/$SUBJECT.lock
+if [ -f "$LOCK_FILE" ]; then
+ echo "Script is already running"
+ exit
+fi
+
+trap 'rm -f $LOCK_FILE' EXIT
+touch "$LOCK_FILE"
+
+##############################################################################
+# Backup existing certs and create dir structure #
+##############################################################################
+
+# If our user certs dir already exists, rename it so we don't
+# overwrite anything important.
+
+time_stamp=$(date +"%Y-%m-%d_%H-%M")
+mv "${MOSQUITTO_DIR}/certs/csr_files/${MOSQUITTO_USER}_req.csr" "${MOSQUITTO_DIR}/certs/clients/${MOSQUITTO_USER}" 2>/dev/null
+mv "${MOSQUITTO_DIR}/certs/clients/${MOSQUITTO_USER}req" "${MOSQUITTO_DIR}/certs/clients/${MOSQUITTO_USER}-${time_stamp}" 2>/dev/null
+
+mkdir -p "${MOSQUITTO_DIR}/certs/clients/${MOSQUITTO_USER}"
+
+##############################################################################
+# Create the key in the requested format #
+##############################################################################
+
+echo "Running:"
+echo "
+openssl genpkey ${ALGORITHM} ${PKEYOPT} \
+-outform ${OUTPUT_FORMAT} \
+-out ${CLIENT_KEY} \
+-config ${SSL_CNF}
+"
+
+openssl genpkey ${ALGORITHM} ${PKEYOPT} \
+ -outform "${OUTPUT_FORMAT}" \
+ -out "${CLIENT_KEY}" \
+ -config "${SSL_CNF}"
+
+retval=$?
+if [ $retval -ne 0 ]; then
+ echo "Generating key failed!"
+ exit $retval
+fi
+printf '\n\n'
+echo "#######################################################################"
+printf '\n\n'
+echo "Successfully generated client key."
+##############################################################################
+# Create the cert signing request #
+##############################################################################
+
+
+echo "Running:"
+echo "
+openssl req \
+-new \
+-nodes \
+-key $CLIENT_KEY \
+-subj /CN=${MOSQUITTO_USER} \
+-out $CLIENT_CSR \
+-config ${SSL_CNF}
+"
+openssl req \
+ -new \
+ -nodes \
+ -key "${CLIENT_KEY}" \
+ -subj "/CN=${MOSQUITTO_USER}" \
+ -out "${CLIENT_CSR}" \
+ -config "${SSL_CNF}"
+
+retval=$?
+if [ $retval -ne 0 ]; then
+ echo "Generating CSR request failed!"
+ exit $retval
+fi
+echo "Successfully generated CSR request"
+
+printf '\n\n'
+echo "#######################################################################"
+printf '\n\n'
+
+##############################################################################
+# Cert signing and creation #
+##############################################################################
+
+echo "Running:"
+echo "openssl x509 \
+-req \
+-in ${CLIENT_CSR} \
+-CA ${SSL_CA_CRT} \
+-CAkey ${SSL_CA_KEY} \
+-CAcreateserial \
+-out ${CLIENT_CERT} \
+-outform ${OUTPUT_FORMAT} \
+-days ${DAYS} \
+-config ${SSL_CNF}
+"
+openssl x509 \
+ -req \
+ -in "${CLIENT_CSR}" \
+ -CA "${SSL_CA_CRT}" \
+ -CAkey "${SSL_CA_KEY}" \
+ -CAcreateserial \
+ -out "${CLIENT_CERT}" \
+ -outform "${OUTPUT_FORMAT}" \
+ -days "${DAYS}" \
+
+retval=$?
+if [ $retval -ne 0 ]; then
+ echo "Signing CSR and certificate creation failed!"
+ exit $retval
+fi
+echo "Successfully signed CSR and created client certificate."
+
+printf '\n\n'
+echo "#######################################################################"
+printf '\n\n'
+
+##############################################################################
+# Check the cert #
+##############################################################################
+
+printf '# This is your new client certificate\n\n\n'
+
+openssl x509 \
+ -text \
+ -in "${CLIENT_CERT}" \
+ -noout
+
+printf '\n\n'
+echo "#######################################################################"
+printf '\n\n'
diff --git a/config.py.example b/config.py.example
index b6070ee..fe41508 100644
--- a/config.py.example
+++ b/config.py.example
@@ -3,9 +3,9 @@ Settings for main.py
"""
# Wlan settings:
-WLAN_NAME =
-WLAN_PWD =
-COUNTRY =
+WLAN_NAME = 'your_wlan_name'
+WLAN_PWD = 'your_password'
+COUNTRY = 'GR' # Two letter country code
# PID settings
KP = 1
@@ -17,21 +17,26 @@ SAMPLE_TIME = 900
# NTP server address:
NTP_SERVER = 'pool.ntp.org'
# Depends on your location:
-NTP_OFFSET =
+NTP_OFFSET =
+NTP_DELTA =
# MQTT settings:
-MQTT_SERVER_IP =
-MQTT_SERVER =
+MQTT_SERVER_IP =
+MQTT_SERVER =
MQTT_PORT = 8883
-MQTT_CLIENT_ID =
-MQTT_USER =
+MQTT_CLIENT_ID =
+MQTT_USER =
MQTT_TOPIC = 'temperature'
+MQTT_TOPIC_CERT_RENEWAL = 'cert_renew'
# TLS settings:
SSL_CERT_FILE = "pico_crt.der"
SSL_KEY_FILE = "pico_key.der"
SSL_CADATA_FILE = "ca_crt.der"
+# Save latest valid timestamp to this file:
+LATEST_TIMESTAMP_FILE = 'latest_timestamp.txt'
+
# Mosfet pin:
MOSFET_PIN = 5
diff --git a/main.py b/main.py
index 3bc072d..0384723 100644
--- a/main.py
+++ b/main.py
@@ -6,6 +6,7 @@ connecting to wifi and sending temperature data to MQTT server.
import ssl
import time
from math import isnan
+import uos
from ubinascii import hexlify
from ds18x20 import DS18X20
@@ -22,11 +23,40 @@ from PID import PID
from umqttsimple import MQTTClient
# Import settings from config.py:
-from config import *
-
+from config import (
+ WLAN_NAME,
+ WLAN_PWD,
+ COUNTRY,
+ KP,
+ KI,
+ KD,
+ TARGET_TEMPERATURE,
+ SAMPLE_TIME,
+ NTP_SERVER,
+ NTP_OFFSET,
+ MQTT_SERVER_IP,
+ MQTT_SERVER,
+ MQTT_PORT,
+ MQTT_CLIENT_ID,
+ MQTT_USER,
+ MQTT_TOPIC,
+ MQTT_TOPIC_CERT_RENEWAL,
+ SSL_CERT_FILE,
+ SSL_KEY_FILE,
+ SSL_CADATA_FILE,
+ MOSFET_PIN,
+ ONEWIRE_PIN,
+ LATEST_TIMESTAMP_FILE,
+)
+MQTT_TOPIC_CERT_RENEWAL_CLIENT = f"{MQTT_TOPIC_CERT_RENEWAL}/{MQTT_USER}"
# That led is different for different MicroPython versions:
-led = Pin("LED", Pin.OUT)
+LED = Pin("LED", Pin.OUT)
+global CLIENT_UNINITIALISED
+global TIME_RETVAL
+global MQTT_RETVAL
+global WLAN_CONNECTED
+global WLAN_INITIALISED
# Blinking communication:
# 1 slow sequence of one: on initializatio
@@ -48,9 +78,9 @@ def blink(n=1, interval=0.5, repeat=0):
try:
while repeat >= r:
for i in range(n):
- led.on()
+ LED.on()
time.sleep(interval)
- led.off()
+ LED.off()
time.sleep(interval)
r += 1
time.sleep(1)
@@ -65,17 +95,33 @@ def create_wifi():
Create wireless connection. Takes SSID and password from global variables atm.
:return: wlan object, return value, connection state
"""
- wlan_retval = 1
- wlan = WLAN(STA_IF)
- wlan.active(True)
- rp2.country(COUNTRY)
- try: # FIXME: this often raises EPERM, find out if it's code or buggy wifi repeater
- wlan.connect(WLAN_NAME, WLAN_PWD)
- time.sleep(0.5)
- wlan_retval = 0
+ global WLAN_INITIALISED
+ try:
+ wlan = WLAN(STA_IF)
+ wlan.active(True)
+ rp2.country(COUNTRY)
+ WLAN_INITIALISED = True
except Exception as exc:
- print("Failed to connect to wlan: ", exc)
- return wlan, wlan_retval, wlan.isconnected()
+ print("Exception creating wifi: %r" % exc)
+
+ return wlan
+
+def connect_wifi(wlan):
+ """
+ Connect to wifi.
+ :return: int
+ """
+ r = 0
+ while not wlan.isconnected() and r < 3:
+ try:
+ wlan.connect(WLAN_NAME, WLAN_PWD)
+ time.sleep(1)
+ r += 1
+ except Exception as exc:
+ print("Exception connecting to wlan!: %r" % exc)
+ blink(3, 0.2)
+ r += 1
+ return wlan.isconnected()
def _time():
@@ -88,16 +134,16 @@ def _time():
# try 5 times to get time from NTP server:
while counter < 5:
try:
- NTP_QUERY = bytearray(48)
- NTP_QUERY[0] = 0x1B
+ ntp_query = bytearray(48)
+ ntp_query[0] = 0x1B
addr = (NTP_SERVER, 123)
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM)
sock.settimeout(10) # was 1 second
- res = sock.sendto(NTP_QUERY, addr)
+ res = sock.sendto(ntp_query, addr)
msg = sock.recv(48)
val = ustruct.unpack("!I", msg[40:44])[0]
cur_time = val - NTP_OFFSET
- print("NTP time with offset is: ", cur_time)
+ print("NTP time with offset is: %r" % cur_time)
break
except OSError as exc:
if exc.args[0] == 110: # ETIMEDOUT
@@ -105,10 +151,10 @@ def _time():
utime.sleep(2)
counter += 1
continue
- print("Error from _time(): ", exc)
+ print("Error from _time(): %r" % exc)
counter = 5
except Exception as exc:
- print("Getting time failed: ", exc)
+ print("Getting time failed: %r" % exc)
counter = 5
finally:
if sock:
@@ -126,47 +172,50 @@ def set_time():
"""
print("Calling ntptime.time() ...")
retval = 1
- # t will never be less than zero on success:
- t = -1
+ # new_timestamp will never be less than zero on success:
+ new_timestamp = -1
try:
- t = _time()
- except Exception as exc:
- print("Exception calling ntptime.time: ", exc)
- if t == -1:
- # Getting time from NTP failed, return early:
- print("Getting time failed, returning early from set_time()")
- return retval
- try:
- tm = utime.gmtime(t)
+ new_timestamp = _time()
+ retval = 0
except Exception as exc:
- print("Failed to set tm = utime.gmtime(t)", exc)
+ print("Exception calling ntptime.time: %r" % exc)
+ if new_timestamp == -1:
+ print("Getting time from NTP failed.")
+ retval = 1
+ if not retval:
+ # Convert seconds since the Epoch to a time tuple expressing UTC:
+ tm = utime.gmtime(new_timestamp)
+ try:
+ # Check timedelta of the latest timestamp from timestamp file.
+ # If newer, save it to that file, else load from it.
+ with open(LATEST_TIMESTAMP_FILE, 'w', encoding='utf-8') as f:
+ timestamp_from_file = f.read()
+ # There will not be a timestamp the first time around:
+ if timestamp_from_file:
+ old_timestamp = utime.time(float(timestamp_from_file))
+ # If new timestamp is greater that last recorded, update record:
+ if new_timestamp > old_timestamp:
+ print("Saving new timestamp to the timestamp file.")
+ f.write(str(new_timestamp))
+ # Best effort: set time to the oldest received time:
+ else:
+ print("Loading timestamp from timestamp file.")
+ new_timestamp = old_timestamp
+ else:
+ print("Gunna write that timestamp to that file now ...")
+ f.write(str(new_timestamp))
+
+ except Exception as exc:
+ print("Failed to set time: %r" % exc)
+ retval = 1
try:
RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))
retval = 0
except Exception as exc:
- print("Failed to RTC().datetime ... : ", exc)
+ print("Failed to RTC().datetime ... : %r" % exc)
return retval
-def connect_wifi(wlan):
- """
- Connect to wifi.
- :return: int
- """
- retcode = 1 # 1 for failure, 0 for success
- r = 0
- while not wlan.isconnected() and r < 3:
- try:
- wlan.connect()
- time.sleep(1)
- r += 1
- except:
- blink(3, 0.2)
- r += 1
- if wlan.isconnected():
- retcode = 0
- return retcode
-
def load_certs():
"""
@@ -175,26 +224,26 @@ def load_certs():
"""
try:
with open(SSL_CERT_FILE, 'rb') as _file:
- SSL_CERT = _file.read()
+ ssl_cert = _file.read()
except Exception as exc:
- print(f"Reading {SSL_CERT_FILE} failed: {exc}")
- led.off()
+ print("Reading %r failed: %r" % (SSL_CERT_FILE, exc))
+ LED.off()
try:
with open(SSL_KEY_FILE, 'rb') as _file:
- SSL_KEY = _file.read()
+ ssl_key = _file.read()
except Exception as exc:
- print(f"Reading {SSL_KEY_FILE} failed: {exc}")
- led.off()
+ print("Reading %r failed: %r" % (SSL_KEY_FILE, exc))
+ LED.off()
try:
with open(SSL_CADATA_FILE, 'rb') as _file:
- SSL_CADATA = _file.read()
+ ssl_cadata = _file.read()
except Exception as exc:
- print(f"Reading {SSL_CADATA_FILE} failed: {exc}")
- led.off()
+ print("Reading %r failed: %r" % (SSL_CADATA_FILE, exc))
+ LED.off()
ssl_params = {
- "key": SSL_KEY,
- "cert": SSL_CERT,
- "cadata": SSL_CADATA,
+ "key": ssl_key,
+ "cert": ssl_cert,
+ "cadata": ssl_cadata,
"server_hostname": MQTT_SERVER,
"server_side": False,
"cert_reqs": ssl.CERT_REQUIRED,
@@ -202,7 +251,6 @@ def load_certs():
}
return ssl_params
-
def create_mqtt():
"""
Instantiate MQTT client
@@ -212,7 +260,7 @@ def create_mqtt():
try:
ssl_params = load_certs()
except Exception as exc:
- print("failed to create mqtt client: ", exc)
+ print("failed to create mqtt client: %r" % exc)
blink(3, 0.3, 2)
ssl_params = None
if ssl_params:
@@ -230,7 +278,7 @@ def create_mqtt():
return mqtt_client
-def connect_mqtt(client):
+def connect_mqtt(_client):
"""
Connect MQTT client.
:return: int
@@ -241,18 +289,50 @@ def connect_mqtt(client):
retval = 0
# reset to clear memory on OSError:
except OSError as exc:
- print("OSError: ", exc)
+ print("OSError: %r" % exc)
if exc == 'Exception in thread rx':
pass
- print("OSError repr: ", repr(exc))
print("OSError encountered, reseting ...")
reset()
except Exception as exc:
- print("Failed to connect to mqtt: ", exc)
+ print("Failed to connect to mqtt: %r" % exc)
blink(2, 0.2, 3)
return retval
+def cert_reneval(_topic, _message):
+ """
+ Callback function receiving new SSL certificate and SSL key
+ upon old one expiry and installing it.
+ In order to be recognised as a message containing SSL certificate,
+ message needs to start with 'certificate: '
+ In order to be recognised as a message containing SSL key,
+ message needs to start with 'key: '.
+ """
+ if _topic == MQTT_TOPIC_CERT_RENEWAL_CLIENT:
+ global CLIENT_UNINITIALISED
+ global client
+ cert_prefix = 'certificate: '
+ key_prefix = 'key: '
+ # Backup certificate and key:
+ uos.rename(SSL_CERT_FILE, ''.join((SSL_CERT_FILE, '.bak')))
+ uos.rename(SSL_KEY_FILE, ''.join((SSL_KEY_FILE, '.bak')))
+ # Overwrite old certificate and key with new ones:
+ if _message.startswith(cert_prefix):
+ new_cert = _message.removeprefix(cert_prefix)
+ with open(SSL_CERT_FILE, 'w', encoding='utf-8') as f:
+ f.write(new_cert)
+ if _message.startswith(key_prefix):
+ new_key = _message.removeprefix(key_prefix)
+ with open(SSL_KEY_FILE, 'w', encoding='utf-8') as f:
+ f.write(new_key)
+ client.publish(_topic, 'WARNING: Renewed certificate and key')
+ # Disconnect client and delete:
+ client.disconnect()
+ del client
+ # Set to True so that we recreate the client on the next iteration:
+ CLIENT_UNINITIALISED = True
+
def get_temperature():
"""
Get temperatures and ids from one wire sensors.
@@ -266,7 +346,7 @@ def get_temperature():
try:
_temperatures = [(hexlify(_id).decode(), str(round(float(sensor.read_temp(_id)), 1))) for _id in roms]
except Exception as exc:
- print("Failed to get temperatures: ", exc)
+ print("Failed to get temperatures: %r" % exc)
return _temperatures
@@ -314,66 +394,80 @@ class Mosfet:
# Valid values in range 0-65535:
self.mosfet.duty_u16(value)
except Exception as exc:
- led.off()
- print(f"Setting mosfet value failed: {exc}")
+ LED.off()
+ print("Setting mosfet value failed: %r" % exc)
raise
+def scan_wlan(wlan):
+ print("Scanning the network ...")
+ wifi_list = wlan.scan()
+ print("Wi-fi list:")
+ for ssid in wifi_list:
+ print(" %r" % repr(ssid[0]))
+
-WLAN_UNINITIALISED = CLIENT_UNINITIALISED = WLAN_RETVAL = TIME_RETVAL = MQTT_RETVAL = 1
+CLIENT_UNINITIALISED = TIME_RETVAL = MQTT_RETVAL = 1
temperatures, client, wlan = None, None, None
-WLAN_CONNECTED = False
+WLAN_CONNECTED, WLAN_INITIALISED = False, False
while True:
# Create connection object only once:
- if WLAN_UNINITIALISED:
+ if not WLAN_INITIALISED:
print("Creating wlan... ")
blink(1, 1, 1)
- wlan, WLAN_UNINITIALISED, WLAN_CONNECTED = create_wifi()
- if WLAN_CONNECTED:
- WLAN_RETVAL = 0
- if wlan and not WLAN_UNINITIALISED and not WLAN_CONNECTED:
- try:
- print("Connecting to wifi ...")
- WLAN_RETVAL = connect_wifi(wlan)
- print("WLAN_RETVAL: ", WLAN_RETVAL)
- print("Wlan connected: ", wlan.isconnected())
- except Exception as exc:
- print('Failed to connect to wifi: ', exc)
+ wlan = create_wifi()
+ if wlan and WLAN_INITIALISED and not WLAN_CONNECTED:
+ scan_wlan(wlan)
+ print("Connecting to wifi ...")
+ connect_wifi(wlan)
+ print("Wlan connected: %r" % wlan.isconnected())
if TIME_RETVAL and wlan.isconnected():
print("Setting time: ...")
TIME_RETVAL = set_time()
- print("TIME_RETVAL: ", TIME_RETVAL)
- if CLIENT_UNINITIALISED and not WLAN_RETVAL and not TIME_RETVAL:
+ print("TIME_RETVAL: %r" % TIME_RETVAL)
+ if CLIENT_UNINITIALISED and not TIME_RETVAL:
print("Creating client ...")
try:
client = create_mqtt()
+ # Connecting client to server:
+ print("Connecting to %s" % MQTT_SERVER)
+ client.connect()
+ # Set callback for certificate renewal:
+ print("MQTT:Setting callback ...")
+ client.set_callback(cert_reneval)
+ # Subscribe to certificate renewal topic:
+ print("MQTT: Subscribing to %s ..." % MQTT_TOPIC_CERT_RENEWAL_CLIENT)
+ client.subscribe(MQTT_TOPIC_CERT_RENEWAL_CLIENT)
CLIENT_UNINITIALISED = 0
except Exception as exc:
- print("Won't be any client: ", exc)
- if MQTT_RETVAL and not WLAN_RETVAL and not TIME_RETVAL and client:
+ CLIENT_UNINITIALISED = 1
+ print("Won't be any client: %r" % repr(exc))
+ if MQTT_RETVAL and not TIME_RETVAL and not CLIENT_UNINITIALISED:
try:
print("Connecting to mqtt broker ...")
MQTT_RETVAL = connect_mqtt(client)
- print("MQTT_RETVAL: ", MQTT_RETVAL)
+ print("MQTT_RETVAL: %r" % MQTT_RETVAL)
except Exception as exc:
- print("Failed to connect to mqtt broker: ", exc)
+ print("Failed to connect to mqtt broker: %r" % exc)
if MQTT_RETVAL:
- print("Failed to connect to mqtt broker: ", MQTT_RETVAL)
+ print("Failed to connect to mqtt broker: %r" % MQTT_RETVAL)
+ else:
+ client.check_msg()
try:
temperatures = get_temperature()
- print("Temperatures: ", temperatures)
+ print("Temperatures: %r" % temperatures)
except Exception as exc:
- print("Failed to get temperature(s): ", exc)
- if not WLAN_RETVAL and not MQTT_RETVAL and not TIME_RETVAL and temperatures:
+ print("Failed to get temperature(s): %r" % exc)
+ if not MQTT_RETVAL and not TIME_RETVAL and temperatures:
try:
for _id, temp in temperatures:
- print(f"Publishing temperature: sensor id: {_id} - {temp} ...")
+ print("Publishing temperature: sensor id: %r - %r" % (_id, temp))
client.publish(f'temperature/{_id}', temp)
client.check_msg()
except Exception as exc:
- print("Exception publishing: ", exc)
+ print("Exception publishing: %r" % exc)
blink(3, 0.2, 1)
else:
- print(f"Failed to publish:\nWLAN_RETVAL: {WLAN_RETVAL}\nMQTT_RETVAL: {MQTT_RETVAL}\nTIME_RETVAL: {TIME_RETVAL}\n")
+ print(f"Failed to publish:\nMQTT_RETVAL: {MQTT_RETVAL}\nTIME_RETVAL: {TIME_RETVAL}\n")
print("Going to sleep for 10")
time.sleep(10)
# ntptime.settime() kills the board. Check out solutions at:
@@ -382,7 +476,7 @@ while True:
# pid_val = mft.pid_value(temp)
# mft.set(pid_val)
# except Exception as exc:
-# led.off()
+# LED.off()
# print(f"Setting mosfet value failed: {exc}")
# pass
# time.sleep(10)
diff --git a/sequence_diagram.png b/sequence_diagram.png
new file mode 100644
index 0000000..482f1e7
--- /dev/null
+++ b/sequence_diagram.png
Binary files differ
diff --git a/sequence_diagram.txt b/sequence_diagram.txt
new file mode 100644
index 0000000..8999efc
--- /dev/null
+++ b/sequence_diagram.txt
@@ -0,0 +1,9 @@
+@startuml
+server -> server: Start MQTT
+server -> server: Check certificate validity
+server -> client: Send certificate
+client -> server: Confirm installed
+client -> client: reload MQTT
+server -> server: reload MQTT
+client -> server: reestablish connection
+@enduml
diff --git a/state_diagram.png b/state_diagram.png
new file mode 100644
index 0000000..7030366
--- /dev/null
+++ b/state_diagram.png
Binary files differ
diff --git a/state_diagram.txt b/state_diagram.txt
new file mode 100644
index 0000000..2da76b1
--- /dev/null
+++ b/state_diagram.txt
@@ -0,0 +1,27 @@
+@startuml
+:MQTT service starts;
+start
+
+if (certificate expired) then
+ #pink:raise error;
+ kill
+endif
+:start accepting connections;
+:check certificate;
+
+if (expiring soon) then (yes)
+ :generate new certificate;
+ :send to clients;
+
+ if (confirmation NOK) then
+ #pink:raise error;
+ kill
+ endif
+ :restart MQTT;
+
+else (no)
+ :continue;
+endif
+
+stop
+@enduml
diff --git a/umqttsimple.py b/umqttsimple.py
index 44d1dde..f0b7539 100644
--- a/umqttsimple.py
+++ b/umqttsimple.py
@@ -1,214 +1,218 @@
-import usocket as socket
-import ustruct as struct
-
-
-class MQTTException(Exception):
- pass
-
-
-class MQTTClient:
- def __init__(
- self,
- client_id,
- server,
- port=0,
- user=None,
- password=None,
- keepalive=0,
- ssl=False,
- ssl_params={},
- ):
- if port == 0:
- port = 8883 if ssl else 1883
- self.client_id = client_id
- self.sock = None
- self.server = server
- self.port = port
- self.ssl = ssl
- self.ssl_params = ssl_params
- self.pid = 0
- self.cb = None
- self.user = user
- self.pswd = password
- self.keepalive = keepalive
- self.lw_topic = None
- self.lw_msg = None
- self.lw_qos = 0
- self.lw_retain = False
-
- def _send_str(self, s):
- self.sock.write(struct.pack("!H", len(s)))
- self.sock.write(s)
-
- def _recv_len(self):
- n = 0
- sh = 0
- while 1:
- b = self.sock.read(1)[0]
- n |= (b & 0x7F) << sh
- if not b & 0x80:
- return n
- sh += 7
-
- def set_callback(self, f):
- self.cb = f
-
- def set_last_will(self, topic, msg, retain=False, qos=0):
- assert 0 <= qos <= 2
- assert topic
- self.lw_topic = topic
- self.lw_msg = msg
- self.lw_qos = qos
- self.lw_retain = retain
-
- def connect(self, clean_session=True):
- self.sock = socket.socket()
- addr = socket.getaddrinfo(self.server, self.port)[0][-1]
- self.sock.connect(addr)
- if self.ssl:
- import ussl
-
- self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
- premsg = bytearray(b"\x10\0\0\0\0\0")
- msg = bytearray(b"\x04MQTT\x04\x02\0\0")
-
- sz = 10 + 2 + len(self.client_id)
- msg[6] = clean_session << 1
- if self.user is not None:
- sz += 2 + len(self.user) + 2 + len(self.pswd)
- msg[6] |= 0xC0
- if self.keepalive:
- assert self.keepalive < 65536
- msg[7] |= self.keepalive >> 8
- msg[8] |= self.keepalive & 0x00FF
- if self.lw_topic:
- sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
- msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
- msg[6] |= self.lw_retain << 5
-
- i = 1
- while sz > 0x7F:
- premsg[i] = (sz & 0x7F) | 0x80
- sz >>= 7
- i += 1
- premsg[i] = sz
-
- self.sock.write(premsg, i + 2)
- self.sock.write(msg)
- # print(hex(len(msg)), hexlify(msg, ":"))
- self._send_str(self.client_id)
- if self.lw_topic:
- self._send_str(self.lw_topic)
- self._send_str(self.lw_msg)
- if self.user is not None:
- self._send_str(self.user)
- self._send_str(self.pswd)
- resp = self.sock.read(4)
- assert resp[0] == 0x20 and resp[1] == 0x02
- if resp[3] != 0:
- raise MQTTException(resp[3])
- return resp[2] & 1
-
- def disconnect(self):
- self.sock.write(b"\xe0\0")
- self.sock.close()
-
- def ping(self):
- self.sock.write(b"\xc0\0")
-
- def publish(self, topic, msg, retain=False, qos=0):
- pkt = bytearray(b"\x30\0\0\0")
- pkt[0] |= qos << 1 | retain
- sz = 2 + len(topic) + len(msg)
- if qos > 0:
- sz += 2
- assert sz < 2097152
- i = 1
- while sz > 0x7F:
- pkt[i] = (sz & 0x7F) | 0x80
- sz >>= 7
- i += 1
- pkt[i] = sz
- # print(hex(len(pkt)), hexlify(pkt, ":"))
- self.sock.write(pkt, i + 1)
- self._send_str(topic)
- if qos > 0:
- self.pid += 1
- pid = self.pid
- struct.pack_into("!H", pkt, 0, pid)
- self.sock.write(pkt, 2)
- self.sock.write(msg)
- if qos == 1:
- while 1:
- op = self.wait_msg()
- if op == 0x40:
- sz = self.sock.read(1)
- assert sz == b"\x02"
- rcv_pid = self.sock.read(2)
- rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
- if pid == rcv_pid:
- return
- elif qos == 2:
- assert 0
-
- def subscribe(self, topic, qos=0):
- assert self.cb is not None, "Subscribe callback is not set"
- pkt = bytearray(b"\x82\0\0\0")
- self.pid += 1
- struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
- # print(hex(len(pkt)), hexlify(pkt, ":"))
- self.sock.write(pkt)
- self._send_str(topic)
- self.sock.write(qos.to_bytes(1, "little"))
- while 1:
- op = self.wait_msg()
- if op == 0x90:
- resp = self.sock.read(4)
- # print(resp)
- assert resp[1] == pkt[2] and resp[2] == pkt[3]
- if resp[3] == 0x80:
- raise MQTTException(resp[3])
- return
-
- # Wait for a single incoming MQTT message and process it.
- # Subscribed messages are delivered to a callback previously
- # set by .set_callback() method. Other (internal) MQTT
- # messages processed internally.
- def wait_msg(self):
- res = self.sock.read(1)
- #self.sock.setblocking(True)
- if res is None:
- return None
- if res == b"":
- raise OSError(-1)
- if res == b"\xd0": # PINGRESP
- sz = self.sock.read(1)[0]
- assert sz == 0
- return None
- op = res[0]
- if op & 0xF0 != 0x30:
- return op
- sz = self._recv_len()
- topic_len = self.sock.read(2)
- topic_len = (topic_len[0] << 8) | topic_len[1]
- topic = self.sock.read(topic_len)
- sz -= topic_len + 2
- if op & 6:
- pid = self.sock.read(2)
- pid = pid[0] << 8 | pid[1]
- sz -= 2
- msg = self.sock.read(sz)
- self.cb(topic, msg)
- if op & 6 == 2:
- pkt = bytearray(b"\x40\x02\0\0")
- struct.pack_into("!H", pkt, 2, pid)
- self.sock.write(pkt)
- elif op & 6 == 4:
- assert 0
-
- # Checks whether a pending message from server is available.
- # If not, returns immediately with None. Otherwise, does
- # the same processing as wait_msg.
- def check_msg(self):
- self.sock.setblocking(False)
- return self.wait_msg() \ No newline at end of file
+import usocket as socket
+import ustruct as struct
+
+
+class MQTTException(Exception):
+ pass
+
+
+class MQTTClient:
+ def __init__(
+ self,
+ client_id,
+ server,
+ port=0,
+ user=None,
+ password=None,
+ keepalive=0,
+ ssl=False,
+ ssl_params={},
+ ):
+ if port == 0:
+ port = 8883 if ssl else 1883
+ self.client_id = client_id
+ self.sock = None
+ self.server = server
+ self.port = port
+ self.ssl = ssl
+ self.ssl_params = ssl_params
+ self.pid = 0
+ self.cb = None
+ self.user = user
+ self.pswd = password
+ self.keepalive = keepalive
+ self.lw_topic = None
+ self.lw_msg = None
+ self.lw_qos = 0
+ self.lw_retain = False
+
+ def _send_str(self, s):
+ self.sock.write(struct.pack("!H", len(s)))
+ self.sock.write(s)
+
+ def _recv_len(self):
+ n = 0
+ sh = 0
+ while 1:
+ b = self.sock.read(1)[0]
+ n |= (b & 0x7F) << sh
+ if not b & 0x80:
+ return n
+ sh += 7
+
+ def set_callback(self, f):
+ self.cb = f
+
+ def set_last_will(self, topic, msg, retain=False, qos=0):
+ assert 0 <= qos <= 2
+ assert topic
+ self.lw_topic = topic
+ self.lw_msg = msg
+ self.lw_qos = qos
+ self.lw_retain = retain
+
+ def connect(self, clean_session=True):
+ self.sock = socket.socket()
+ addr = socket.getaddrinfo(self.server, self.port)[0][-1]
+ # Try/except block for debugging by Erg:
+ try:
+ self.sock.connect(addr)
+ except Exception as exc:
+ print("Exception connecting to socket: %r" % exc)
+ if self.ssl:
+ import ussl
+
+ self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
+ premsg = bytearray(b"\x10\0\0\0\0\0")
+ msg = bytearray(b"\x04MQTT\x04\x02\0\0")
+
+ sz = 10 + 2 + len(self.client_id)
+ msg[6] = clean_session << 1
+ if self.user is not None:
+ sz += 2 + len(self.user) + 2 + len(self.pswd)
+ msg[6] |= 0xC0
+ if self.keepalive:
+ assert self.keepalive < 65536
+ msg[7] |= self.keepalive >> 8
+ msg[8] |= self.keepalive & 0x00FF
+ if self.lw_topic:
+ sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
+ msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
+ msg[6] |= self.lw_retain << 5
+
+ i = 1
+ while sz > 0x7F:
+ premsg[i] = (sz & 0x7F) | 0x80
+ sz >>= 7
+ i += 1
+ premsg[i] = sz
+
+ self.sock.write(premsg, i + 2)
+ self.sock.write(msg)
+ # print(hex(len(msg)), hexlify(msg, ":"))
+ self._send_str(self.client_id)
+ if self.lw_topic:
+ self._send_str(self.lw_topic)
+ self._send_str(self.lw_msg)
+ if self.user is not None:
+ self._send_str(self.user)
+ self._send_str(self.pswd)
+ resp = self.sock.read(4)
+ assert resp[0] == 0x20 and resp[1] == 0x02
+ if resp[3] != 0:
+ raise MQTTException(resp[3])
+ return resp[2] & 1
+
+ def disconnect(self):
+ self.sock.write(b"\xe0\0")
+ self.sock.close()
+
+ def ping(self):
+ self.sock.write(b"\xc0\0")
+
+ def publish(self, topic, msg, retain=False, qos=0):
+ pkt = bytearray(b"\x30\0\0\0")
+ pkt[0] |= qos << 1 | retain
+ sz = 2 + len(topic) + len(msg)
+ if qos > 0:
+ sz += 2
+ assert sz < 2097152
+ i = 1
+ while sz > 0x7F:
+ pkt[i] = (sz & 0x7F) | 0x80
+ sz >>= 7
+ i += 1
+ pkt[i] = sz
+ # print(hex(len(pkt)), hexlify(pkt, ":"))
+ self.sock.write(pkt, i + 1)
+ self._send_str(topic)
+ if qos > 0:
+ self.pid += 1
+ pid = self.pid
+ struct.pack_into("!H", pkt, 0, pid)
+ self.sock.write(pkt, 2)
+ self.sock.write(msg)
+ if qos == 1:
+ while 1:
+ op = self.wait_msg()
+ if op == 0x40:
+ sz = self.sock.read(1)
+ assert sz == b"\x02"
+ rcv_pid = self.sock.read(2)
+ rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
+ if pid == rcv_pid:
+ return
+ elif qos == 2:
+ assert 0
+
+ def subscribe(self, topic, qos=0):
+ assert self.cb is not None, "Subscribe callback is not set"
+ pkt = bytearray(b"\x82\0\0\0")
+ self.pid += 1
+ struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
+ # print(hex(len(pkt)), hexlify(pkt, ":"))
+ self.sock.write(pkt)
+ self._send_str(topic)
+ self.sock.write(qos.to_bytes(1, "little"))
+ while 1:
+ op = self.wait_msg()
+ if op == 0x90:
+ resp = self.sock.read(4)
+ # print(resp)
+ assert resp[1] == pkt[2] and resp[2] == pkt[3]
+ if resp[3] == 0x80:
+ raise MQTTException(resp[3])
+ return
+
+ # Wait for a single incoming MQTT message and process it.
+ # Subscribed messages are delivered to a callback previously
+ # set by .set_callback() method. Other (internal) MQTT
+ # messages processed internally.
+ def wait_msg(self):
+ res = self.sock.read(1)
+ #self.sock.setblocking(True)
+ if res is None:
+ return None
+ if res == b"":
+ raise OSError(-1)
+ if res == b"\xd0": # PINGRESP
+ sz = self.sock.read(1)[0]
+ assert sz == 0
+ return None
+ op = res[0]
+ if op & 0xF0 != 0x30:
+ return op
+ sz = self._recv_len()
+ topic_len = self.sock.read(2)
+ topic_len = (topic_len[0] << 8) | topic_len[1]
+ topic = self.sock.read(topic_len)
+ sz -= topic_len + 2
+ if op & 6:
+ pid = self.sock.read(2)
+ pid = pid[0] << 8 | pid[1]
+ sz -= 2
+ msg = self.sock.read(sz)
+ self.cb(topic, msg)
+ if op & 6 == 2:
+ pkt = bytearray(b"\x40\x02\0\0")
+ struct.pack_into("!H", pkt, 2, pid)
+ self.sock.write(pkt)
+ elif op & 6 == 4:
+ assert 0
+
+ # Checks whether a pending message from server is available.
+ # If not, returns immediately with None. Otherwise, does
+ # the same processing as wait_msg.
+ def check_msg(self):
+ self.sock.setblocking(False)
+ return self.wait_msg()