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.pngBinary files differ new 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.pngBinary files differ new 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() | 
