diff --git a/awsiot-sample/Makefile b/awsiot-sample/Makefile index 4cc0578..66b49f6 100644 --- a/awsiot-sample/Makefile +++ b/awsiot-sample/Makefile @@ -25,7 +25,9 @@ CFLAGS += $(LOG_FLAGS) CFLAGS += -DGIT_REVISION=\"$(if $(GIT_REVISION),-g$(GIT_REVISION))\" CFLAGS += $(shell pkg-config --cflags awsiotsdk) +CFLAGS += $(shell pkg-config --cflags libconfuse) LDLIBS += $(shell pkg-config --libs --static awsiotsdk) +LDLIBS += $(shell pkg-config --libs --static libconfuse) SRCS = $(wildcard $(SRC)/*.c) OBJS = $(SRCS:.c=.o) diff --git a/awsiot-sample/cfg_files/awsiotsdk.conf b/awsiot-sample/cfg_files/awsiotsdk.conf new file mode 100644 index 0000000..f0ae5b0 --- /dev/null +++ b/awsiot-sample/cfg_files/awsiotsdk.conf @@ -0,0 +1,49 @@ +#=============================================================================== +# AWS IoT Device SDK Settings +#=============================================================================== + +# Thing Name: Thing Name of the Shadow this device is associated with. +# A thing is the representation and record of your physical device in the cloud. +# Any physical device needs a thing to work with AWS IoT. Creating a thing will +# also create a thing shadow. +thing_name = "" + +# Client ID: MQTT client ID should be unique for every device. +# By default, it is the same as 'thing_name'. +#client_id = "" + +# Host: Customer specific MQTT HOST. +# This is your custom endpoint that allows your device to connect to AWS IoT. +# You can obtain it in your 'AWS IoT Console': +# 1. From the dashboard, in the left navigation pane, choose 'Registry' and +# then choose 'Things'. +# 2. Select the box representing your device to show its details page. +# 3. On the details page, in the left navigation pane, choose 'Interact' +# 4. Look for the HTTPS section, your endpoint will look something like: +# ABCDEFG1234567.iot.us-east-2.amazonaws.com +# where 'ABCDEFG1234567' is the subdomain and 'us-east-2' is the region. +host = "" + +# Port: Default port for MQTT/S. +# By default, 8883. +port = 8883 + +# Certificates path: Absolute path to the certificates directory in the device. +# By default, '/etc/ssl/certs'. +certs_path = "/etc/ssl/certs" + +# Root CA file name: Name of the Root CA file located in the configured +# 'certs_path'. +# By default, 'rootCA.crt' +rootca_filename = "rootCA.crt" + +# Signed certificate file name: Name of the device signed certificate file +# located in the configured 'certs_path'. +# By default, 'cert.pem' +signed_cert_filename = "cert.pem" + +# Device private key filename: Name of the device private key file located in +# the configured 'certs_path'. +# By default, 'privkey.pem' +private_key_filename = "privkey.pem" + diff --git a/awsiot-sample/src/aws_config.c b/awsiot-sample/src/aws_config.c new file mode 100644 index 0000000..27ec4de --- /dev/null +++ b/awsiot-sample/src/aws_config.c @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2017 Digi International Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + * Digi International Inc. 11001 Bren Road East, Minnetonka, MN 55343 + * ======================================================================= + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "aws_config.h" + +/*------------------------------------------------------------------------------ + D E F I N I T I O N S +------------------------------------------------------------------------------*/ +#define SETTING_THING_NAME "thing_name" +#define SETTING_CLIENT_ID "client_id" +#define SETTING_HOST "host" +#define SETTING_PORT "port" +#define SETTING_CERTS_PATH "certs_path" +#define SETTING_ROOTCA_NAME "rootca_filename" +#define SETTING_SCERT_NAME "signed_cert_filename" +#define SETTING_PRIVKEY_NAME "private_key_filename" + +#define SETTING_UNKNOWN "__unknown" + +/*------------------------------------------------------------------------------ + F U N C T I O N D E C L A R A T I O N S +------------------------------------------------------------------------------*/ +static int fill_aws_iot_config(aws_iot_cfg_t *aws_cfg); +static int cfg_check_thing_name(cfg_t *cfg, cfg_opt_t *opt); +static int cfg_check_client_id(cfg_t *cfg, cfg_opt_t *opt); +static int cfg_check_port(cfg_t *cfg, cfg_opt_t *opt); +static int cfg_check_certificates_path(cfg_t *cfg, cfg_opt_t *opt); +static int cfg_check_cert_file(cfg_t *cfg, cfg_opt_t *opt); +static int cfg_check_int_range(cfg_t *cfg, cfg_opt_t *opt, uint32_t min, uint32_t max); +static int cfg_check_empty_string(cfg_t *cfg, cfg_opt_t *opt); +static int cfg_check_string_length(cfg_t *cfg, cfg_opt_t *opt, uint16_t min, uint16_t max); +static int file_exists(const char *const filename); +static int file_readable(const char *const filename); + +/*------------------------------------------------------------------------------ + G L O B A L V A R I A B L E S +------------------------------------------------------------------------------*/ +static cfg_t *cfg; + +/*------------------------------------------------------------------------------ + F U N C T I O N D E F I N I T I O N S +------------------------------------------------------------------------------*/ +/* + * parse_configuration() - Parse and save the settings of a configuration file + * + * @filename: Name of the file containing the configuration settings. + * @aws_cfg: AWS IoT Device SDK configuration struct (aws_iot_cfg_t) where + * the settings parsed from the configuration file are saved. + * + * Read the provided configuration file and save the settings in the given + * aws_iot_cfg_t struct. If the file does not exist or cannot be read, the + * configuration struct is initialized with the default settings. + * + * Return: 0 if the file is parsed successfully, -1 if there is an error + * parsing the file. + */ +int parse_configuration(const char *const filename, aws_iot_cfg_t *aws_cfg) +{ + /* Overall structure of the settings. */ + static cfg_opt_t opts[] = { + /* --------------------------------------------------------------------- */ + /*| TYPE | SETTING NAME | DEFAULT VALUE | FLAGS |*/ + /* --------------------------------------------------------------------- */ + CFG_STR (SETTING_THING_NAME, AWS_IOT_MY_THING_NAME, CFGF_NONE), + CFG_STR (SETTING_CLIENT_ID, AWS_IOT_MQTT_CLIENT_ID, CFGF_NONE), + CFG_STR (SETTING_HOST, AWS_IOT_MQTT_HOST, CFGF_NONE), + CFG_INT (SETTING_PORT, AWS_IOT_MQTT_PORT, CFGF_NONE), + + CFG_STR (SETTING_CERTS_PATH, DEFAULT_CERTS_PATH, CFGF_NONE), + CFG_STR (SETTING_ROOTCA_NAME, AWS_IOT_ROOT_CA_FILENAME,CFGF_NONE), + CFG_STR (SETTING_SCERT_NAME, AWS_IOT_CERTIFICATE_FILENAME,CFGF_NONE), + CFG_STR (SETTING_PRIVKEY_NAME, AWS_IOT_PRIVATE_KEY_FILENAME,CFGF_NONE), + + /* Needed for unknown settings. */ + CFG_STR (SETTING_UNKNOWN, NULL, CFGF_NONE), + CFG_END() + }; + + if (!file_exists(filename)) { + IOT_ERROR("File '%s' does not exist.", filename); + return -1; + } + + if (!file_readable(filename)) { + IOT_ERROR("File '%s' cannot be read.", filename); + return -1; + } + + cfg = cfg_init(opts, CFGF_IGNORE_UNKNOWN); + cfg_set_validate_func(cfg, SETTING_THING_NAME, cfg_check_thing_name); + cfg_set_validate_func(cfg, SETTING_CLIENT_ID, cfg_check_client_id); + cfg_set_validate_func(cfg, SETTING_HOST, cfg_check_empty_string); + cfg_set_validate_func(cfg, SETTING_PORT, cfg_check_port); + cfg_set_validate_func(cfg, SETTING_CERTS_PATH, cfg_check_certificates_path); + cfg_set_validate_func(cfg, SETTING_ROOTCA_NAME, cfg_check_cert_file); + cfg_set_validate_func(cfg, SETTING_SCERT_NAME, cfg_check_cert_file); + cfg_set_validate_func(cfg, SETTING_PRIVKEY_NAME, cfg_check_cert_file); + + /* Parse the configuration file. */ + switch (cfg_parse(cfg, filename)) { + case CFG_FILE_ERROR: + IOT_ERROR("Configuration file '%s' could not be read: %s\n", + filename, strerror(errno)); + return -1; + case CFG_SUCCESS: + break; + case CFG_PARSE_ERROR: + IOT_ERROR("Error parsing configuration file '%s'\n", filename); + return -1; + } + + return fill_aws_iot_config(aws_cfg); +} + + +/* + * free_configuration() - Release the configuration var + */ +void free_configuration(void) +{ + cfg_free(cfg); +} + +/* + * fill_aws_iot_config() - Fill the AWS IoT Device SDK configuration struct + * + * @aws_cfg: AWS IoT Device SDK configuration struct (aws_iot_cfg_t). + * + * Return: 0 if the configuration is filled successfully, -1 otherwise. + */ +static int fill_aws_iot_config(aws_iot_cfg_t *aws_cfg) +{ + aws_cfg->thing_name = cfg_getstr(cfg, SETTING_THING_NAME); + if (aws_cfg->thing_name == NULL) + return -1; + aws_cfg->client_id = cfg_getstr(cfg, SETTING_CLIENT_ID); + if (aws_cfg->client_id == NULL) + return -1; + aws_cfg->host = cfg_getstr(cfg, SETTING_HOST); + if (aws_cfg->host == NULL) + return -1; + aws_cfg->port = cfg_getint(cfg, SETTING_PORT); + aws_cfg->certs_path = cfg_getstr(cfg, SETTING_CERTS_PATH); + if (aws_cfg->certs_path == NULL) + return -1; + aws_cfg->rootca_fname = cfg_getstr(cfg, SETTING_ROOTCA_NAME); + if (aws_cfg->rootca_fname == NULL) + return -1; + aws_cfg->signed_cert_fname = cfg_getstr(cfg, SETTING_SCERT_NAME); + if (aws_cfg->signed_cert_fname == NULL) + return -1; + aws_cfg->priv_key_fname = cfg_getstr(cfg, SETTING_PRIVKEY_NAME); + if (aws_cfg->priv_key_fname == NULL) + return -1; + + return 0; +} + +/* + * cfg_check_thing_name() - Validate thing_name in the configuration file + * + * @cfg: The section where the option is defined. + * @opt: The option to check. + * + * @Return: 0 on success, any other value otherwise. + */ +static int cfg_check_thing_name(cfg_t *cfg, cfg_opt_t *opt) +{ + return cfg_check_string_length(cfg, opt, 1, MAX_SIZE_OF_THING_NAME); +} + +/* + * cfg_check_client_id() - Validate client_id in the configuration file + * + * @cfg: The section where the option is defined. + * @opt: The option to check. + * + * @Return: 0 on success, any other value otherwise. + */ +static int cfg_check_client_id(cfg_t *cfg, cfg_opt_t *opt) +{ + return cfg_check_string_length(cfg, opt, 1, MAX_SIZE_OF_UNIQUE_CLIENT_ID_BYTES); +} + +/* + * cfg_check_port() - Validate port is between 0 and 65535 + * + * @cfg: The section where the option is defined. + * @opt: The option to check. + * + * @Return: 0 on success, any other value otherwise. + */ +static int cfg_check_port(cfg_t *cfg, cfg_opt_t *opt) +{ + return cfg_check_int_range(cfg, opt, 0, 65535); +} + +/* + * cfg_check_certificates_path() - Validate certs_path value + * + * @cfg: The section where the option is defined. + * @opt: The option to check. + * + * @Return: 0 on success, any other value otherwise. + */ +static int cfg_check_certificates_path(cfg_t *cfg, cfg_opt_t *opt) +{ + DIR *dir = NULL; + char *val = cfg_opt_getnstr(opt, 0); + + dir = opendir(val); + + if (dir) { + closedir(dir); + return 0; + } else { + cfg_error(cfg, + "Invalid %s: cannot find directory '%s'", + opt->name, val); + return -1; + } +} + +/* + * cfg_check_cert_file() - Validate certificate file name value + * + * @cfg: The section where the option is defined. + * @opt: The option to check. + * + * @Return: 0 on success, any other value otherwise. + */ +static int cfg_check_cert_file(cfg_t *cfg, cfg_opt_t *opt) +{ + char *certs_path = NULL; + char *file_path = NULL; + char *val = cfg_opt_getnstr(opt, 0); + + if (cfg_check_string_length(cfg, opt, 1, 0) != 0) + return -1; + + certs_path = cfg_getstr(cfg, SETTING_CERTS_PATH); + if (certs_path == NULL) { + cfg_error(cfg, "Invalid %s: NULL value", SETTING_CERTS_PATH); + return -1; + } + + file_path = calloc(1, sizeof(*file_path) * (strlen(certs_path) + strlen(val) + 2)); + if (file_path == NULL) { + cfg_error(cfg, + "Error checking %s: unable to allocate memory", + opt->name); + return -1; + } + sprintf(file_path, "%s/%s", certs_path, val); + + if (!file_readable(file_path)) { + cfg_error(cfg, + "Invalid %s: cannot find file '%s'", opt->name, val); + free(file_path); + return -1; + } + + free(file_path); + return 0; +} + +/* + * cfg_check_int_range() - Validate a parameter int value is between the given range + * + * @cfg: The section where the option is defined. + * @opt: The option to check. + * @min: Minimum value of the parameter. + * @max: Maximum value of the parameter. + * + * @Return: 0 on success, any other value otherwise. + */ +static int cfg_check_int_range(cfg_t *cfg, cfg_opt_t *opt, uint32_t min, uint32_t max) +{ + unsigned long val = cfg_opt_getnint(opt, 0); + + if (val > max || val < min) { + cfg_error(cfg, + "Invalid %s (%d): value must be between %d and %d", + opt->name, val, min, max); + return -1; + } + return 0; +} + +/* + * cfg_check_empty_string() - Validate string is empty + * + * @cfg: The section where the option is defined. + * @opt: The option to check. + * + * @Return: 0 on success, any other value otherwise. + */ +static int cfg_check_empty_string(cfg_t *cfg, cfg_opt_t *opt) +{ + return cfg_check_string_length(cfg, opt, 1, 0); +} + +/* + * cfg_check_string_length() - Validate the length of a string is in range + * + * @cfg: The section where the option is defined. + * @opt: The option to check. + * @min: The string minimum length. + * @max: The string maximum length. 0 to unlimited. + * + * @Return: 0 on success, any other value otherwise. + */ +static int cfg_check_string_length(cfg_t *cfg, cfg_opt_t *opt, uint16_t min, uint16_t max) +{ + char *val = cfg_opt_getnstr(opt, 0); + + if ((val == NULL) || ((strlen(val) == 0) && (min > 0))) { + cfg_error(cfg, "Invalid %s (%s): cannot be empty", opt->name, val); + return -1; + } + if (strlen(val) < min) { + cfg_error(cfg, + "Invalid %s (%s): cannot be shorter than %d character(s)", + opt->name, val, min); + return -1; + } + if (max != 0 && strlen(val) > max) { + cfg_error(cfg, + "Invalid %s (%s): cannot be longer than %d character(s)", + opt->name, val, max); + return -1; + } + + return 0; +} + +/** + * file_exists() - Check that the file with the given name exists + * + * @filename: Full path of the file to check if it exists. + * + * Return: 1 if the file exits, 0 if it does not exist. + */ +static int file_exists(const char *const filename) +{ + return access(filename, F_OK) == 0; +} + +/** + * file_readable() - Check that the file with the given name can be read + * + * @filename: Full path of the file to check if it is readable. + * + * Return: 1 if the file is readable, 0 if it cannot be read. + */ +static int file_readable(const char *const filename) +{ + return access(filename, R_OK) == 0; +} diff --git a/awsiot-sample/src/aws_config.h b/awsiot-sample/src/aws_config.h new file mode 100644 index 0000000..1bd0fda --- /dev/null +++ b/awsiot-sample/src/aws_config.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2017 Digi International Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + * Digi International Inc. 11001 Bren Road East, Minnetonka, MN 55343 + * ======================================================================= + */ + +#ifndef AWS_CONFIG_H_ +#define AWS_CONFIG_H_ + +#include +#include +#include + +/*------------------------------------------------------------------------------ + D E F I N I T I O N S +------------------------------------------------------------------------------*/ +#define AWS_IOT_CONFIG_FILE "/etc/awsiotsdk.conf" +#define DEFAULT_CERTS_PATH "/etc/ssl/certs" + +/*------------------------------------------------------------------------------ + D A T A T Y P E S D E F I N I T I O N S +------------------------------------------------------------------------------*/ +/** + * aws_iot_cfg_t - AWS IoT Device SDK configuration type + * + * @thing_name: Thing Name of the Shadow this device is associated with + * @client_id: MQTT client ID. It should be unique for every device + * @host: MQTT host + * @port: Port for MQTT/S + * @certs_path: Absolute path to the certificates directory + * @rootca_fname: Name of the Root CA file + * @signed_cert_fname: Name of the device signed certificate + * @priv_key_fname: Name of the device private key + */ +typedef struct { + char *thing_name; + char *client_id; + char *host; + int port; + char *certs_path; + char *rootca_fname; + char *signed_cert_fname; + char *priv_key_fname; +} aws_iot_cfg_t; + +/*------------------------------------------------------------------------------ + F U N C T I O N D E C L A R A T I O N S +------------------------------------------------------------------------------*/ +int parse_configuration(const char *const filename, aws_iot_cfg_t *aws_cfg); +void free_configuration(void); + +#endif /* AWS_CONFIG_H_ */ diff --git a/awsiot-sample/src/main.c b/awsiot-sample/src/main.c index 738f274..3f5bf00 100644 --- a/awsiot-sample/src/main.c +++ b/awsiot-sample/src/main.c @@ -23,6 +23,7 @@ #include #include +#include "aws_config.h" #include "daemonize.h" /*------------------------------------------------------------------------------ @@ -38,6 +39,8 @@ "\n" \ "Usage: %s [options]\n\n" \ " -d --daemon Daemonize the process\n" \ + " -c --config-file= Use a custom configuration file instead of\n" \ + " the default one located in " AWS_IOT_CONFIG_FILE "\n" \ " -h --help Print help and exit\n" \ "\n" @@ -66,9 +69,10 @@ int main(int argc, char **argv) static int opt, opt_index; int create_daemon = 0; char *config_file = NULL; - static const char *short_options = "dh"; + static const char *short_options = "dc:h"; static const struct option long_options[] = { {"daemon", no_argument, NULL, 'd'}, + {"config-file", required_argument, NULL, 'c'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; @@ -83,6 +87,9 @@ int main(int argc, char **argv) case 'd': create_daemon = 1; break; + case 'c': + config_file = optarg; + break; case 'h': usage(name); goto done; @@ -119,9 +126,13 @@ done: static int start_aws_iot(const char *config_file) { int result = EXIT_SUCCESS; + aws_iot_cfg_t aws_cfg; add_sigkill_signal(); + if (parse_configuration(config_file ? config_file : AWS_IOT_CONFIG_FILE, &aws_cfg) != 0) + return EXIT_FAILURE;; + do { sleep(1); } while (!check_stop());