diff options
m--------- | Mosquitto_CA_and_Certs | 0 | ||||
-rw-r--r-- | Readme.md | 18 | ||||
-rwxr-xr-x | ca_maker_erg.sh | 355 | ||||
-rwxr-xr-x | client_maker.sh | 6 | ||||
-rwxr-xr-x | client_maker_erg.sh | 316 | ||||
-rw-r--r-- | config.py.example | 21 | ||||
-rw-r--r-- | main.py | 298 | ||||
-rw-r--r-- | sequence_diagram.png | bin | 0 -> 12924 bytes | |||
-rw-r--r-- | sequence_diagram.txt | 9 | ||||
-rw-r--r-- | state_diagram.png | bin | 0 -> 23760 bytes | |||
-rw-r--r-- | state_diagram.txt | 27 | ||||
-rw-r--r-- | umqttsimple.py | 432 |
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 @@ -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 @@ -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 Binary files differnew file mode 100644 index 0000000..482f1e7 --- /dev/null +++ b/sequence_diagram.png 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 Binary files differnew file mode 100644 index 0000000..7030366 --- /dev/null +++ b/state_diagram.png 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() |