From 973dec14756e6b174e3d4581e82315682de575d6 Mon Sep 17 00:00:00 2001 From: Andrew Beltrano <2082148+abeltrano@users.noreply.github.com> Date: Mon, 7 Jun 2021 17:09:02 -0600 Subject: [PATCH] Add initial source code (#1) * Initial source commit --- .clang-format | 112 ++ .gitattributes | 3 + .gitignore | 383 +--- CMakeLists.txt | 56 + README.md | 33 +- SUPPORT.md | 23 +- cmake/find_cpprest.cmake | 7 + cmake/find_git.cmake | 2 + cmake/find_gpiod.cmake | 8 + cmake/find_jsonc.cmake | 8 + cmake/find_openssl.cmake | 2 + cmake/find_systemd.cmake | 8 + cmake/find_wpa_client.cmake | 12 + cmake/security_hardening.cmake | 37 + docs/ztppenguin.png | Bin 0 -> 32836 bytes external/cpprestsdk/CMakeLists.txt | 33 + external/hostap/.config | 2 + ...r-function-to-read-file-link-targets.patch | 69 + ...pa-conf-file-link-target-for-writing.patch | 70 + ...t-when-a-network-is-added-or-removed.patch | 67 + ...iguration-Request-timeout-in-hostapd.patch | 74 + ...onfiguration-Request-timeout-in-wpas.patch | 77 + ...load_dynamic_generic-externally-acce.patch | 350 ++++ ...from-tls_engine_load_dynamic_generic.patch | 88 + ...g-bootstrap-keys-using-an-OpenSSL-en.patch | 262 +++ external/hostap/CMakeLists.txt | 30 + samples/ztpd/config/config-bootstrapinfo.json | 122 ++ samples/ztpd/config/config-configurator.json | 65 + .../config/config-configurator.schema.json | 812 ++++++++ samples/ztpd/config/config-enrollee.json | 8 + .../ztpd/config/config-enrollee.schema.json | 93 + samples/ztpd/config/config.json | 23 + samples/ztpd/config/config.schema.json | 269 +++ scripts/ztp-keyinfo.sh | 65 + scripts/ztp-release.sh | 96 + src/CMakeLists.txt | 8 + src/core/CMakeLists.txt | 49 + src/core/biproviders/CMakeLists.txt | 18 + src/core/biproviders/azuredps/CMakeLists.txt | 78 + .../azuredps/azure_dps_service_client.cpp | 478 +++++ .../azuredps/azure_dps_service_client.h | 105 + .../azuredps/azure_dps_service_client_log.h | 11 + .../azure_dps_service_client_proxy.cpp | 161 ++ .../azuredps/azure_dps_service_client_proxy.h | 73 + .../bootstrap_info_provider_azure_dps.c | 250 +++ .../bootstrap_info_provider_azure_dps.h | 32 + ...bootstrap_info_provider_azure_dps_config.c | 207 ++ ...bootstrap_info_provider_azure_dps_config.h | 60 + src/core/biproviders/azuredps/sample.cpp | 39 + src/core/biproviders/azuredps/sample_input.h | 24 + .../biproviders/bootstrap_info_provider.c | 369 ++++ .../biproviders/bootstrap_info_provider.h | 330 ++++ .../bootstrap_info_provider_private.h | 34 + .../bootstrap_info_provider_settings.c | 56 + .../bootstrap_info_provider_settings.h | 48 + src/core/biproviders/file/CMakeLists.txt | 13 + .../file/bootstrap_info_provider_file.c | 411 ++++ .../file/bootstrap_info_provider_file.h | 30 + .../bootstrap_info_provider_file_config.c | 215 +++ .../bootstrap_info_provider_file_config.h | 57 + src/core/ztp.c | 31 + src/core/ztp.h | 55 + src/core/ztp_configurator.c | 577 ++++++ src/core/ztp_configurator.h | 59 + src/core/ztp_configurator_config.c | 1701 +++++++++++++++++ src/core/ztp_configurator_config.h | 77 + src/core/ztp_enrollee.c | 1167 +++++++++++ src/core/ztp_enrollee.h | 68 + src/core/ztp_enrollee_config.c | 328 ++++ src/core/ztp_enrollee_config.h | 41 + src/core/ztp_settings.c | 981 ++++++++++ src/core/ztp_settings.h | 170 ++ src/core/ztp_wpa_supplicant.c | 756 ++++++++ src/core/ztp_wpa_supplicant.h | 133 ++ src/dbus/CMakeLists.txt | 32 + src/dbus/dbus_common.h | 32 + src/dbus/dbus_message_helpers.c | 665 +++++++ src/dbus/dbus_message_helpers.h | 238 +++ src/dbus/ztp_dbus_client.c | 266 +++ src/dbus/ztp_dbus_client.h | 78 + src/dbus/ztp_dbus_configurator.c | 793 ++++++++ src/dbus/ztp_dbus_configurator.h | 82 + src/dbus/ztp_dbus_network_configuration.c | 636 ++++++ src/dbus/ztp_dbus_network_configuration.h | 117 ++ src/dbus/ztp_dbus_server.c | 1563 +++++++++++++++ src/dbus/ztp_dbus_server.h | 97 + src/systemd/CMakeLists.txt | 22 + src/systemd/ztp_systemd.c | 130 ++ src/systemd/ztp_systemd.h | 57 + src/utils/CMakeLists.txt | 22 + src/utils/event_loop.c | 671 +++++++ src/utils/event_loop.h | 214 +++ src/utils/file_utils.c | 72 + src/utils/file_utils.h | 18 + src/utils/json_parse.c | 418 ++++ src/utils/json_parse.h | 217 +++ src/utils/led_ctrl.c | 340 ++++ src/utils/led_ctrl.h | 121 ++ src/utils/string_utils.c | 150 ++ src/utils/string_utils.h | 80 + src/utils/string_utils.hpp | 102 + src/utils/time_utils.c | 28 + src/utils/time_utils.h | 67 + src/utils/userspace/linux/compiler.h | 12 + src/utils/userspace/linux/kernel.h | 22 + src/utils/userspace/linux/list.h | 590 ++++++ src/utils/userspace/linux/poison.h | 17 + src/utils/userspace/linux/types.h | 17 + src/utils/ztp_log.h | 77 + src/wifi/CMakeLists.txt | 22 + src/wifi/dpp.c | 764 ++++++++ src/wifi/dpp.h | 543 ++++++ src/wpas/CMakeLists.txt | 31 + src/wpas/wpa_controller.c | 1111 +++++++++++ src/wpas/wpa_controller.h | 316 +++ src/wpas/wpa_controller_watcher.c | 446 +++++ src/wpas/wpa_controller_watcher.h | 94 + src/wpas/wpa_core.c | 74 + src/wpas/wpa_core.h | 61 + src/wpas/wpa_supplicant.c | 57 + src/wpas/wpa_supplicant.h | 50 + src/ztpd/CMakeLists.txt | 40 + src/ztpd/main.c | 320 ++++ src/ztpd/systemd/CMakeLists.txt | 15 + src/ztpd/systemd/com.microsoft.ztp1.service | 5 + src/ztpd/systemd/ztpd.service.in | 13 + src/ztpd/ztpd.c | 519 +++++ src/ztpd/ztpd.h | 86 + src/ztpd/ztpd_ui.c | 55 + src/ztpd/ztpd_ui.h | 25 + 130 files changed, 25007 insertions(+), 372 deletions(-) create mode 100644 .clang-format create mode 100644 .gitattributes create mode 100644 CMakeLists.txt create mode 100644 cmake/find_cpprest.cmake create mode 100644 cmake/find_git.cmake create mode 100644 cmake/find_gpiod.cmake create mode 100644 cmake/find_jsonc.cmake create mode 100644 cmake/find_openssl.cmake create mode 100644 cmake/find_systemd.cmake create mode 100644 cmake/find_wpa_client.cmake create mode 100644 cmake/security_hardening.cmake create mode 100644 docs/ztppenguin.png create mode 100644 external/cpprestsdk/CMakeLists.txt create mode 100644 external/hostap/.config create mode 100755 external/hostap/0001-Add-helper-function-to-read-file-link-targets.patch create mode 100755 external/hostap/0002-Use-wpa-conf-file-link-target-for-writing.patch create mode 100755 external/hostap/0003-Generate-an-event-when-a-network-is-added-or-removed.patch create mode 100755 external/hostap/0004-DPP-Add-Configuration-Request-timeout-in-hostapd.patch create mode 100755 external/hostap/0005-DPP-Add-Configuration-Request-timeout-in-wpas.patch create mode 100755 external/hostap/0006-Make-tls_engine_load_dynamic_generic-externally-acce.patch create mode 100755 external/hostap/0007-Drop-tls-prefix-from-tls_engine_load_dynamic_generic.patch create mode 100755 external/hostap/0008-DPP-Allow-loading-bootstrap-keys-using-an-OpenSSL-en.patch create mode 100644 external/hostap/CMakeLists.txt create mode 100644 samples/ztpd/config/config-bootstrapinfo.json create mode 100644 samples/ztpd/config/config-configurator.json create mode 100644 samples/ztpd/config/config-configurator.schema.json create mode 100644 samples/ztpd/config/config-enrollee.json create mode 100644 samples/ztpd/config/config-enrollee.schema.json create mode 100644 samples/ztpd/config/config.json create mode 100644 samples/ztpd/config/config.schema.json create mode 100755 scripts/ztp-keyinfo.sh create mode 100755 scripts/ztp-release.sh create mode 100644 src/CMakeLists.txt create mode 100644 src/core/CMakeLists.txt create mode 100644 src/core/biproviders/CMakeLists.txt create mode 100644 src/core/biproviders/azuredps/CMakeLists.txt create mode 100644 src/core/biproviders/azuredps/azure_dps_service_client.cpp create mode 100644 src/core/biproviders/azuredps/azure_dps_service_client.h create mode 100644 src/core/biproviders/azuredps/azure_dps_service_client_log.h create mode 100644 src/core/biproviders/azuredps/azure_dps_service_client_proxy.cpp create mode 100644 src/core/biproviders/azuredps/azure_dps_service_client_proxy.h create mode 100644 src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps.c create mode 100644 src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps.h create mode 100644 src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps_config.c create mode 100644 src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps_config.h create mode 100644 src/core/biproviders/azuredps/sample.cpp create mode 100644 src/core/biproviders/azuredps/sample_input.h create mode 100644 src/core/biproviders/bootstrap_info_provider.c create mode 100644 src/core/biproviders/bootstrap_info_provider.h create mode 100644 src/core/biproviders/bootstrap_info_provider_private.h create mode 100644 src/core/biproviders/bootstrap_info_provider_settings.c create mode 100644 src/core/biproviders/bootstrap_info_provider_settings.h create mode 100644 src/core/biproviders/file/CMakeLists.txt create mode 100644 src/core/biproviders/file/bootstrap_info_provider_file.c create mode 100644 src/core/biproviders/file/bootstrap_info_provider_file.h create mode 100644 src/core/biproviders/file/bootstrap_info_provider_file_config.c create mode 100644 src/core/biproviders/file/bootstrap_info_provider_file_config.h create mode 100644 src/core/ztp.c create mode 100644 src/core/ztp.h create mode 100644 src/core/ztp_configurator.c create mode 100644 src/core/ztp_configurator.h create mode 100644 src/core/ztp_configurator_config.c create mode 100644 src/core/ztp_configurator_config.h create mode 100644 src/core/ztp_enrollee.c create mode 100644 src/core/ztp_enrollee.h create mode 100644 src/core/ztp_enrollee_config.c create mode 100644 src/core/ztp_enrollee_config.h create mode 100644 src/core/ztp_settings.c create mode 100644 src/core/ztp_settings.h create mode 100644 src/core/ztp_wpa_supplicant.c create mode 100644 src/core/ztp_wpa_supplicant.h create mode 100644 src/dbus/CMakeLists.txt create mode 100644 src/dbus/dbus_common.h create mode 100644 src/dbus/dbus_message_helpers.c create mode 100644 src/dbus/dbus_message_helpers.h create mode 100644 src/dbus/ztp_dbus_client.c create mode 100644 src/dbus/ztp_dbus_client.h create mode 100644 src/dbus/ztp_dbus_configurator.c create mode 100644 src/dbus/ztp_dbus_configurator.h create mode 100644 src/dbus/ztp_dbus_network_configuration.c create mode 100644 src/dbus/ztp_dbus_network_configuration.h create mode 100644 src/dbus/ztp_dbus_server.c create mode 100644 src/dbus/ztp_dbus_server.h create mode 100644 src/systemd/CMakeLists.txt create mode 100644 src/systemd/ztp_systemd.c create mode 100644 src/systemd/ztp_systemd.h create mode 100644 src/utils/CMakeLists.txt create mode 100644 src/utils/event_loop.c create mode 100644 src/utils/event_loop.h create mode 100644 src/utils/file_utils.c create mode 100644 src/utils/file_utils.h create mode 100644 src/utils/json_parse.c create mode 100644 src/utils/json_parse.h create mode 100644 src/utils/led_ctrl.c create mode 100644 src/utils/led_ctrl.h create mode 100644 src/utils/string_utils.c create mode 100644 src/utils/string_utils.h create mode 100644 src/utils/string_utils.hpp create mode 100644 src/utils/time_utils.c create mode 100644 src/utils/time_utils.h create mode 100644 src/utils/userspace/linux/compiler.h create mode 100644 src/utils/userspace/linux/kernel.h create mode 100644 src/utils/userspace/linux/list.h create mode 100644 src/utils/userspace/linux/poison.h create mode 100644 src/utils/userspace/linux/types.h create mode 100644 src/utils/ztp_log.h create mode 100644 src/wifi/CMakeLists.txt create mode 100644 src/wifi/dpp.c create mode 100644 src/wifi/dpp.h create mode 100644 src/wpas/CMakeLists.txt create mode 100644 src/wpas/wpa_controller.c create mode 100644 src/wpas/wpa_controller.h create mode 100644 src/wpas/wpa_controller_watcher.c create mode 100644 src/wpas/wpa_controller_watcher.h create mode 100644 src/wpas/wpa_core.c create mode 100644 src/wpas/wpa_core.h create mode 100644 src/wpas/wpa_supplicant.c create mode 100644 src/wpas/wpa_supplicant.h create mode 100644 src/ztpd/CMakeLists.txt create mode 100644 src/ztpd/main.c create mode 100644 src/ztpd/systemd/CMakeLists.txt create mode 100644 src/ztpd/systemd/com.microsoft.ztp1.service create mode 100644 src/ztpd/systemd/ztpd.service.in create mode 100644 src/ztpd/ztpd.c create mode 100644 src/ztpd/ztpd.h create mode 100644 src/ztpd/ztpd_ui.c create mode 100644 src/ztpd/ztpd_ui.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..57834dc --- /dev/null +++ b/.clang-format @@ -0,0 +1,112 @@ +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: false +# AlignConsecutiveBitFields: true +AlignConsecutiveDeclarations: false +# AlignConsecutiveMacros: true +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: true +# AllowAllArgumentsOnNextLine: true +# AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: false +# AllowShortLambdasOnASingleLine: Inline +# AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: InlineOnly +# AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterReturnType: All +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BasedOnStyle: WebKit +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + # AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakInheritanceList: AfterColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: AfterColon +BreakStringLiterals: false +ColumnLimit: 0 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth : 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +# DeriveLineEnding: true +DerivePointerAlignment: true +FixNamespaceComments: true +ForEachMacros: [ + 'list_for_each', + 'list_for_each_prev', + 'list_for_each_safe', + 'list_for_each_prev_safe', + 'list_for_each_entry', + 'list_for_each_entry_reverse', + 'list_for_each_entry_continue', + 'list_for_each_entry_continue_reverse', + 'list_for_each_entry_from', + 'list_for_each_entry_from_reverse', + 'list_for_each_entry_safe', + 'list_for_each_entry_safe_continue', + 'list_for_each_entry_safe_from', + 'list_for_each_entry_safe_reverse' +] +IncludeBlocks: Preserve +# IndentCaseBlocks: false +IndentCaseLabels: true +# IndentExternBlock: NoIndent +# IndentGotoLabels: false +IndentPPDirectives: None +IndentWidth: 4 +# InsertTrailingCommas: Wrapped +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +# SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +# SpaceAroundPointerQualifiers: After +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +# SpaceBeforeSquareBrackets: false +# SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +# SpacesInConditionalStatement: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +Standard: Cpp11 +# UseCRLF: false +UseTab: Never \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..301d73d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +.gitattributes export-ignore +.gitignore export-ignore +.vscode/ export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index dfcfd56..d7fff70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,350 +1,33 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ +*.d +*.o +*.ko +*.obj +*.elf +*.ilk +*.map +*.exp +*.gch +*.pch +*.lib +*.a +*.la +*.lo +*.dll +*.so +*.so.* +*.dylib +*.out +*.app +*.hex +*.su +*.idb +*.pdb +*.mod* +*.cmd +bin/ +compile_commands.json +.ccls-cache/ +build*/ +debug/ +release/ +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8ddd6c3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,56 @@ +# project options +cmake_minimum_required(VERSION 3.10) +project(ztp C CXX) + +option( + BUILD_CPPREST_EXTERNAL + "Build cpprestsdk from built-in external source" + ON +) + +option( + BUILD_HOSTAP_EXTERNAL + "Build wpa_supplicant/hostapd from built-in external source" + ON +) + +option( + WERROR + "Treat compiler warnings as errors" + ON +) + +# specify the C standard +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED True) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_EXTENSIONS OFF) + +include(CheckCCompilerFlag) +include(ExternalProject) +include(GNUInstallDirs) +include(cmake/find_git.cmake) +include(cmake/find_openssl.cmake) +include(cmake/find_cpprest.cmake) +include(cmake/find_wpa_client.cmake) +include(cmake/find_systemd.cmake) +include(cmake/find_gpiod.cmake) +include(cmake/find_jsonc.cmake) +include(cmake/security_hardening.cmake) + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +add_compile_options( + -Wall + -Wshadow + -Wformat-security + -Wextra + -Wpedantic + -Wconversion +) + +if (WERROR) + add_compile_options(-Werror) +endif() + +add_subdirectory(src) diff --git a/README.md b/README.md index 5aa7cfb..1005613 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,37 @@ # Wi-Fi Zero Touch Provisioning (ZTP) + +![ZTP Penguin](docs/ztppenguin.png) + This project provides a Wi-Fi zero touch provisioning library and daemon for Linux. It supports provisioning of Wi-Fi credentials using [Wi-Fi Easy Connect](https://www.wi-fi.org/discover-wi-fi/wi-fi-easy-connect), also known as the Device Provisioning Protocol (DPP). ## Building -### Ubuntu (bionic, focal) + +### Ubuntu (focal) + Install package dependencies: + +```bash +sudo apt install \ + build-essential \ + cmake \ + git \ + libboost-filesystem-dev \ + libboost-random-dev \ + libboost-regex-dev \ + libboost-system-dev \ + libboost-thread-dev \ + libbrotli-dev \ + libgpiod-dev \ + libjson-c-dev \ + libssl-dev \ + libsystemd-dev \ + pkg-config \ + zlib1g-dev \ ``` -sudo apt install -y build-essential git cmake libssl-dev libboost-system-dev libsystemd-dev libjson-c-dev libgpiod-dev libboost-random-dev libboost-thread-dev libboost-filesystem-dev libboost-regex-dev zlib1g-dev libbrotli-dev pkg-config -``` + Checkout and build: -``` + +```bash git clone git@github.com:microsoft/wifi-ztp.git cd wifi-ztp mkdir build && cd $_ @@ -20,7 +43,7 @@ make This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. +the rights to use your contribution. For details, visit [https://cla.opensource.microsoft.com](https://cla.opensource.microsoft.com). When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions diff --git a/SUPPORT.md b/SUPPORT.md index dc72f0e..b69d58e 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,25 +1,14 @@ -# TODO: The maintainer of this repo has not yet edited this file - -**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - -- **No CSS support:** Fill out this template with information about how to file issues and get help. -- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). -- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. - -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* - # Support ## How to file issues and get help -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. +This project uses GitHub Issues to track bugs and feature requests. Please +search the existing issues before filing new issues to avoid duplicates. For +new issues, file your bug or feature request as a new Issue. -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE -FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER -CHANNEL. WHERE WILL YOU HELP PEOPLE?**. +For help and questions about using this project, please contact +. ## Microsoft Support Policy -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. +Support for this project is limited to the resources listed above. diff --git a/cmake/find_cpprest.cmake b/cmake/find_cpprest.cmake new file mode 100644 index 0000000..01e0d2e --- /dev/null +++ b/cmake/find_cpprest.cmake @@ -0,0 +1,7 @@ + +if (BUILD_CPPREST_EXTERNAL) + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../external/cpprestsdk) +else() + find_package(cpprestsdk CONFIG REQUIRED) + find_package(Boost 1.65 COMPONENTS system REQUIRED) +endif() diff --git a/cmake/find_git.cmake b/cmake/find_git.cmake new file mode 100644 index 0000000..d73d727 --- /dev/null +++ b/cmake/find_git.cmake @@ -0,0 +1,2 @@ + +find_package(Git REQUIRED) diff --git a/cmake/find_gpiod.cmake b/cmake/find_gpiod.cmake new file mode 100644 index 0000000..2769939 --- /dev/null +++ b/cmake/find_gpiod.cmake @@ -0,0 +1,8 @@ +find_library(LIBGPIOD + NAMES libgpiod.so + REQUIRED) + +if(LIBGPIOD) + set(LIBGPIOD_TARGET ${LIBGPIOD}) + MESSAGE(STATUS "Found libgpiod: ${LIBGPIOD}") +endif() diff --git a/cmake/find_jsonc.cmake b/cmake/find_jsonc.cmake new file mode 100644 index 0000000..7091051 --- /dev/null +++ b/cmake/find_jsonc.cmake @@ -0,0 +1,8 @@ +find_library(LIBJSONC + NAMES libjson-c.so + REQUIRED) + +if(LIBJSONC) + set(LIBJSONC_TARGET ${LIBJSONC}) + MESSAGE(STATUS "Found json-c: ${LIBJSONC}") +endif() diff --git a/cmake/find_openssl.cmake b/cmake/find_openssl.cmake new file mode 100644 index 0000000..059168f --- /dev/null +++ b/cmake/find_openssl.cmake @@ -0,0 +1,2 @@ + +find_package(OpenSSL) \ No newline at end of file diff --git a/cmake/find_systemd.cmake b/cmake/find_systemd.cmake new file mode 100644 index 0000000..ef0f1e3 --- /dev/null +++ b/cmake/find_systemd.cmake @@ -0,0 +1,8 @@ +find_library(LIBSYSTEMD + NAMES libsystemd.so + REQUIRED) + +if(LIBSYSTEMD) + set(LIBSYSTEMD_TARGET ${LIBSYSTEMD}) + MESSAGE(STATUS "Found systemd: ${LIBSYSTEMD}") +endif() diff --git a/cmake/find_wpa_client.cmake b/cmake/find_wpa_client.cmake new file mode 100644 index 0000000..5e0d9d0 --- /dev/null +++ b/cmake/find_wpa_client.cmake @@ -0,0 +1,12 @@ +if (BUILD_HOSTAP_EXTERNAL) + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../external/hostap) +else() + find_library(LIBWPA_CLIENT + NAMES libwpa_client.so + REQUIRED) +endif() + +if(LIBWPA_CLIENT) + set(LIBWPA_CLIENT_TARGET ${LIBWPA_CLIENT}) + MESSAGE(STATUS "Found wpa client: ${LIBWPA_CLIENT}") +endif() diff --git a/cmake/security_hardening.cmake b/cmake/security_hardening.cmake new file mode 100644 index 0000000..b83cdd1 --- /dev/null +++ b/cmake/security_hardening.cmake @@ -0,0 +1,37 @@ +set(C_COMPILER_HARDENING_FLAGS "") + +if (CMAKE_C_COMPILER_ID STREQUAL "Clang") + check_c_compiler_flag(-fstack-protector HAS_STACK_PROTECTOR) + + if (HAS_STACK_PROTECTOR) + set(C_COMPILER_HARDENING_FLAGS "${C_COMPILER_HARDENING_FLAGS} -fstack-protector") + endif() + + set(C_COMPILER_HARDENING_FLAGS "${C_COMPILER_HARDENING_FLAGS} -fvisibility=hidden") +elseif (CMAKE_C_COMPILER_ID STREQUAL "GNU") + check_c_compiler_flag(-fstack-clash-protection HAS_STACK_CLASH_PROTECTION) + check_c_compiler_flag(-fstack-protector-all HAS_STACK_PROTECTOR_ALL) + check_c_compiler_flag(-fvisibility=hidden HAS_VISIBILITY) + + if (HAS_STACK_CLASH_PROTECTION) + set(C_COMPILER_HARDENING_FLAGS "${C_COMPILER_HARDENING_FLAGS} -fstack-clash-protection") + endif() + + if (HAS_STACK_PROTECTOR_ALL) + set(C_COMPILER_HARDENING_FLAGS "${C_COMPILER_HARDENING_FLAGS} -fstack-protector-all") + endif() + + if (HAS_VISIBILITY) + set(C_COMPILER_HARDENING_FLAGS "${C_COMPILER_HARDENING_FLAGS} -fvisibility=hidden") + endif() + + set(C_COMPILER_HARDENING_FLAGS "${C_COMPILER_HARDENING_FLAGS} -Wl,-z,noexecstack") + set(C_COMPILER_HARDENING_FLAGS "${C_COMPILER_HARDENING_FLAGS} -Wl,-z,now") + set(C_COMPILER_HARDENING_FLAGS "${C_COMPILER_HARDENING_FLAGS} -Wl,-z,relro") +endif() + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_COMPILER_HARDENING_FLAGS}") + +if (CMAKE_BUILD_TYPE MATCHES "(Release|RelWithDebInfo|MinSizeRel)") + add_compile_definitions(_FORTIFY_SOURCE=2) +endif() \ No newline at end of file diff --git a/docs/ztppenguin.png b/docs/ztppenguin.png new file mode 100644 index 0000000000000000000000000000000000000000..11dacf8b6da73dc38e0e209ad64bff7477c85328 GIT binary patch literal 32836 zcmcee1y@^5yM}SsP_z^&?(XhRk>F0DxVyVM1a}Ya?p~m{yA>#I#i5+M-;X$vm6h39 z+h*pm`xu|h$e8bd+xXFx&WJLYt%3PLs@m@3FfLVf)A zEa)jufwUmH$|*@99wH#Z!Ex0POS(Wo1<1-tiUB>>FLFIRiI`S-U zpsIp&L5D}mzqjBXg)vorPxrk9vK#ZVVrL*LN+T~D7r?cYoUw!k*-e)dJrQUJ1Dcd* z8K&H69{PuS>R>}DgKmq{98!YT`dvXP8ii>tx<>gnc&tHF{m1?6yAr#8AgX1kj7v#U zg7(J-(iBRy=YC3Kyn0Ml#Fl2%`L4+hee&R2zjsW}&z_jtXw@UQb^dWaS1*Ke0`3wj zX$wL9(EXl^jSe24z_pg*I(iC$=|LWi0i5%frXP$jbjPi{{ zh7gtut82A_v{ZWQe>|G}^xO0S6*oI)eS^n;9c>c- z{Bu`Z-HZcI37#jaC%_YW6-xVZX#TPNu^_=|xDsoX^a;h0mws^5E$>kXdr|q|JMGO2 znpXSECy(>jK*|_vT1O;DgnE_mrQW z{yoctxM5!P*%u#btSC9j1^qJ>uZ;@Eq4lo9aT7|*@2L8wMTO;wcFN}m0A2=WDk&dv zYO_#U?x6#{N>N&jW@a3q>NTjZV36GTlJb?~Qu{|_NklCS?`K|A+_y}I@5Ev{ayBYd zI(WtOpKTFZix|suVz1ptmqn33ut4zK=&9#I*i^P5<6jsUF}h*j5OBvd@?#FxNu9>n zU@1q`nBbVj_!eJZ_M%|j`kjrjC;A%E%*D80>Y)tRPE8f!wlCV7@jI;M@Cbl zDYT7BpT^`HGJHJM-aLBFYq@}FgUTQ&RdurQeNfANb7;rqyURGQoI-3A;4*!7H6~@l zy}F`L;VC+FFFx!z8DU!JUM_=v&u2QOfaj_TWK9>46#DhiztarP!qkV7m}M`-u(?vv zLVSlgm2~t<`Se#xvl%h{9w+iQdGN)^E3vy_@WCckk-`L}((lAj8!FqAQe<1XC*WWl z;K6Ztz7*f_(xWi6nDR|(u+^~pIEIq=h*8Htzw=wNwvr>Yy#wzu6S4x;+pO0{=VY(& zV8^4E8s7;?pb4!LW^4og(M&R7WD7a%HPjTBxKJb6492b1oSczNd@*Dhg~J1s$3ca2 zI%E$0IcF<;3)4?>xaCbklQ@l<^fG6{^Y^OvsL7jfBYf`mKrd73&~i&90&NZB{f-=Xay}u4?OLnC-D}gcatqGF{!oEeV^BsXOV0@zQ=Zo2cnOL| zt~x%o{;L9xbqjDHK&n=w$MhHWrw)N|f(Ojst;590+Jafy` zI1Edcj!?+EnL3oV*{%zXc7f9l1ai&*HEJ(3!vKZcktIt&O5%|%;*r*LhX}^GR#^he zHF_3^A!0;CL@zIm?BN_&uE4PpQrOqTwF+|8A~(Kz@3ssVKR=i?>T!KxyroYCkV7^Q zJ@H*z6j&8t{sF5NV~JaK)~3tRjjgo`=^?RwA&5Bb(~hhxzm!4Nu%WCZmnP8Y^g_MP z1UV;R?HdfS_Fg~x?#&3lR@bp`QBY3xHT1XiSD3YKWy6t3L@o0ZtYY4a_O|{WcZPGs zXQdu8x*@st+tomL0$l&3NHAM#G>6YBn@Ut8iK)!&MXU_>qh>ZJ@+Rwr7o-pMlv-%c zV(EKptYZA4=;aqcH2lTs$y12Qy8DUDT^SaE#`r;m*{rZi% zD6aFY1-*fI>Prk>NzEX3Q8DFW2#K&qAc3++SjJq0&f#~XCrOa8-Rbp9E3?vql7Svk zdpONo#Mot1TCm^hbUSMd+TD>~f8#?syxZgmiAX3udQAxY0_$Xt(QYH}okI=Phlug( zT1zm-I=R!WA3aY(N(%nQQ*h_(K99Pz8)3<28-izrPK&Dk^xV0hy;+_`F4Fc3i zjBhp(h&=-XFgP)r`PNtBsGFqtt~o&YR13c(bw0y3vYTmW;AWNKdI{!{%F{U%UyYy` zw_@#McGl+W>+4s4%i^r4o1sx>gr0^ak=BvP>`%Bg_e+7+Mm7^gu6J>XR&l6tC~5^Y z4M>ANPRS8eLgptXKQ{OI07LhKAGO=$~I+K=WVa z6vGP!Nv09H3HjgfcE9eb67XWah<3yePd!4f6bw^tU~c@_mEUzh3&i`KnwkprtTypE zP~qplr_xZYeT7|H^5M^F@|rceODpA&%iUx`s70KHhau$uix0+s{LIQq@&MgHsfdCm z1m7OJ?X?;X(^lZ_`{LGX?Bv8|vs^tF}3B8nEU5_76%&0Y}02 zdXOEY14t4q63eVsWa(rc@1I?9(0PYiFC@6^3KaM7=By=q;2dN|ip}@yX^PXH-?x!> z>aN_AQ{EB#yM+Z)vHR`7`Xl}ny12NwtF3W%8)LF`m?vj$^&H={5DyOzzYw7z5OP@qjq=q-WgDCQ3cP;+evS$T^z66E*1pjbH^ zSEKslc!e-uDc;rUiK&a!u;gD~Bz0Ah-%wH=bDNGHR~x?1aWat}`b?gk-N}W(?%zBy zZnVC>2(l~BU+&%aADA6|@uan;*~+eRY2tXvHRcN;hO4FK@X}tRUFXF(4B(9=#%0=Q zWm9rNd-sikbhsJ8C=7Wh7!3Y%wlI$OS@kB2#bQ^R=-O^EhW=O!`Xbl2kDSiw?-}^1 z+?5Ryf^8dS`b&Ki9a$t!cD>}2u(F~nv$|vm{pm{et9MtFeAuzP{v=R**3_y-GHQ8@ zkk^)tzMVe>SJ6AnpGq6Ur|1;fd{FKU?OF&FmBTdm!lex3UK_c*?Q^B1q!d?s_R_01 z@sAf#mxC~aR|Q5+U=J2FTRq<2P*%*&75r|s_%dbYhR_0cpGKG-B+(mz60sUux#$Uh z4VSr+DikQ-1ZZ$EajrJ($r>FURU;p6?iEf)!)~_o*)e8aT{%wTO12$4R`rSyV0ZkP zlKZW%&wglWRrC;8Xe#3zvDy?M1ZYE~KK2Ek3dyy&xs zVVTywym;@3qBvN(#GPV(J{)}#ncw02C6lVTUEJroGwwAYPc)nfKEHFl+XUP&u?$V~ zq~9G|gG%pN*aJSwihOln{~rD(5O8NStiw+je`g`;zFSn|-g{QTs6VC%DsAEU8)unW z7PWJQm^uw=lAESu9d5lqnQVM{5p0a-aNsFUE)|*RC+`cMnE&a;z)%j)V09gjsQjy+ z{3Ar(%d)LYX+B<&OTIae`tH&38&D#ak5JgYElTpMF#8ToodTfCQw?Iu%sjQ~RvW9? zpoYTH&n^_s7=*>C6db1>xt=u6jZl#=1qj?#X2Xy;2pE+h5>3U z#^WvD5i-{BCnd;35j8k#S6G!&Mn|Ow_gKrU05Kz$mX>6IsKml8A*F@p26S}v7tt7R zhXl%s5BzJH`JKJI5PBBSf|a@SOTP?adma9EahM{E`#S; zO`CS388Xz#Q+_DcYO4^+wO(3k6g?te0#wNa#~XU(+(e-j$)zGNy{OIGMff2!=TkLh zSU#+8$ltL`xtul?4@}S#Ux#`#XO3$&n&D|LZT@nY*(m;;fuJb3Cm>M`5G8s2`i@4# z8EMHWZ)e98ZnAsh>HU{VnNtWApZWKG49kxR5M47fqPm{mn`bqb zDn5l@djrO^GCw97|DqDl=_a7K5g?YRf3>7Mg@0j7Oks+4R+YpFPK{Xa6ylf~ddK8N z*Up(3p^iHCT+BX?WYoVsqn(q-X!?nw{4ya6YUxl2LEdO>V2mk_=Z~)QzpnxUfwPN? z!$ndtp1VPa+8xeRUS3-pipgq#gfYI4x4Vw>#z|U6#`C|&B2T-pwVkK*X-s<3WLUCp znNML>SrL|5t@cw{io==@n91y-%rYH9^Cyg3f%H{xm_LPrezx4n;YA(BACE$QwfOT z2TOUozvjfi2G`#g1UIX|>2GEFIFb5potWZF9rA7L8%e{zoD8czf$Ve?gcgt;FGCES%#T+2<#-% zI4V^xV2p{5Mp0E&Ez|-638wQa%$`_eqBN=5kM&$wtzC@u(>yTIlP~=k@xGt0HsttR zcI5K+(kSQrJWMx=A3G>4c4k(S=Tth#ahaBD?zqB+3?#FBzXx(aREeuWs~!F&0$*6} z4c>%Nw(_TLM2$r!*V;HUW#lvtUJ2EWs*_KLF~x62F~h@AMMXuXypEI;Y|pIMD3wMV zHT_x)^z_nhGTq75&F0`kJ#NV}R!|)y&U5nC=W5TK|Ax(FG9Ua9tGBH=GP&zUE2__51z02t|E&HV-kP3y zV+8sc*76C8!&u{%TZ2-KVmjtwy*~7?yw9pSUfNQW&H~p2*WZARcUmf6+~Z}Q+x`Uu z>Y4(LJtQYH2_{w2ma7O!P=cJ#KZppjOUF$Q&^pX@KH=dx|0LgHI57kQhX=z@=QwK& z#2W0fB`}3Ry!K34MLfHS+RXEY2d!TdEv(?DC{81#Nf25ms%;-y&T_tnCj)lu3TH_L)r5ml zqjFYCT^(L{z9X7kK zjpmc>2k`=)4{H`1+Uk%_rZG1+Z&nV0oU$lcf1c0f8g;ba`^&w)_ets7LtEeB`{T~> zUIgh;FegsMr!r z*$^u+S1h9r1TNlQmCKx@S}YC8-d4Hs674qNLI$`i z*jnveW`rKEkFp&AXb3m2?~q<;G#x>?>^!UG{zq@HLdfw$k$&1EE+~~W6Cj5?{}Dm| zmQIgQIiO(FJY$odgT)w82$nCR>m%nOK){3#;VkW?yoNbG2xa(`s zTpxjHw9DS#Hx)0OW!`CXK9tY%I?6QYm5`V7_ZN2PxyMsTXSQ>Rhx7x1bizTF!N7Vd zReuP$LRcLLt;&FX7LR%hyEp+f5MDec% zEg>fSwrvXqWMSz=KvS`kJ9c0dCF)>pZ3^i&rmirOgb2Hh)*FcJZ+&aT3RaxyMlY zTv7n7B_K1K5@Z?ov;Rghd0y*ll5C7fWov8NR5s65vWO)CLJU4jS+9Tj=&P^rI)~YY z-l&F_R$IfUD005JPPIW->(te1Ypyk;X8z0x@pmfv49?u`@C8Q{i-#7=`;#f9&L>zE zFE=_9*aX~9*QH}A?PO7aif<~y1stmX3Sv~(E=Y^j)S1iQXF~zr1pS%Kf^SIXRz10U zd-Fkch0&O0n_9|9L0E&*=FkOJM0#!Zr5!qi`f27Z*!W5dtKD84adC07bZf0{3^prv zWcH_=GKGBfmhmY2X#=B58=hD1n5%|OUluqabLoqKyDPo$jc4~%-^${man=udDof5k z-h6!miJZ;}Me@Pr(fEgxM4Kxsixx*Nkc<+jb(go3FTz12w3f&YI&Dbv=@CrLPk_f( zI>hLWif-{nT#Y_3w$H7F81M#MT=W3St{+2|CC7gZV*xb+sv3msVc~!p-fJObJ?|G@ zfwY)XD_sg&Hr+P+FZcKNkof?a7<#^1HEZ=IumN6w9Nx~x%U7G7xE97<34}P6O?ro$ zjrDRz*O|C{A8#iK1YO}?FWOkfzW96+kK~q{K@P?Ggpc3${Mcf%(u!n(67PYJNWb~EnkB4_)2)t$2hS#nzA;^A>y zXNA|E*>1KcaoH@hnhqmHM@NGzEB7I$bolC#qI8%Qt<|n-uPBC@V*8p8^`VLH)K=`! zQf`*!2FYy-8})Q37?)wFyR#%5KmR>c=HrGoEUeIg_>$>SSzeF?6O!O|Z6`Wre7s); z(7`9nKUSsbA6hNSQ!Mquz|*!3HvtCya8H&Qf)67>qI}KAMebi79%Xg-gowCM=KriS zn5=;;|Nc5XJxxH+(rEBAe{^@sb(xSTg|f24oGb&*&{c3wA<`xoWmaH72Nzja})e4o)L%5B3w5-o6{D8CHWP* zva&LR-F$-i_^I#XJ>i@1Gv(5YHf*1?v$d@xyxa`=eePn2Y7g@ENmb0x&gXL^2{vo= zH?3XrUEXlsQr0g~z9kWuBf( zohA_F40V3*J&3@2p5b6xl2IW+wd;JCIcwzu{Zp|D-&8g}QKBHQ6rQXge!%i40EwkzZ9&)0Eyk2%CoGg?V zpYe-y9coU9o7i{o4hE?!nVJOE3~X9aYfyY%b19PRr)VfT-LUi&QB8g|<>8UkL z5XgClD)VYvh)njjMmR2-W0PV;LP_#)|6dEs1g*v2SK_4Wg6a&?BFU;LkJbKIOHfyv zy?I7vZj3rl#<1vK}^m2dGarlS^h7{{pWMw(KOItJ<4LdyputEYXl? zDGWT!qR(>PbJSdXAz!@uE<5NnFt0&0(+khiN3*&Pl>e^IK{BM=QXCIg3v2J59%|=g z9|mO)-J3pI_z%T5TiCJJ;@6=3t%gDL?V9WAZ&BEX7|C;!vdrn^zaqTTeVQTetd)xA zd9-K6T^V44n$NO*=$@aGw;ey+MvvRfG@HA@MG<({I46tHHgQdqiobC#gPDXD7i|yU z4vO_1lckc<4Te$r8yCwt7@NM&w1JGX0_m=iGlZ5+A9x4azF~D=?=WqC*aqT$MuX`E zF<$+f?{EL+cvK*%!ORCM+d{e?1iqE&S&M-B8E8zU!xa*gU*ulL{|N;-dy?_&0*C1H zBaFAInhQ^y6KSGc8M)7rcJi&hR>DB4R zlV7em{ONu|h*OJ(*YiG`ltbTW+z ziCjGDV}%&^oM-EI9w}FYu{+utV`p;_dsvyoYOozT(M`Saxp8(j3mQko;8&unn_{xS zK&-E&*5zHW(~;q-xg}~B%w`3e?1kIHAI)$KR)kMDAZ<6Z20s*i+kv)#7Q2lz3dK99 z&Ob3L$lTY06ZGgU%)PRTo=@45Bev`7Np+wg}NqGaGTBy5!h zdDAK_G2$T#21+W4JC7kv&ulIq7+0w-`PM+mxjq1~1D7;w+`*I2C6J zhziO{-SMx#6Ls2#;B7a40%rUMPdMRZZDE}c<$c;NegHHalzZdP^tN zQ*&o0k%S$ClB9V+`9yXzDaBH(HOtPPRdK6J9}0-CL5J}3Pfjm%DqV#IL1mj;NdLlP zB{l=quVG4~NQnTAVxo2_qfD5;8*F33$5@*7d+}hpp~sK2-I(8WgpI1_o)0}fXY%@% zyU}ogtpLMQpf?J0NSyXl(eKf#{~)$P(k);;an$4p)~lyuGHE*55p!2}-J$A}SgLbJ zQmcPq$RDqo3OJ`l=lX`+fGUV}5pc$RRlT9;lV0-t;AZWv3u8D5f~XS<6TncvU|*2P znKPNAEN>1$Oi~n%c8l^*{D9W6%$kjSj!i)B%Hgtbl%nm(6JMflMG5NA%E>h0rMLt*@~w(`gC*O2b@VRVHUHy zdZ-kOzE8zbj|{`vd8+yHsF25LA*)C+lK&L1jtP z9Y)fK1NBxafzhXZ=LvePo=Pop?4SCJ6%2iMCe!seo@46q6NhhTA8qs{s4!H$8sgy4>g_9&LF(J06Go zB`Ue;M+-Tj)mGy25-*21_e#8F7$BUFk6;)I6WC$w8)wqrb*&!6YRfCqoU8TN+lu#~ zT8bqhFxm7@A$SX-<~_JE#Nshk^LXvrKfoXqe<`ua^C=G z`DRKzqf-H#ky+5)d7!VKT#aiXZXRQE@?ZaF@bDktY~cH!_5p>_let*gb=)_)31VBN zBJv^nz|Ky7WBhv|1m4s|aUR9p^#0^{{|-nkLWQ;&-MoUb(jP;HDTe=ri>31Cza;P_ z{}0+v>jWraSrUaRlU#ohc#4WNRW-i$iT3oc>p%j;U*q_uZyWv}vg$|%1)+y2xcuTF zgegk5K|SRep(s?3f~UuKZv5XFnB=1=VdhR=wQ&l3s5%k3xsgZ$rpD_2;ThZHj(X8Z zKP@|{hU2ey!IhwZ;9D!Dk1GW@xw&Kava0%q_D5o@Y;Di$(==&3OF=U*ZUXLRf(V}d zjv+6_$Xr)6Wu6&=pqSFjQA6HidsLbEjZ@$*$s#*N<`0ne*2;RLGu&iC3Whc7X@C~o&>Q{c8~SlR#`CNduvEjGE`1WHsTjnABMMq%P_R0NdN za4_|@VvIkWUXrkD1-HyBd!2__^y=;cKCYWgPMCd6p`Je*tJCN|F?S`qey)Y`7qeD{jRRMbCp=0+S$%`ALP{m@ zH4_lIrK}{hGC&OGLR(^BTB>=K$vNQhcq~bgTuxZ;6++d>?ri?tw;kCyuNzv0DXCD9 zw&Rg#GFQ36Y0tuZRA{VHda01z-3?=_XQ6oF-Rsgh3a!qBMsC##=iq00{YOD~*>;&g z)taqz-qbZSPJ}Fp+jgzC!8dOD_MZQSR z^7m#jg(8s!n;<2&n&}9IO!oa{m$L*`db!uJC&jK-l zgQ&+%7fAJkRBeKUMgo-_15%TGJ?;Fbrs=TQzpDTeDEGPO+hB)1q_3QMh;mn`Pu{PM zFL_T4O+P6(5l>#@Lp*#^3eyF?X-uG8A}Ow7*LtXg!Ob9Il-_*`@df4&zRT&-YXX60 zeia#%x-+~V&o%}0>SLH|+&eWjQZSD;MDXzNzw(44GC3@6W+eh3MKu4Rd5!F+HL74X z3C6z}#w;hS*Bc!H(7?SqS^k%zCN>HBbm9Adr0-HGEh`OUB*n)gVD;w3xgvSb-o7Sp}xCr zma~dMQR(-U<9pzyknCBg^oGaAN(7s398Glc_s%a$o+Oh0=~NaIgFeoCW%_p=@$vop z0!hxt_b1nycp9>(Y%EOF>K`q`3pp;$-(ffyBc)O!sbH*inL`r*SEV-qqys~H&NK5-VQMNCWEQ41n0y4{>bmG1Y@ z25jFz)Yh=IPqigKf0wJ8e9pvWn%rM^^h|%FCx;X*coKa4KlFS<9uD<*2Kz)ffsrBV z|5PSQzE#~Vp|ipyyP*^De8?SdlAdV~gtQ-HK00sMwbesN2rM)N0fSSo9zzVlYf(Xx zZ}LJYrJ#^7Ec}s!n`Cl{@V`%w9*D`(A2>kx9U$u;oiGsSB+6K+ zIY|5o-W>uqN_Uq)73v+Yj~?t^)8fo8VWTW0?5nG^voPQ?0Lg4_!WgP>zT}I^_nuLq zZznnKOoA^*NX-8(?**^RMI3*UNBp();_-WNg+vMJjg3&Y8^WAeNI9As8dRfkXSfsv zeU0&dCxU07y4NtsE}|Cv*e^WNy%K6E%xRK4Kdd>FvCO$W$SSLDiH44i1zy*cVpMlUgNtTPyZeV4uaIb+0m^E zd;hiczdPILxazIg4A{x{e7WZ$W=DP4mMFF*c=EGG#=}5euEk+|QM^5(N&ewOfEq8H z*3|37k3*v@Pn8$_Uv<~#;T%sy6D3VkRNia*Xe!%?=1zkR0dBgRUDY5@S@DLoST$VE zt#zhFA%~eG0HJsLH2*8D%v%7zH}9dc#SIl*-oXaA=KV_rO4|1uORyM|%mp{|*tneS z)n@Jvo#X|;ABRe|(B9YJ3%%}p0yr-CMG-aq?x!`_-3l_C0ayE7MkPPhteXZq@sl|oEyCpeVzEmPdy@$6LEX-!;Wh)h04UIawGDV&25xbt?r9mv1$4IMJPKAmk!Qhq=Je= z$WRb|pa?{0>xSlR!vpCX&{KyA3Wn>FY)hXy+1u%~Vbpe_pusBJcF7}!iHC|8#`+mp zfYQ@B9JU2DtF@Ejjp0i`S}5dc+}zx~oc5c%y-)iwhI}9X!_vDhYB-G0u`*L5)k{@M zZIaFu);g<%xFva*E0HLc3h5QiTcw|9_xN#f%N#`s$=Cw_&i zxDEXaKKB{rI-6HR|hfFh0^4y_;tS^8LaUADF}fGsdN{3)L7!N zaJ3Mfsxr2G1=FRVG2}0On4+Yew0WE>d{ra{drL~$k}Qa|WY8M4AY{-w=cPSN1{%5d z0G38j1O`7yQdw!;&;hYO9Ez)(rTJ+>8{7X$BS1{D_Jhq4Xx4vZ4h~vP50ge)>U6G6G^Sn~Toi0k z_;pjDi(JTnUcO-Td2K}ru^Rk|UL{R)4%dQ#F=CS;Q|4X@^!U!eliaTh;4Q0!S9z*PT_+PC2 zPi2GV(S+k+lc96DVwt+GiW163sTs$Z-oweg+sVD1k4v1nEI}{!|Fp6sS<2k&a5(e+ zsSpJ8tS?Be<#bQ>5)`?Q(tl!I^B(QzQmFQ5nzw3jz=?11@i=7nxm7=i2PWW8%KJN)NAH79`eTBlLviZ*sk!|+wU?cmHmJhZfrqV|lMH9(D)(bTz9mlzq= zE`+v|T)A(~xsp?CuzR(DC=kV)!r@FMcbkN~dt7VYxo$Sx0vjvPi-58gbtR||lrVAh z@g+V7l>lHNV;s;O!@U4Txy@Z;yuPRiqWP4Bi5H#}s{FAjRI!w%{d5~4UihLy`$Eb zFg;xzG9SC&ZYLKP7Yi(;V;RtgkgT&Fydlblt4FPhcgV|-I9D1k=g3gCw4JQZ8d!84 zXCN7zQKVY)k1sMhRX0svZMEY`2YjeXpVVZdiYuSp+39%v_M?z^&7vs!Z*J&>Lv*-! zA$p+-?Rw|$1`2usmGR+=C?}cU&>X4??eYa>9D5K)XM+(Yo?m3zkyZ!*3jk`Jfix-R z5V>Cv;=e+%3=LN5lGO4-Qh?uKKkmFM7Tg|UrIF3lnpp|EhJp#p+`{Z~%SzjHF``uaJocS6V_j$v5)!}IfMm6eUBJkvVKY53VE>wO#8A(nc= zLf>5#>T@ELieRWVeBO!0UcZ)8YgCr9l3L~)JY_owfnSSu|a z=rED0^WA!;v9%*edRs&aHGXvGeKYQ}GTjjbu6 z(XPPb|C5}Tzr?!8`FJhXpt?1x5VwVE%%pxB{ELW0B~sn-z_L6aq* z?JZv6s_?}FQa{%^UQu~hz53+p;BpnNIlYvKM&pN0O)daZqPK!KDvf6i;zA9PIg>%K z?t~kjbV1>-WdOf>vq+5fp^}?hOLcASc6ejEvP3Gqe|qgmLsO|K34L`@kCqo#?$Z@L zJ^j<$-3$cf8si)Lv+Vatw8`pLUIW)n%MdxUcJTSKOTHP8W%}G{!{pCwA)|hKz0z-= zK?qF^QU_O8Qxao50-MoxH(KrT1udqD+05lDmGWL_re&pMg*<^0OsGxb35<*~&D?wE z+bwq#IDz@^4fT%16V9~f4+9=likR|WX-2e{iuOz5Wsq0STz4?jer?cBGNkG26fWK8 z2R1HlpN)D@vfy69_(xW5J5pV|K5ZAr@aRLbU03qqgg=G2pJSxguKHmKUqVRx<9NiK z_@y0vC>~pNNtkt#*A7~Tg5gmK_?#)nqkf4a70uBK=O>VUJdIUEYc^o*uRrbt1jv8? zUdd4GA4ho);uXV#uNs?1qX{)euS6M&b@wk(3yGFi(sMF)xr0%wT4;uCLDS3a<~kkL zL#8>BZ^bX$4W(Z!t}eDvQRqT;Wr5Z!66a)=3BzkfW{`b}M*vYhGFr>HU|G|xt|=5i z;bJ(kTFW#EHLSyJ2CKt#+&^k*XcSuMCV_5$?A9;$*=}${s+(H`R1oR^+1K~VXXpE7 zC^D-bKU~O!35gunPRp0&+OSdulLuP@eomJ$J1G+r@+z&mBMA4gLp3b6;+Dp!GZ=|Y z@$|CNHw^)zlne|E5>Sv)$irpnLi!mXexxmWrc>;_LFA(C?-@#v_g>$&g_HI;FQyHe zc2MQ$+IB4QPKgxs(O&N?k?Wf8!T3*giC|reb ze^3|E|NPOn1rA;hfz2;uX1yAOjs;36;;)M49V=B!UBg}3TEFyMQqg8DoeMD&~7INhlRL_$&r-J z6p?l-+R?9|dEl#ydwiMICtTeB=m-cqrz_7&Q(e)vxCE?|bFPsN%|pSt(=;Hb|_Dd5+~*`n+^E z1Y?c9)%e?W*cz#_@#s;-ii@Yn{q0hsbmEDBHBKS)yWrOJqpUl(8!vYIr>Fdbrqksp zY9^VLBH|Ar9s1TIcp!PN_SscFgn3FczZs{=KS$P=u4-?CP=;Q=lm5fQWEpN6>^_sj zjgj~;!o$m%up6@E_%Lre!kTrG?by`Ov-5AV}%h?Twl_DBbF>G1JggWU$Vea9hQJl_!kB=cQ4I_?~Z zy77D)-eN4^R6Gq%m0{M=M$SQG2yV3Z4+TJp>S%8-I@HT;BnC-|TG*hXAO8FYZ=Lzb z0PLKmn%>#)QA;nBq&R490t#YK8Cn#=sLqJ%M9>uYYP!kwHK9V7xg-ji_%^#&z`B=@ z)K}~uQ&}UB`{)UY6*hR&4{msirQ`M?$k^Fi2~N>{F)3Z2Cbid%6qPQ6rkQW`yT6D< zjuXOwalO)qI2{#)d4kj=BP{Z~!mDaKqX$9|A35cPzacpVff}88P-rhIJ7q0Ok_9%I z<}tk7b2g|a<}}e2+rkJq2RWbrvznUAM2ix{eMxD#<2B&irUq~s-WR&|@O7kB)TOy& zS)jO+%{~|_as{xGgq{p2pn$9hu{k?E6|ShNdWEdzxom=jvGxr8+T0gW15xPvIjW~HX9I{{%1yZR)K9vLj2G5sNHI@rTNmE3Q*TB6}Tut$f7PA@0r|KW7@YRx7D zoSdAF5Khigh^_BsOuRo2m8n_3nWXbvw8v2U(gX!67drfpIW^T2hMHVBJy5 zVC!=yh;Hd)wd^e9M*=ZlOHq;dRmr{>mo?8N11+hl$I7NrL2IRvA~6g8jdu@FLj!_Z zDr1Qg&#&)gBYo$6yB?yWrQL-XRoj(L6v&CY&dXJ}87W6DbFGMqAuxomatl6}oQ|dp zgqJ}a{lYhq+~nkB?%wL*3^_VTy%Uj8#9t7?|4~j&ZFwxo(m|56Rfque?Mx{ST9#h` zO~>Y6j?#vEy1BtOZ5sj{BCZQnAYTUZMHoFL@`^Pm;J8=0mnEKl{?P|0Hb4U{pf!R5 ztlhjloMb2k9V^gnoX2OC$EDqQ@nl4CtzDaU&%TU?-5HR0I)P(GFa-ksEp+NixEhFT zv|fmsl<`nI$GCptv-n)$?lxcXX#{qOxCaH-?H_?76Kg9)3@WbK%eVEqy*ojAu_rj- zpP`7~?b=G2>|uox5&Ms?-%{14?XN5#VUP^56P%Zqm*D-Px*hRUtKGrlE8p|)6c6-I zN@3lCEumdUL_dvDy=^~}_;D5&9o~5ILLxp$;pakEC#%dv2QNY7A1@@E`F%ZvdF50m zL3fC4Sm$BSN?`W;Cy;&S1^xs{bGdWz$A(WvvJM@T5>pTyD`AV%4GR$Tnt>545wXx4 z@@@d6%S@=Ms+x)yM|6&IpZen4( z41ssYARPaH7eGr(`zI-ts+4#iGOX^rdxD_5IXPwhtkPQoTWnD@eHwsUgNcBvS}nVR z&WX)X-Dy8kf@7p~QLaSZ^aof4o}>}zUz{2m8h<)#H(KAh_$>?Z;-uFc`rc_77!DwR zC1CU2K#q{tids36(0|6R=>h6-!B|~R@F({EBDQLKq>|{ zMz2H=QRt#<9|Dp=Gz8C+()0{I7itLB;}D_nQ+d=JLfni(VNfd_Bgl-;bEIbn!+(L# zB?9lxW(SQ0?eOMD1j4RAKgactC3a2UFGChKS!I1!?QP|Ll#!3BJtGcyyJ0lw1Zh-h zwm5-hidD;$b0;9c+W*z|l|fZ~VZU^DcS(15cXxLSNF&nS(s1bR{39S8f~0_eG}6)` z4bplSckY+_et#Vu#xrvcXZBuuJKtg;#?G= zy=lvk#lMe!dl^HEpELs6fPQ}CHxSs9v)=(h`R$U$D6d+Q_~W7O6gD?85kU;|I>Mp12Li-S+>VK@!(1=B4U!Dr-<=~@c_+t)NS5L6dTU!K0G z0`dCxP~7)izW9Tp5Q;h8RYL@AWde3nMPZY3@X+MulH}&n`pFFks`=3zD-;JpMYMTaFbg_kS~9E~!bslQL>8en8SMgG`uGQY5J3AWHzh2;KJ zfeOk154Iqq4PlW9B&ExeZx$1tMfpijshgIn;ekO~+6Me*>DdmH);$fsjvo%r9Mgc6nJ53<4@bx$410 zMw*g4!-Q|wW^pQ&HU=?wI*;f-1U}Hs;IU=P-hMP>T|8?=qJFKK(nx>SeKs-vYf`^} z8kH}bk#x*-3cke-zj@>;*U7EaX=XDNYuNjR^(~#dudC~kM_1_Aj+ULJzW0t#omP#= z9gKZQQ-gdWi5x2d3q(6O1A`_CG$_CmQPJF-MyHb5VuZ2MrUtf*ui@}wXCGNB%D;Rm zjE+W##9H@~WH2$_e5Rkx>dZI5PEP-KF?jFR8AZOZAghh{9}trEfZDDZT#wJ9{|b}? za|Nirr(hqV9%7{)+C}p&nZlpNuh%t+^j2zvXIYpcg)XlC(FT@YQW-*-9GuoO+0L7D>YY7S!B7ezuStSGGxgwKMv#nBhSm`Tx19ld% z$G?$=e0+SjJrfC#ZF5;s_WefRLUbq2Uyoo3&Q3Et6;+IPuv*>ycbOcErss#%MD&0uj+8r+ zRM$Ym>(9GEQBIrzfw%*wPjhvF{3oC_#E3zT8B8D(O7qwmxm2GHU^&jMJmrY&*yK!r zm{N5{eomTGG;4IWG-;dS01s3JVu9OAiCVLpcKEIQ0S{G*wzbisyV=r#&#p||=+0}? z4<-hnS@#5{fTnm$I-HU_FsM=>YY39L8X;8rVSfvU$z|`wvohLU50RjeDaeuIRsLJ_ zFJa#MxbIF(x=h~crr3I5tvMJ#a;k~&;}ZYr%D2f2b;IoCpBlp+yuCHrcb;(fA+ z9Vh$2uVef2wY=Ga?P>i%S(1&KltkOCwLO4(Sx%1E+ynlX*VNeWD^8q}t-nu~>%k)E zr2Lk5{X*i7v$XW;`OB3KMOZKs&BmEwzz<3kovJKwRMO_6K`AL!e$0eUOE)q%CvlKfj$m z`{Vh8R}EacnOj#&jZ_>FywYDES&%|@NEO}oT}xtR8Se)vIbFLGD<^@-2wl(6@Y|~m zrvVF@+V~?00RaK1eA?LcFw{feEjbmHlxkUO@~t^*Q1yjf&bnz=!&`IaPmU9);w$vM z;ESdeIElz+w9?cl|0km>nrdv((k%Atu}*2^aZk7c199`lg~M;5q`A|RH}E_ ztiW$XhLJYJXtsoSd*D>A#BC=3WLDWV_~G#~%^kMp03W}YnG?-{gp?G%cF;9OqXm-I z(<2(LzBj#y8D(emI==&Y`3L@`Z@&4vKG)S{Iyzo+Kf1}PqCzbd71Wr^O%KGYB$1SL zhjl)%vxC*pp#rpus}KmJC818Im}5bQ`R*Z*?hD%E=_69&d{-;Ova|$2y|DJ?Hd?Wh zHM-$Q0q9PWKg zctxBjuX8k(CEk`O=75F1`9$&SQwVvEQjva;*mjjfRQK7jc1xvd&aAfa3a?r@M72n~ zw?b6I#>PfCSWITF1`d=wZvtD~1-l$OCCB{Fft)mqA}%WYepj}y_0-Oe4m{9O5eQ97<^iqGPhdRPAz-DBw9nm7;h7RFGl=0sMJs)w zC|sv5IOsL8?KZV8&GfHLke6F?oxJkYAFbC%3b6S0JC@8AKH{Sr32w+ryYGd)$3}GG<>!3dTXUg4pL^iLM48A10UbritY$L4&F?f5vi>)u#L# z<24^8H>5=;Z9-N`PTYkDd&ytrJHAH)A@Jh^00Ibnz8q!Kd3)gndB=C-*U2~ogk$jQOd{vDfX7@wjZK@ zt(9A7Qf?L6aPCjB{Bu(C1Ep$Tx6oxZqU-4zY_Sx;4HPL&O|P2W4_tjylz%S}L2I{I zVkuWPv)0^@*5vZLM5gB7S4>GHI%6>ooi%Zj_5sY0Z=;kU-|0B|zE`@eUeK{!mqS3J zMuA!$|0TdvJpaAi@qlL}N$igyXyYATAtbP^2tFQ`J)F9QOo09|cKYCid6|Eu2v$MM zBe{m4IvIwgIVjwS-0T3m$(=;1{F|VqLmOggy9aAM)%wjTfNfq)xC}*sFx68(s)~Yz zUmf6$=f#jTMcP>(k%)C_rIuU`K`FL9?&1AO$fh3yqRXQxmmRZdz;7g^lY zl=}P}h#`J&$*WcdFr$?fV^CPuTxY~5nz>C9sZ9hc9IbiYmbz^uynXOoC}Duzg$)?l zappn;Ua#6O#v&37k^MWDz^J|aB}nAo!*s~~bjaU`r4u`Vp}ayrmYm;0{ykZ+PB_^( zB2$+`t}H3fb~ zSsa^*YbFLGSm63PayR+kI)NQ*w9#tt2XEq7Is=dMGUvzmRs9yVNa@{O&qh9N8dTJ{ z>D;I`JJiByiRpX?rLlSk$Yk_a!u4;C^WNm37=WAfI$z#jO>pFZq?H)*4@BmlvII7K zVb$i0lk3ha@VP){&Igea*{>Lh@`r32 zZf0W{w)l7N-U$uFDV-#}&_@4`M*KtD;SQ#j#x4eO01C;a zZ{P4|aqGF1j~PvtFFVquI6V3Km6@aJ*W*bXnU|_O`Q@NmI_1K=yR6`viwcVto6StA zoG8Fr5=aG-Ku_K3YS=WwnQ;2j5|D(s*R9MqcCDg4O?-yk+llxjUbE!OG2|$>S=4Ts zhAmxu;*VC1%~*n>00+&HKx)P12pLYHiL;XuWN!Rjuzi00@H%A}SOoFC>S^HbLT1OX^L3VJ`N|wA zr?7P>`U_}g43?9dk1s%k@CG9RalgVhn`TSS(j{b*>Af^b5@EPSbeXlHT+XmdR+P|n zBhjD?e=z2KFBWr(Dw!i!hW4&lnuq{{jZ zk^oAYJ+c}g-6tp5C;UofzU=PqA7g6ZUEvr1jHsTdeNAJW5sIGARe{qCb9B2g_)5ZM z4X3Vhos&x+jGL&XC%(L&#F8iI-YnN+WN8A!w7|pU(x?_@k2;dnxD39!cOGA+KB;fI zY+Xr7n1Q)p6tLy;1V8&KD=UMrC^jasvD?ijI^>yLKGdL7V>)AE8rxOXF$ znOWY5j5_`6o4IKm*CfkB;i<+mDDg$_AQvkutNOl3zO~}NL&-mA;pOK zXPsxN7F}4y;K-Mp&~aeq3BVl}izaSbU@FPxviUYWJ$iEcfuv;JO|h*-|I>(JPG_^q zaNA^yVM2yE$2+t#vzd=084Gy_>2QHC$qHEQaKBe3z|ACCt-Hlw$)XbK-j{IS|flWqFh$R2RR$k(%go& zrMtIVkbn-`{FcH$6F%w0^z5b^a@^^wzCE;3QEwzTDFQcE}D)t7xF5 zs`#Qa@M*y7^it%%9cM)|kpG#t7 z3^6DFy=4BN{b&tqdv)E?&=r&S5~GyYfBvoELLb!$>WKyoa0@Oc>WC4-Aiy2 zP0h}_xl>%_nV!0#E8;;f2#*@`0br4xEdIxUhZ2O|(I7-~m0Uo$6Y!RQhfglKP-rzg zt=KlOVshf#4@qH|ozx+2cLQv97XYc((bgX5@Yn&@TVcr?Xny{g)2#3pHgjKL2qSe` z!v|i`Y+AA_G%rLf`>WF*$M<>9r};aaml<4qsD zi~Gf8dFd&$oTk;)poc%u~YEfuak50Nmu!yCq%^1utDlGNq%(YK2wwz7lI)OuI2--(q74 z|LtUWyZE1$`_IO~LrrZ?PEPlCeg7r_P@2u}LIRXYzM_S}eX`%LpDr^pE{%0?EyVwc zX0#P>&5+m{sm2Z8tNQ*t7UoKpGFzPqPmAg%2W7gQ#lN8A;LPc>>2NX*mPz7iI9m7Na8VH zBN9M@!0^GfmZ@pN^ufd_Ge*Id2Xow)MkjoOhihN9l&qN?Q7(d=x3{Yg69eRd2lI>% zB!=|l4alsxvbyg3Gg=tAF0*8*U0B-?mQOv+%rnpYh?iy0V>OvF{94_A#DexJ}g4@T(!+R##effnncO{y3@&$iC<%nLN zws+MzM^k8M|E8>Txj>cJYzYj_uYLPo9zf$bdT($5AcPWyW0(8n|II(L(h+FUc}Xh# z_muUCpv|)lkOJ%)a4{KYa-Ral9<6b^No8zJG#wPxEwF~qGZe>Unf9c@Btx5|bRhomor^GHwryWp!#;O20qaC>{(5kxHz`ihQNR6sO_ zRHs}Va3U1YnmYGpIW5E_g?{o-<9^U}fBx%HnamS6Buv;3+By(*HcXdgIw0}y0VGjH zz{7-r)GzfiM6^Lm@ch)9v}4E|q7}iLV)qxGjyZEx{xc%#bCfr+_dZc-vJUSZ$y04; zj2*dj%Kae8N`TsO_U31mO~>^og%a%z?Y+M0f&5-&3uJx13we@03uxBEGOx`jpv_WN zQMu+*BV&r}!1Ntxe(?6v6{!`XM?hz4X}u`G+U(AexiN2B_sPJ$Zxu+joZ+p{L+H{aHeWgRjlz)BokK6U5eDCU{^X`BD&t15N9vfb8LG1EgU}-_ zwJ=5U&ZX}kK!!`rK@NBo8 z+$n_%Y@X2VmW7QrWF656zQQf^-9`o@Kx1Pg!@AWY9Fv_&%I_p{@1bpacxWOrh@wd- zx+f&bPl@8s_wQfgTPx^C+y-Ou3rvhN;q~6IArOLX6Gr!EW`2L?=ise(`s=*e6cZZw&Wu|G z$3ruJrk%-U6aG?}OV*^3@2>m)o!suNzoV1Wb~LH$76=940J$Io0|WVOjX1Z<&~0v? zs43c!_N5r9d@`k4r1Y6pCJHmGW*%jYj0@s2T?S)m^0@X0JXDCZ$8)rm-7L-t=zF98 z(A)-^hq6`gSr*lMJ;^raD@d1a*Er1z&IkAZ?e#kYv)dc6jt;wmGaSc_po?}H2%|fH zzr!BCqPvXkPq<|z;x#LWD@w`tc7{kr;e005QBnrcVs8PIxAuNc?KSzMf?9^%1WYZ| z73FPh1QVHfaJ0dn))u}4qPD_snTMOzm{KHjQMt~{BBzBqg&~J?Mf(>=Kz@47jQ)1S~pK!K^qG@`;Z`Vd%Cd29z^VtgA=YuIliX=Cy8 zR}h<_NGOA1FAh_`6(f(piLSoBHBmGI89omag~-76{Ghc%ud`Nb3>=z?`+hj%o0yfM z04h+UiWDe=AOf3#pXpp#2$fio*(OTCrE!#zk}UNTxv*KG8m-EK< z6YjXf8Xf7Znpa#IC-W+%7bg{X>x{N_QcgI2L42WHWQG*PXsCqpLQ~slQHzE!1rzvl z5$!eg00Spn%-Z9tdAgcB!D{bz)iH)X*$8I){o^mK?-VTsd}tA1yP*JSUYYEpoyVa| zyy&ZK0OO~klAxO5{dqYRkM3Eo{(L36JCF7;6Ylh-qq${qrCZfuB0RN%KR|;!ZWvG` z@F0f+|CA5)NUpNz(BK$Nh^np=9WQv$DGNQ zq4DL!RZ4*E@~l5cl7|6N(}t^GZaG{Zy^*gbS|4>2YbDCpP7nes0zh$da-O%Qyby1_ z)R7YY(#(ivj&{38-Nt;>_Amw|jm{XDP1%g1v5f~5Xe1eBQR(VO#ZFrNVr^M@$a21) zt}dHSw_k8qgN}~QAc#8)EqL(nl$UM?8tFBdZM+Hd)k(#Th z!3qXZ6jbzdveq|~`bva`T3_Q3M+Ds~QM?Y>rIEnRMg_Qm<4GY4ZPpN2vHSb>Es>Vq zchM>-JD)8bm45ss9lbXrYSLoEtcVPx?mWJ+uS{%paH<62bJ7!^--5MS76{_H3I{We zWHzK1oJSA2Jkmv|nb(Sj)eTK~bAq~D?i4?hCq7a+q)56%py+?+n`w4oyi#ruWE=Rg zeI~LMg#}!w`_2+F)Nr|#0QDbAdh|y#vvDt3|4gFP_Qp7CpPIq0tt;)1KH1K{f!N9M zS#Gtjhf@5n{^MoKJVjj$Sl0ykvh-In#AJ}4FheU1B%0r)U1hG21ppIk$#p>ojQhXcUq z;-Vob$E+CEbcE4;ivt-yal{1w{Tt#ubCq}2n~mG?X;O8>i6{vBgNMx+*ck!f2wT6%=jd9C?Tn5p z+mKiq0oiJYHz**m+7weAz)sbzGo|m+n46!Ebj-~M%C1)p#B!S&?d-wXM>8zx-!z6E zbM~?|<)ZW6e{ExysThujI8dw0_f@H-0@$K2pX`S|kTOXQ!M~*>wEAP4?k@WnwY4Tm zrDTfjuim8ulHg*L+HUsi1XF}%l*G{j4t2&F9kLMs{4Gsnxfb5>* z$r+0N>X0DwW?Qbz|9CuFhKphJYN+j;Q_{&)-UTACv5~f9`FzZDk#P*$?ZGX*7sHvb z7XG%w?J_28T~TI@d#7~m{%LFXX@5lZ8#PGTO!-&pi8fb)128OzBg;gSD*rp_A#3`1 zS3(D|52ZKkuhn99J7yCt{UAfOlpa7=Yado!M8N>?4B))|S7ip+w!u~jz;#dgdK{wY zuu4bg<;F04mM=pGeA(htvOL_4EUZ%M{IM2=Rw@$+rxipay_1?Opo#RG+y31fqx2oG z3&MKi-x~$c{~U^jpisATbHLYcB}-s86sU!7G1UFQreL6U$-i{QW&e3uwn_LMJyf#aU(!pcTz1xbd2|xp>>u79GA*0Zt zqM4{Qhin67XVi<<$=BLL7poiG8TTm zmnmci^CK>g*qYB~5Rfy4xW##le;vZ(+$gkW7;h1P!uIek%KN~7tKk3f;w79_sYb5CBJZQJ9Z-coHd{^L$0kk!d#TWR5I<9%PXC zhuaNQ)8w7>5myGre(kz7c{tJt8K_jt15h?t(~n&r(jmNGI?V`mr$}yVRw&1!6pxMpi*KQ;?OfT_mr`ckjb(^}PyFjFx{G$Go zngO1LY4n0xw^~LR4`x%LH)xgy59JjT7NxL}n4=eLk=536z3m}(qQp%`u}>E0{N2JC zCP{JrJt`csXIF{x9I()oy8o!9Z*Z!&>9BhVxQ)eLc!z+ge!y#W(6ANfSXh9(h8PIx zM4e(4Vlw#Twg zTY?Pac!ML;7Jsj(IC?H%K;<DN7ri5Z% z4Lt50#3sk84ZOsn)r2Ev;bMcDlMLL9#^y{yy(M991)MMP{=K+`0ea{?rVwT;OQCwf z2hEYMus5Dw^MyYQ%)I;uysDirs;d0*cS3!_EGF>W6sfSFs?)U3tQ9mlx;%G zY>7)PT`r#BvP$A54v1?J=6J^d@&c0!|8#1F^TXe9jc$1k{kzH)XE3=w{1bPmfpSl# zH$AiSmOV1MSTD(2xERX3WP0o%OLjY{ruL(;^A>8sNh6nlPVdgj(lEbVZqFP z*11w&cSUZS`US*-pWHF{F-Z)@Nt$vPXrbJr{RfTpdo2Hb@FJoq3Rzm1*8{`FdwLFb(i=on zRB+OZPrrti0F$0Pi~w|t~K>TSaG-E>LU0=PG?Z@@^ zHZx{P{Nfj%Gsw0Sde!kK32q~)i{0u1mP!t&w}E7FXvYJHGiDs{CYl>r>*iz~0e4Z$ zN?Uq@OQ|*oWKjQ^v^P7<(7eud<{>ZM$0sKsV$wyKZ&Zy)$C0bD#qBxzPvLk~hLQ^E zEln@pUfRE!-|~!MJhLzh4JV?rAxT+f3E|>(3mayh=8bU2@QBawdWqEQMCqN3xQ{5v zB3%Iy;4`tM(SZIm-33l>kQGIY%Bx(|vsRHbJa|kXDXIBT2R5!4ED&GOsj_78@oPZG z01?Kk6J;5Q5o>^NhJ)jioGQ*ktonbAW=a1!k^q7KiSXFh)HavTe%JP|WLlovm$BCk z11J|`M|bW6#jmbc7p?HBJ=xM(*gjr;{T;8Xfc%FQ_S#yZWzkNzg`uEXKkN?YMO@U!Lpqi&?4gzKSPGuBP#!Q&NLjAh*j(IiRp zc76e1}*3-!u*PkTMb5Y0o%Gg2tWW*9!3t1 zD!{4$$OVwF|GjBro1npo!@`Jhba1c)JZK=uEe*UHV+aKDbX@}jT;SEXf)inJW`H(} z8OtE3wYH#4?1l?y3-fb?nl6}Zq_-)}i1Ex{t@a~!4T@6YcZR>Pp~9`8D3l_G{ScBp z_96{x6Kn@*2ZmymvkerVP;FS>nCpDa2MT%gA1OFuqfVVw5p2}E>15p@2?7#VDXfS9 zuR?-L(QfuW(FZ}E#IOIZuC5r_+3mljP@|FZ*AHW6U={d^s>{g0f(%JJz*9S~wk5t= zB64#ZT}VGI#aj`rpZ%~AA@`)uFmVWSUO+)_fkj*7-`#o8%WfV?DA0sv&K!qv&H7=k zRcyWGzqN%HxT(CwU)&}Ui+}Uxt7kncZU>?r24%rMHDbxL5ve9$i16@O)4OjI6Lt8~ zHeUZxeYx&=6w9Q`=9wyOS+)=8?RVHSA&|BtLWnM@OmJCcv%a|;rEI^nqMHrE`CB)1 z#R-DmKfztq7olqm&~~1a3${FOE9?AdajkFd2k)!LwzeeQSE+UP4_l)LcK= zPh<9WDqsSgpVuMbTUOADB%h8V>bSDAJY>bWfseDRnm!Y&1oDL?s)OA`+T#bkM0V8s zd{-*CL-DbyllQ^5DMf^vmo$9KnunVnpY(6e)K=S*dCKd*{aAtzd#5=uxT#3TPU23_ zyugafmPjF|hjU7(@)b^qeDO!FKAl5woF`G`gm^#4Ax(D>P*MZs+-T_AAc|xC`lW}t zzQ7^$xI)AHKQ=ll%0I-Me%h^eW|VxI(%`IC++I4m)n%lX^r!phzd9ihV%k6F7I5in zw|1WDrhLD+0}szYqHQjV=~LKUjD&yYjy>LN9BYh1V7IkQ3Ef^yLVU;aSP+$AOCb@< zJnKVnBw?WdBLKUZ+Spna!N&8n6lWA}S-3_|=c968Fv`gv zR97iq3X|`>B~ZTfk0VEVKIsD;CnY{@E<0Z}{`lL@KibC_FPHgYnTSrJ%o$Yc;~$ID z>d)nR7mkHVh^hk4iME#7*pd^5BF)9W+4m9Cd)>u;9%an?L6ry7N4p92X}^50Y)ED0 zSfc#&*~bF2JfdN6(f{k1070W?S+u~Yx!t7JSFWd_`&)b~(GTZ6NW5ypgwI!Q!Dm6U zF(q%rq7B1WZv3!g|1+*wair&C6(g{FdE3lvQQ0P^Ht)1eUyO*p~OWwoz&rRYh-&Il7OW`8_P2B=67A<^xtP}3}dXl zu>MZmCtOUi7ciqGo4cMdC%Vs20?Ue$-=VH|5ynJ+}Ano6NaLEa2Iq zpoX)NN>E@!mM``nz=!f{qup#9Yp3h7W0Jy!K{M0d%1U7WoWnn5-6y76nJmCWNjsBj z=#jcWmjc!rW8lsRjJR3F&b4OD_^*Ug){54C!fL#ve@aXOz$k0nw`svp!CL_VW#aBo zq^`C$Mf(`Ulsn`SK(zPE+HT@COZ7sVhjPP++a@z8V0XeOd7IB^(|t3Y;ix6ARsLS) zW>KycD6HWSbp`%xJ4JO_{Tm2O$YvC8YIY|s`%lX)8U)Swp*CR(PtGv|LnK0q?ZVMy z;4>()N(T8$;7&gHrzjQ8lj)>u`$u981Mw)-aC8ZwiV`uWbiX@XSw*?DGQw)wb4T2C z3!J$O8^gi5qvsklS7kEa<-1xTC;rHZZ&}Zq>A}IC7EpZ}i%Fcv8(MAtaEnp8rsjhgCOfad$Pic`${E#QS2> z<`OcGtEGPBSP**Uc$-nN5Xt=CjfiK5v#QV0`2l7N6Lx?ECyz{wt2ABIhBV5a#L6Hx zuhnWB5L@U7wM#9M?i3|tpkxx72xO`c#lD7YA2iPxC$yv+S8|Z)}Ly> z14T-MG$F;`_T;>CRIH?krNOstlYa2<4{oQ+O%Z2?24l2wi%Qj0lkauL{guh?CFbR| zrx^HsV#3_f*#vQSk#uob_87EHdi|I{Z`N8o{P=maa+z?-Kx-It!f*l&Jn*q_W zFdn_Hy+6~{5cZVH(Lb2JuR{;whWUXOchiqb>DzI3Cd%)!bm6xb15VIUWRf^N!r{mR z#}!aTW;0a;mJ2bDP>8ZjxivVK8pf#JOmg<5b!BheY1}`&?1(#?Y+ZiSDBlHVfV_(< z=+{*=rU-`LvnTo)FLTG}&eiBwEP4pLCmd?F;ZRlf{Xz;5oM`tv%F^v5*lp`={>)a_ZebC~~E8$y>>^ ze}#<%vv+If>p>vzFkCQ)g<+=m%uce{49DH2W^P3J@eNXoFPicJ8x1&&=bK zoTY@(r|6K)(8lyJgnOZV(0kt|;Ru}W4xIcQaUdBP0NN`DsS+sQh$)yW z+6jC4H9U$5KqOB_&hQ5$@#F|?Sj%<}rxcfiaAwkCra5+`LAUF1{RpHef8D@!wskVfw{;8x36g`& zf~J(P2QcUT{r=w&XIopgRFeX?6sm1!RCmqke91HNAKBNduTqfnx5IKP9rybG_Jf0c zepb>c3~djj0A$Uw-(FFXyINBl;7D z$o_k_9-#fLIZV}#6UapfCYt3Vo?GoaoWJ9(Xg!T|{N6=MS1>|Co>gt;*1|PK!V*Se zknq&$9jT-A^*xm1Q2Fv>hA(XCjez2qS(D*!SG74O|Km@`7*kBdj5A$PzYUF9!cfBq zah4=fh#Z#Y!g++GtK%-!xJkYKFfj|=`uY1ERMgj_QAcVl-Nr>*{eA=Yq%?+R!oAiu z_Sq${4KKFQ<+!BD60*Qg}%BDmvz1P^Px?If%whEurN98eNVH| z+%Uol{T(WlPs?;2! z|KQ$f$<0%gC;N8*K_CdWf(08mv|cWNrd`3Y5zDr#e~ee#B>2#5YOIL( z+FVBap1IexA9^Q%R=K*aZt(n$&@uNz66A_z4#&pY^|j?0baFF`|}~#(nAIfrb193w|?7# z!~?_33PLGE?1`2%%rcR ze%GtjJ&Bn9)6XzMS*x3n)60s&K0Yka1?+qFc*-qF(-+=()vj-rcIJmS*bw*>h=A^= z%>|W-`0U!x9)-FRN>A}4-JvuVY}<01DG(tN$5yntd_wfMBhJ_LJ+EUda-tt6VbFm)q=*ZZT5>}ves17kepFPKV_ggBI^pZEn7Vw)7a+X&* za!oXEOUJgz;B~2_MG4@wUV4ci2_!hEk=I@goF9%NPqLp<`2@?W`09r5-B<8D{G`#- z`QB{GM7lHkBzUcw)%Yp$E$vV<=Te(Whiv$C!8=%@x7>-4i$*veR9q(ZUq;AJLg|Zk z0@OzpL=g9q$PA-w3+(}ZWJ1M@j_Y><{W-dWv};)v?M0AA<=Tyy;j?m*dqFQ{#ccXt z(>Nrbc|Y+h3XbB}t)smSM^CH$79yyUUS~95Gd0`a+J-=-q_0;J=|#ty!TiiZdhHk$ znZDlHl9$Fi;|kHts!*b*(*q%+_q z3lcgLc_f~3Fh0nZQ%_u%Nez6WL-Rg}R_UhAdBi0vk`Cb>@BV8b^Rmlmv`w{k3SLm*aB&~;N{|rZf#{lfDk4`q=ZK{O%pxCJuOn9{7UO~ z@kNalx{rQ6UouNItwr}fH=`lN$i>j9M_DC zFW-aF^c0F3b(3y;D^9Xhk=yqTJ_@F*LFs71uc*U(d^~*btP2}{r)d^sj`#(RgsA_v zcXhCnLtIy5{HUM0-_(*B>v!2)a~OVqLBB=l%KA~EEsoFHEY%Tu4kD{8@{%*R#bgV^ zP?+%+t}f3cWiZ@d1tQ^XV$L$8nuz5aK^LXQ(@=3A{5wd*}lz zR2WpvNyL5V!Y|^VFx2@_BDE&I=`n6DG0#V|AupV}iqvN4Mw$&|`HP38)oZ2R%xJ2# zOZ8joa*SCqvb3TQMI5J@h8jQGH8Z)=|3ne}=+u78)t&RNQNVqaCteWqEwMX|JH~il zTKvEQ1-+9HI9;cQMAOXJsX!y~`Tzs6K%nv{3ys>!tMd0gG_ZL20YdxIi$!+@Up*Ng z_&EJs4{t8ry7I^oG;iqR(@(y{nr^F=AnpD1g5cyhXAxOfH<7Ls;&#yNlVdZczaUu* z?Tda7d%^vKSCgI9z+<#MOYzE04~7A@sx*)*s54?OjU)mTnA@pNP{i1v?13qq(Cvy%S&hfbZkuJikA9 z-9OlGQmuw^_v;9DjqBVdE%F_fJJk5LFj1?{{;W@SbxQJEO_30lF#Ef(Ba^kuX=<{| zafOOhvUrsE?AVVoo=4;dXSa2NHzy4m0mlXZXB?`v)cM`f7 zJDQIM5MO>J*^^I_Mw)J)c^+0_d{Q1s&t8rXXdk*^stbm5s;`)NLY1oXd)9{^(-=eM zmA7~x@U2K0yS=bJolEB)=bKP(l=`J5g~sn%32U?09U$-oH0{h}<;rl&(I3+v$>=oW zwI+HJr~10n1EGFxOhBurh5Bhq)!Jxk{fX0Nv(dtMhN^|AG#J_7WN4C0JrpmNu~zO? z7xZRKRT1Ft9;qKgCj_|p@T5IkASi>~Nd1Xf7kU7j(TYV*PlqV7wsK3R0?$s^&!{wV zL_P|KR{5v13_kH%=#|iavJ1a!<=CERZa!~EupjMgxMi6s*2bB~;(RW?ev716S3C3& zP;xycgKPms7cMMvP2&Rbwk=bh;uR^PhiKGL&`GWqXuvABkfi@TLvZP8*_wjSqL@*K z7GHVz{Mll?0?N{$Zx;yL{+Iu7_uq$i>I#i<_;Zz2E +Date: Tue, 6 Apr 2021 15:24:07 -0600 +Subject: [PATCH 1/8] Add helper function to read file link targets. + +Signed-off-by: Andrew Beltrano +--- + src/utils/common.c | 29 +++++++++++++++++++++++++++++ + src/utils/common.h | 2 ++ + 2 files changed, 31 insertions(+) + +diff --git a/src/utils/common.c b/src/utils/common.c +index 2c1275193..999e4ebe3 100644 +--- a/src/utils/common.c ++++ b/src/utils/common.c +@@ -8,6 +8,7 @@ + + #include "includes.h" + #include ++#include + + #include "common/ieee802_11_defs.h" + #include "common.h" +@@ -1302,3 +1303,31 @@ void forced_memzero(void *ptr, size_t len) + if (len) + forced_memzero_val = ((u8 *) ptr)[0]; + } ++ ++int read_link_target(const char *name, char **target) ++{ ++ ssize_t len; ++ char *path; ++ struct stat statbuf; ++ ++ if (lstat(name, &statbuf) < 0) ++ return -1; ++ if (!S_ISLNK(statbuf.st_mode)) { ++ *target = NULL; ++ return 0; ++ } ++ ++ path = os_malloc(statbuf.st_size+1); ++ if (!path) ++ return -1; ++ ++ len = readlink(name, path, statbuf.st_size+1); ++ if (len < 0 || len > statbuf.st_size) { ++ os_free(path); ++ return -1; ++ } ++ ++ path[len] = '\0'; ++ *target = path; ++ return 0; ++} +diff --git a/src/utils/common.h b/src/utils/common.h +index 45f72bb30..51e88df98 100644 +--- a/src/utils/common.h ++++ b/src/utils/common.h +@@ -595,4 +595,6 @@ void * __hide_aliasing_typecast(void *foo); + #define WPA_MEM_DEFINED(ptr, len) do { } while (0) + #endif /* CONFIG_VALGRIND */ + ++int read_link_target(const char *name, char **target); ++ + #endif /* COMMON_H */ +-- +2.20.1 + diff --git a/external/hostap/0002-Use-wpa-conf-file-link-target-for-writing.patch b/external/hostap/0002-Use-wpa-conf-file-link-target-for-writing.patch new file mode 100755 index 0000000..f063529 --- /dev/null +++ b/external/hostap/0002-Use-wpa-conf-file-link-target-for-writing.patch @@ -0,0 +1,70 @@ +From 8ef18819c383df0191b96e84d5f8c6f21e73c785 Mon Sep 17 00:00:00 2001 +From: Andrew Beltrano +Date: Tue, 6 Apr 2021 16:04:35 -0600 +Subject: [PATCH 2/8] Use wpa conf file link target for writing. + +When the wpa conf file is a symbolic link, the link is clobbered when +written. This occurs because the rename() of the (regular) tmp file +used to write changes replaces the original file, thus discarding the +link. + +Fix this by first resolving the conf file link target (if any), and +using the target as the defacto file path instead of the link file +itself. This ensures the tmp file is created on the same filesystem +as the link target, which is required for rename(). + +Signed-off-by: Andrew Beltrano +--- + wpa_supplicant/config_file.c | 15 +++++++++++++-- + 1 file changed, 13 insertions(+), 2 deletions(-) + +diff --git a/wpa_supplicant/config_file.c b/wpa_supplicant/config_file.c +index a535e3f08..fca21b9c1 100644 +--- a/wpa_supplicant/config_file.c ++++ b/wpa_supplicant/config_file.c +@@ -1545,15 +1545,23 @@ int wpa_config_write(const char *name, struct wpa_config *config) + struct wpa_config_blob *blob; + #endif /* CONFIG_NO_CONFIG_BLOBS */ + int ret = 0; +- const char *orig_name = name; ++ const char *orig_name; + int tmp_len; + char *tmp_name; ++ char *link_target; + + if (!name) { + wpa_printf(MSG_ERROR, "No configuration file for writing"); + return -1; +- } ++ } else if (read_link_target(name, &link_target) < 0) { ++ wpa_printf(MSG_ERROR, "Failed to read '%s' link target for writing", name); ++ return -1; ++ } + ++ if (link_target) ++ name = link_target; ++ ++ orig_name = name; + tmp_len = os_strlen(name) + 5; /* allow space for .tmp suffix */ + tmp_name = os_malloc(tmp_len); + if (tmp_name) { +@@ -1567,6 +1575,7 @@ int wpa_config_write(const char *name, struct wpa_config *config) + if (f == NULL) { + wpa_printf(MSG_DEBUG, "Failed to open '%s' for writing", name); + os_free(tmp_name); ++ os_free(link_target); + return -1; + } + +@@ -1619,6 +1628,8 @@ int wpa_config_write(const char *name, struct wpa_config *config) + os_free(tmp_name); + } + ++ os_free(link_target); ++ + wpa_printf(MSG_DEBUG, "Configuration file '%s' written %ssuccessfully", + orig_name, ret ? "un" : ""); + return ret; +-- +2.20.1 + diff --git a/external/hostap/0003-Generate-an-event-when-a-network-is-added-or-removed.patch b/external/hostap/0003-Generate-an-event-when-a-network-is-added-or-removed.patch new file mode 100755 index 0000000..f57250d --- /dev/null +++ b/external/hostap/0003-Generate-an-event-when-a-network-is-added-or-removed.patch @@ -0,0 +1,67 @@ +From 607fddff48be708aad9b9bcd427c9baf3f612247 Mon Sep 17 00:00:00 2001 +From: Andrew Beltrano +Date: Wed, 14 Apr 2021 16:37:08 -0600 +Subject: [PATCH 3/8] Generate an event when a network is added or removed. + +Generate an event on the control socket interface when a network is added or +removed. The event name CTRL-EVENT-NETWORK- is followed by the +network entry identifier and a unary string argument 'last' if the removed +network was the last entry remaining. The event matches the corresponding +Network signal on the d-bus interface. + +Signed-off-by: Andrew Beltrano +--- + src/common/wpa_ctrl.h | 6 ++++++ + wpa_supplicant/notify.c | 10 ++++++++-- + 2 files changed, 14 insertions(+), 2 deletions(-) + +diff --git a/src/common/wpa_ctrl.h b/src/common/wpa_ctrl.h +index 126a7892c..61030eeb5 100644 +--- a/src/common/wpa_ctrl.h ++++ b/src/common/wpa_ctrl.h +@@ -126,6 +126,12 @@ extern "C" { + #define WPA_EVENT_FREQ_CONFLICT "CTRL-EVENT-FREQ-CONFLICT " + /** Frequency ranges that the driver recommends to avoid */ + #define WPA_EVENT_AVOID_FREQ "CTRL-EVENT-AVOID-FREQ " ++/** A new network profile was added, followed by network entry id */ ++#define WPA_EVENT_NETWORK_ADDED "CTRL-EVENT-NETWORK-ADDED " ++/** A network profile was removed, followed by prior network entry id and the ++ * string 'last' if the removed network was the last remaining entry. ++ */ ++#define WPA_EVENT_NETWORK_REMOVED "CTRL-EVENT-NETWORK-REMOVED " + /** Result of MSCS setup */ + #define WPA_EVENT_MSCS_RESULT "CTRL-EVENT-MSCS-RESULT " + /** WPS overlap detected in PBC mode */ +diff --git a/wpa_supplicant/notify.c b/wpa_supplicant/notify.c +index e0e7e5433..3536e7e5e 100644 +--- a/wpa_supplicant/notify.c ++++ b/wpa_supplicant/notify.c +@@ -350,8 +350,11 @@ void wpas_notify_network_added(struct wpa_supplicant *wpa_s, + * applications since these network objects won't behave like + * regular ones. + */ +- if (!ssid->p2p_group && wpa_s->global->p2p_group_formation != wpa_s) ++ if (!ssid->p2p_group && wpa_s->global->p2p_group_formation != wpa_s) { + wpas_dbus_register_network(wpa_s, ssid); ++ wpa_msg_ctrl(wpa_s, MSG_INFO, WPA_EVENT_NETWORK_ADDED "%d", ++ ssid->id); ++ } + } + + +@@ -381,8 +384,11 @@ void wpas_notify_network_removed(struct wpa_supplicant *wpa_s, + if (wpa_s->wpa) + wpa_sm_pmksa_cache_flush(wpa_s->wpa, ssid); + if (!ssid->p2p_group && wpa_s->global->p2p_group_formation != wpa_s && +- !wpa_s->p2p_mgmt) ++ !wpa_s->p2p_mgmt) { + wpas_dbus_unregister_network(wpa_s, ssid->id); ++ wpa_msg_ctrl(wpa_s, MSG_INFO, WPA_EVENT_NETWORK_REMOVED "%d%s", ++ ssid->id, ssid->next == NULL ? " last" : ""); ++ } + if (network_is_persistent_group(ssid)) + wpas_notify_persistent_group_removed(wpa_s, ssid); + +-- +2.20.1 + diff --git a/external/hostap/0004-DPP-Add-Configuration-Request-timeout-in-hostapd.patch b/external/hostap/0004-DPP-Add-Configuration-Request-timeout-in-hostapd.patch new file mode 100755 index 0000000..6228797 --- /dev/null +++ b/external/hostap/0004-DPP-Add-Configuration-Request-timeout-in-hostapd.patch @@ -0,0 +1,74 @@ +From 736c80b4612ae804552a34bdc466b0dd4c1361a4 Mon Sep 17 00:00:00 2001 +From: Andrew Beltrano +Date: Mon, 19 Apr 2021 19:29:06 +0000 +Subject: [PATCH 4/8] DPP: Add Configuration Request timeout in hostapd + +Add 10s timeout for receipt of Configuration Request frame from +enrollee. + +Signed-off-by: Andrew Beltrano +--- + src/ap/dpp_hostapd.c | 26 ++++++++++++++++++++++++++ + 1 file changed, 26 insertions(+) + +diff --git a/src/ap/dpp_hostapd.c b/src/ap/dpp_hostapd.c +index aaeb94c2f..2e1a79a78 100644 +--- a/src/ap/dpp_hostapd.c ++++ b/src/ap/dpp_hostapd.c +@@ -28,6 +28,8 @@ static void hostapd_dpp_auth_conf_wait_timeout(void *eloop_ctx, + static void hostapd_dpp_auth_success(struct hostapd_data *hapd, int initiator); + static void hostapd_dpp_init_timeout(void *eloop_ctx, void *timeout_ctx); + static int hostapd_dpp_auth_init_next(struct hostapd_data *hapd); ++static void hostapd_dpp_conf_req_rx_wait_timeout(void *eloop_ctx, ++ void *timeout_ctx); + #ifdef CONFIG_DPP2 + static void hostapd_dpp_reconfig_reply_wait_timeout(void *eloop_ctx, + void *timeout_ctx); +@@ -440,6 +442,24 @@ static void hostapd_dpp_init_timeout(void *eloop_ctx, void *timeout_ctx) + } + + ++static void hostapd_dpp_conf_req_rx_wait_timeout(void *eloop_ctx, ++ void *timeout_ctx) ++{ ++ struct hostapd_data *hapd = eloop_ctx; ++ ++ if (!hapd->dpp_auth || !hapd->dpp_auth->auth_success) ++ return; ++ ++ wpa_printf(MSG_DEBUG, ++ "DPP: terminate exchange due to Configuration Request rx timeout"); ++ wpa_msg(hapd->msg_ctx, MSG_INFO, DPP_EVENT_CONF_FAILED ++ "No Configuration Request received"); ++ ++ dpp_auth_deinit(hapd->dpp_auth); ++ hapd->dpp_auth = NULL; ++} ++ ++ + static int hostapd_dpp_auth_init_next(struct hostapd_data *hapd) + { + struct dpp_authentication *auth = hapd->dpp_auth; +@@ -1039,6 +1059,10 @@ static void hostapd_dpp_auth_success(struct hostapd_data *hapd, int initiator) + + if (!hapd->dpp_auth->configurator) + hostapd_dpp_start_gas_client(hapd); ++ else ++ eloop_register_timeout(10, 0, ++ hostapd_dpp_conf_req_rx_wait_timeout, ++ hapd, NULL); + } + + +@@ -2001,6 +2025,8 @@ hostapd_dpp_gas_req_handler(struct hostapd_data *hapd, const u8 *sa, + query, query_len); + wpa_msg(hapd->msg_ctx, MSG_INFO, DPP_EVENT_CONF_REQ_RX "src=" MACSTR, + MAC2STR(sa)); ++ if (auth->configurator) ++ eloop_cancel_timeout(hostapd_dpp_conf_req_rx_wait_timeout, hapd, NULL); + resp = dpp_conf_req_rx(auth, query, query_len); + if (!resp) + wpa_msg(hapd->msg_ctx, MSG_INFO, DPP_EVENT_CONF_FAILED); +-- +2.20.1 + diff --git a/external/hostap/0005-DPP-Add-Configuration-Request-timeout-in-wpas.patch b/external/hostap/0005-DPP-Add-Configuration-Request-timeout-in-wpas.patch new file mode 100755 index 0000000..97d84b2 --- /dev/null +++ b/external/hostap/0005-DPP-Add-Configuration-Request-timeout-in-wpas.patch @@ -0,0 +1,77 @@ +From 187537d9f116d998f5e454be5e7c78f03ac5c4fb Mon Sep 17 00:00:00 2001 +From: Andrew Beltrano +Date: Mon, 19 Apr 2021 19:29:54 +0000 +Subject: [PATCH 5/8] DPP: Add Configuration Request timeout in wpas + +Add 10s timeout for receipt of Configuration Request frame from +enrollee. + +Signed-off-by: Andrew Beltrano +--- + wpa_supplicant/dpp_supplicant.c | 28 +++++++++++++++++++++++++++- + 1 file changed, 27 insertions(+), 1 deletion(-) + +diff --git a/wpa_supplicant/dpp_supplicant.c b/wpa_supplicant/dpp_supplicant.c +index 2bcf10b4e..2a7918caf 100644 +--- a/wpa_supplicant/dpp_supplicant.c ++++ b/wpa_supplicant/dpp_supplicant.c +@@ -47,6 +47,8 @@ wpas_dpp_tx_pkex_status(struct wpa_supplicant *wpa_s, + const u8 *src, const u8 *bssid, + const u8 *data, size_t data_len, + enum offchannel_send_action_result result); ++static void wpas_dpp_conf_req_rx_wait_timeout(void *eloop_ctx, ++ void *timeout_ctx); + #ifdef CONFIG_DPP2 + static void wpas_dpp_reconfig_reply_wait_timeout(void *eloop_ctx, + void *timeout_ctx); +@@ -654,6 +656,24 @@ static void wpas_dpp_init_timeout(void *eloop_ctx, void *timeout_ctx) + } + + ++static void wpas_dpp_conf_req_rx_wait_timeout(void *eloop_ctx, ++ void *timeout_ctx) ++{ ++ struct wpa_supplicant *wpa_s = eloop_ctx; ++ ++ if (!wpa_s->dpp_auth || !wpa_s->dpp_auth->auth_success) ++ return; ++ ++ wpa_printf(MSG_DEBUG, ++ "DPP: terminate exchange due to Configuration Request rx timeout"); ++ wpa_msg(wpa_s, MSG_INFO, DPP_EVENT_CONF_FAILED ++ "No Configuration Request received"); ++ ++ dpp_auth_deinit(wpa_s->dpp_auth); ++ wpa_s->dpp_auth = NULL; ++} ++ ++ + static int wpas_dpp_auth_init_next(struct wpa_supplicant *wpa_s) + { + struct dpp_authentication *auth = wpa_s->dpp_auth; +@@ -1776,8 +1796,12 @@ static void wpas_dpp_auth_success(struct wpa_supplicant *wpa_s, int initiator) + } + #endif /* CONFIG_TESTING_OPTIONS */ + +- if (wpa_s->dpp_auth->configurator) ++ if (wpa_s->dpp_auth->configurator) { + wpas_dpp_start_gas_server(wpa_s); ++ eloop_register_timeout(10, 0, ++ wpas_dpp_conf_req_rx_wait_timeout, ++ wpa_s, NULL); ++ } + else + wpas_dpp_start_gas_client(wpa_s); + } +@@ -2968,6 +2992,8 @@ wpas_dpp_gas_req_handler(void *ctx, void *resp_ctx, const u8 *sa, + query, query_len); + wpa_msg(wpa_s, MSG_INFO, DPP_EVENT_CONF_REQ_RX "src=" MACSTR, + MAC2STR(sa)); ++ if (auth->configurator) ++ eloop_cancel_timeout(wpas_dpp_conf_req_rx_wait_timeout, wpa_s, NULL); + resp = dpp_conf_req_rx(auth, query, query_len); + + #ifdef CONFIG_DPP2 +-- +2.20.1 + diff --git a/external/hostap/0006-Make-tls_engine_load_dynamic_generic-externally-acce.patch b/external/hostap/0006-Make-tls_engine_load_dynamic_generic-externally-acce.patch new file mode 100755 index 0000000..882fb82 --- /dev/null +++ b/external/hostap/0006-Make-tls_engine_load_dynamic_generic-externally-acce.patch @@ -0,0 +1,350 @@ +From 576ecf1eeddb08f2152720a2fcfe53dfe8fc32f0 Mon Sep 17 00:00:00 2001 +From: Andrew Beltrano +Date: Mon, 5 Apr 2021 10:35:56 -0600 +Subject: [PATCH 6/8] Make tls_engine_load_dynamic_generic externally + accessible. + +Expose tls_engine_load_dynamic_generic such that it can be used +by other code wishing to load an openssl engine dynamically. The +function is already written in way that is not specific to tls and was +moved verbatim. + +Signed-off-by: Andrew Beltrano +--- + src/crypto/openssl_engine.c | 98 ++++++++++++++++++++++ + src/crypto/openssl_engine.h | 10 +++ + src/crypto/tls_openssl.c | 86 +------------------ + wpa_supplicant/Android.mk | 11 +++ + wpa_supplicant/Makefile | 11 +++ + wpa_supplicant/nmake.mak | 1 + + wpa_supplicant/vs2005/wpasvc/wpasvc.vcproj | 4 + + 7 files changed, 136 insertions(+), 85 deletions(-) + create mode 100644 src/crypto/openssl_engine.c + create mode 100644 src/crypto/openssl_engine.h + +diff --git a/src/crypto/openssl_engine.c b/src/crypto/openssl_engine.c +new file mode 100644 +index 000000000..5fbc480d1 +--- /dev/null ++++ b/src/crypto/openssl_engine.c +@@ -0,0 +1,98 @@ ++/* ++ * Engine interface functions for OpenSSL ++ * Copyright (c) 2004-2021, Jouni Malinen ++ * ++ * This software may be distributed under the terms of the BSD license. ++ * See README for more details. ++ */ ++ ++#include "includes.h" ++ ++#include ++ ++#include "common.h" ++#include "openssl_engine.h" ++ ++/** ++ * tls_engine_load_dynamic_generic - load any openssl engine ++ * @pre: an array of commands and values that load an engine initialized ++ * in the engine specific function ++ * @post: an array of commands and values that initialize an already loaded ++ * engine (or %NULL if not required) ++ * @id: the engine id of the engine to load (only required if post is not %NULL ++ * ++ * This function is a generic function that loads any openssl engine. ++ * ++ * Returns: 0 on success, -1 on failure ++ */ ++int tls_engine_load_dynamic_generic(const char *pre[], ++ const char *post[], const char *id) ++{ ++ ENGINE *engine; ++ const char *dynamic_id = "dynamic"; ++ ++ engine = ENGINE_by_id(id); ++ if (engine) { ++ wpa_printf(MSG_DEBUG, "ENGINE: engine '%s' is already " ++ "available", id); ++ /* ++ * If it was auto-loaded by ENGINE_by_id() we might still ++ * need to tell it which PKCS#11 module to use in legacy ++ * (non-p11-kit) environments. Do so now; even if it was ++ * properly initialised before, setting it again will be ++ * harmless. ++ */ ++ goto found; ++ } ++ ERR_clear_error(); ++ ++ engine = ENGINE_by_id(dynamic_id); ++ if (engine == NULL) { ++ wpa_printf(MSG_INFO, "ENGINE: Can't find engine %s [%s]", ++ dynamic_id, ++ ERR_error_string(ERR_get_error(), NULL)); ++ return -1; ++ } ++ ++ /* Perform the pre commands. This will load the engine. */ ++ while (pre && pre[0]) { ++ wpa_printf(MSG_DEBUG, "ENGINE: '%s' '%s'", pre[0], pre[1]); ++ if (ENGINE_ctrl_cmd_string(engine, pre[0], pre[1], 0) == 0) { ++ wpa_printf(MSG_INFO, "ENGINE: ctrl cmd_string failed: " ++ "%s %s [%s]", pre[0], pre[1], ++ ERR_error_string(ERR_get_error(), NULL)); ++ ENGINE_free(engine); ++ return -1; ++ } ++ pre += 2; ++ } ++ ++ /* ++ * Free the reference to the "dynamic" engine. The loaded engine can ++ * now be looked up using ENGINE_by_id(). ++ */ ++ ENGINE_free(engine); ++ ++ engine = ENGINE_by_id(id); ++ if (engine == NULL) { ++ wpa_printf(MSG_INFO, "ENGINE: Can't find engine %s [%s]", ++ id, ERR_error_string(ERR_get_error(), NULL)); ++ return -1; ++ } ++ found: ++ while (post && post[0]) { ++ wpa_printf(MSG_DEBUG, "ENGINE: '%s' '%s'", post[0], post[1]); ++ if (ENGINE_ctrl_cmd_string(engine, post[0], post[1], 0) == 0) { ++ wpa_printf(MSG_DEBUG, "ENGINE: ctrl cmd_string failed:" ++ " %s %s [%s]", post[0], post[1], ++ ERR_error_string(ERR_get_error(), NULL)); ++ ENGINE_remove(engine); ++ ENGINE_free(engine); ++ return -1; ++ } ++ post += 2; ++ } ++ ENGINE_free(engine); ++ ++ return 0; ++} +diff --git a/src/crypto/openssl_engine.h b/src/crypto/openssl_engine.h +new file mode 100644 +index 000000000..50e625bac +--- /dev/null ++++ b/src/crypto/openssl_engine.h +@@ -0,0 +1,10 @@ ++/* ++ * Engine interface functions for OpenSSL ++ * Copyright (c) 2004-2021, Jouni Malinen ++ * ++ * This software may be distributed under the terms of the BSD license. ++ * See README for more details. ++ */ ++ ++int tls_engine_load_dynamic_generic(const char *pre[], ++ const char *post[], const char *id); +diff --git a/src/crypto/tls_openssl.c b/src/crypto/tls_openssl.c +index 345a35ee1..9463574a5 100644 +--- a/src/crypto/tls_openssl.c ++++ b/src/crypto/tls_openssl.c +@@ -23,6 +23,7 @@ + #include + #ifndef OPENSSL_NO_ENGINE + #include ++#include "openssl_engine.h" + #endif /* OPENSSL_NO_ENGINE */ + #ifndef OPENSSL_NO_DSA + #include +@@ -778,91 +779,6 @@ static void ssl_info_cb(const SSL *ssl, int where, int ret) + + + #ifndef OPENSSL_NO_ENGINE +-/** +- * tls_engine_load_dynamic_generic - load any openssl engine +- * @pre: an array of commands and values that load an engine initialized +- * in the engine specific function +- * @post: an array of commands and values that initialize an already loaded +- * engine (or %NULL if not required) +- * @id: the engine id of the engine to load (only required if post is not %NULL +- * +- * This function is a generic function that loads any openssl engine. +- * +- * Returns: 0 on success, -1 on failure +- */ +-static int tls_engine_load_dynamic_generic(const char *pre[], +- const char *post[], const char *id) +-{ +- ENGINE *engine; +- const char *dynamic_id = "dynamic"; +- +- engine = ENGINE_by_id(id); +- if (engine) { +- wpa_printf(MSG_DEBUG, "ENGINE: engine '%s' is already " +- "available", id); +- /* +- * If it was auto-loaded by ENGINE_by_id() we might still +- * need to tell it which PKCS#11 module to use in legacy +- * (non-p11-kit) environments. Do so now; even if it was +- * properly initialised before, setting it again will be +- * harmless. +- */ +- goto found; +- } +- ERR_clear_error(); +- +- engine = ENGINE_by_id(dynamic_id); +- if (engine == NULL) { +- wpa_printf(MSG_INFO, "ENGINE: Can't find engine %s [%s]", +- dynamic_id, +- ERR_error_string(ERR_get_error(), NULL)); +- return -1; +- } +- +- /* Perform the pre commands. This will load the engine. */ +- while (pre && pre[0]) { +- wpa_printf(MSG_DEBUG, "ENGINE: '%s' '%s'", pre[0], pre[1]); +- if (ENGINE_ctrl_cmd_string(engine, pre[0], pre[1], 0) == 0) { +- wpa_printf(MSG_INFO, "ENGINE: ctrl cmd_string failed: " +- "%s %s [%s]", pre[0], pre[1], +- ERR_error_string(ERR_get_error(), NULL)); +- ENGINE_free(engine); +- return -1; +- } +- pre += 2; +- } +- +- /* +- * Free the reference to the "dynamic" engine. The loaded engine can +- * now be looked up using ENGINE_by_id(). +- */ +- ENGINE_free(engine); +- +- engine = ENGINE_by_id(id); +- if (engine == NULL) { +- wpa_printf(MSG_INFO, "ENGINE: Can't find engine %s [%s]", +- id, ERR_error_string(ERR_get_error(), NULL)); +- return -1; +- } +- found: +- while (post && post[0]) { +- wpa_printf(MSG_DEBUG, "ENGINE: '%s' '%s'", post[0], post[1]); +- if (ENGINE_ctrl_cmd_string(engine, post[0], post[1], 0) == 0) { +- wpa_printf(MSG_DEBUG, "ENGINE: ctrl cmd_string failed:" +- " %s %s [%s]", post[0], post[1], +- ERR_error_string(ERR_get_error(), NULL)); +- ENGINE_remove(engine); +- ENGINE_free(engine); +- return -1; +- } +- post += 2; +- } +- ENGINE_free(engine); +- +- return 0; +-} +- +- + /** + * tls_engine_load_dynamic_pkcs11 - load the pkcs11 engine provided by opensc + * @pkcs11_so_path: pksc11_so_path from the configuration +diff --git a/wpa_supplicant/Android.mk b/wpa_supplicant/Android.mk +index f539ce134..cc5239c90 100644 +--- a/wpa_supplicant/Android.mk ++++ b/wpa_supplicant/Android.mk +@@ -276,6 +276,9 @@ NEED_JSON=y + NEED_GAS_SERVER=y + NEED_BASE64=y + NEED_ASN1=y ++ifndef OPENSSL_NO_ENGINE ++NEED_OPENSSL_ENGINE=y ++endif + ifdef CONFIG_DPP2 + L_CFLAGS += -DCONFIG_DPP2 + endif +@@ -1061,6 +1064,9 @@ endif + ifeq ($(CONFIG_TLS), openssl) + ifdef TLS_FUNCS + L_CFLAGS += -DEAP_TLS_OPENSSL ++ifndef OPENSSL_NO_ENGINE ++NEED_OPENSSL_ENGINE=y ++endif + OBJS += src/crypto/tls_openssl.c + OBJS += src/crypto/tls_openssl_ocsp.c + LIBS += -lssl +@@ -1652,6 +1658,11 @@ OBJS += src/utils/json.c + L_CFLAGS += -DCONFIG_JSON + endif + ++ifdef NEED_OPENSSL_ENGINE ++OBJS += src/crypto/openssl_engine.o ++CFLAGS += -DCONFIG_OPENSSL_ENGINE ++endi ++ + OBJS += src/drivers/driver_common.c + + OBJS += wpa_supplicant.c events.c bssid_ignore.c wpas_glue.c scan.c +diff --git a/wpa_supplicant/Makefile b/wpa_supplicant/Makefile +index 271f2aab3..14cc193da 100644 +--- a/wpa_supplicant/Makefile ++++ b/wpa_supplicant/Makefile +@@ -291,6 +291,9 @@ NEED_JSON=y + NEED_GAS_SERVER=y + NEED_BASE64=y + NEED_ASN1=y ++ifndef OPENSSL_NO_ENGINE ++NEED_OPENSSL_ENGINE=y ++endif + ifdef CONFIG_DPP2 + CFLAGS += -DCONFIG_DPP2 + endif +@@ -1104,6 +1107,9 @@ endif + ifeq ($(CONFIG_TLS), openssl) + ifdef TLS_FUNCS + CFLAGS += -DEAP_TLS_OPENSSL ++ifndef OPENSSL_NO_ENGINE ++NEED_OPENSSL_ENGINE=y ++endif + OBJS += ../src/crypto/tls_openssl.o + OBJS += ../src/crypto/tls_openssl_ocsp.o + LIBS += -lssl +@@ -1796,6 +1802,11 @@ OBJS += ../src/utils/json.o + CFLAGS += -DCONFIG_JSON + endif + ++ifdef NEED_OPENSSL_ENGINE ++OBJS += ../src/crypto/openssl_engine.o ++CFLAGS += -DCONFIG_OPENSSL_ENGINE ++endif ++ + ifdef CONFIG_MODULE_TESTS + CFLAGS += -DCONFIG_MODULE_TESTS + OBJS += wpas_module_tests.o +diff --git a/wpa_supplicant/nmake.mak b/wpa_supplicant/nmake.mak +index 617df036a..195faabe5 100644 +--- a/wpa_supplicant/nmake.mak ++++ b/wpa_supplicant/nmake.mak +@@ -122,6 +122,7 @@ OBJS = \ + $(OBJDIR)\l2_packet_winpcap.obj \ + $(OBJDIR)\tls_openssl.obj \ + $(OBJDIR)\ms_funcs.obj \ ++ $(OBJDIR)\openssl_engine.obj \ + $(OBJDIR)\crypto_openssl.obj \ + $(OBJDIR)\fips_prf_openssl.obj \ + $(OBJDIR)\pcsc_funcs.obj \ +diff --git a/wpa_supplicant/vs2005/wpasvc/wpasvc.vcproj b/wpa_supplicant/vs2005/wpasvc/wpasvc.vcproj +index 82d9033ff..b0b51a02c 100755 +--- a/wpa_supplicant/vs2005/wpasvc/wpasvc.vcproj ++++ b/wpa_supplicant/vs2005/wpasvc/wpasvc.vcproj +@@ -394,6 +394,10 @@ + RelativePath="..\..\scan.c" + > + ++ ++ + +-- +2.20.1 + diff --git a/external/hostap/0007-Drop-tls-prefix-from-tls_engine_load_dynamic_generic.patch b/external/hostap/0007-Drop-tls-prefix-from-tls_engine_load_dynamic_generic.patch new file mode 100755 index 0000000..61e3dc6 --- /dev/null +++ b/external/hostap/0007-Drop-tls-prefix-from-tls_engine_load_dynamic_generic.patch @@ -0,0 +1,88 @@ +From 5f6ac7228129a30d601c8aded449863d4ce0d5b8 Mon Sep 17 00:00:00 2001 +From: Andrew Beltrano +Date: Mon, 5 Apr 2021 10:43:34 -0600 +Subject: [PATCH 7/8] Drop tls prefix from tls_engine_load_dynamic_generic. + +Rename tls_engine_load_dynamic_generic to +openssl_engine_load_dynamic_generic, reflecting that it is not tls +specific. Update comment referencing tls to be generic. + +Signed-off-by: Andrew Beltrano +--- + src/crypto/openssl_engine.c | 11 +++++------ + src/crypto/openssl_engine.h | 2 +- + src/crypto/tls_openssl.c | 4 ++-- + 3 files changed, 8 insertions(+), 9 deletions(-) + +diff --git a/src/crypto/openssl_engine.c b/src/crypto/openssl_engine.c +index 5fbc480d1..9a0a19c77 100644 +--- a/src/crypto/openssl_engine.c ++++ b/src/crypto/openssl_engine.c +@@ -14,7 +14,7 @@ + #include "openssl_engine.h" + + /** +- * tls_engine_load_dynamic_generic - load any openssl engine ++ * openssl_engine_load_dynamic_generic - load any openssl engine + * @pre: an array of commands and values that load an engine initialized + * in the engine specific function + * @post: an array of commands and values that initialize an already loaded +@@ -25,7 +25,7 @@ + * + * Returns: 0 on success, -1 on failure + */ +-int tls_engine_load_dynamic_generic(const char *pre[], ++int openssl_engine_load_dynamic_generic(const char *pre[], + const char *post[], const char *id) + { + ENGINE *engine; +@@ -37,10 +37,9 @@ int tls_engine_load_dynamic_generic(const char *pre[], + "available", id); + /* + * If it was auto-loaded by ENGINE_by_id() we might still +- * need to tell it which PKCS#11 module to use in legacy +- * (non-p11-kit) environments. Do so now; even if it was +- * properly initialised before, setting it again will be +- * harmless. ++ * need to execute post-init commands. Do so now; even if ++ * it was properly initialised before, setting it again ++ * will be harmless. + */ + goto found; + } +diff --git a/src/crypto/openssl_engine.h b/src/crypto/openssl_engine.h +index 50e625bac..77b8d80d6 100644 +--- a/src/crypto/openssl_engine.h ++++ b/src/crypto/openssl_engine.h +@@ -6,5 +6,5 @@ + * See README for more details. + */ + +-int tls_engine_load_dynamic_generic(const char *pre[], ++int openssl_engine_load_dynamic_generic(const char *pre[], + const char *post[], const char *id); +diff --git a/src/crypto/tls_openssl.c b/src/crypto/tls_openssl.c +index 9463574a5..095b92b63 100644 +--- a/src/crypto/tls_openssl.c ++++ b/src/crypto/tls_openssl.c +@@ -814,7 +814,7 @@ static int tls_engine_load_dynamic_pkcs11(const char *pkcs11_so_path, + wpa_printf(MSG_DEBUG, "ENGINE: Loading pkcs11 Engine from %s", + pkcs11_so_path); + +- return tls_engine_load_dynamic_generic(pre_cmd, post_cmd, engine_id); ++ return openssl_engine_load_dynamic_generic(pre_cmd, post_cmd, engine_id); + } + + +@@ -842,7 +842,7 @@ static int tls_engine_load_dynamic_opensc(const char *opensc_so_path) + wpa_printf(MSG_DEBUG, "ENGINE: Loading OpenSC Engine from %s", + opensc_so_path); + +- return tls_engine_load_dynamic_generic(pre_cmd, NULL, engine_id); ++ return openssl_engine_load_dynamic_generic(pre_cmd, NULL, engine_id); + } + #endif /* OPENSSL_NO_ENGINE */ + +-- +2.20.1 + diff --git a/external/hostap/0008-DPP-Allow-loading-bootstrap-keys-using-an-OpenSSL-en.patch b/external/hostap/0008-DPP-Allow-loading-bootstrap-keys-using-an-OpenSSL-en.patch new file mode 100755 index 0000000..f9115af --- /dev/null +++ b/external/hostap/0008-DPP-Allow-loading-bootstrap-keys-using-an-OpenSSL-en.patch @@ -0,0 +1,262 @@ +From 74c6789d2d8fb532e309b3f80030861db4157bb6 Mon Sep 17 00:00:00 2001 +From: Andrew Beltrano +Date: Mon, 5 Apr 2021 20:41:17 -0600 +Subject: [PATCH 8/8] DPP: Allow loading bootstrap keys using an OpenSSL + engine. + +Add ability to load a DPP bootstrap key-pair using an arbitrary OpenSSL +engine instead of requiring the private key to be specified explicitly. +The engine name, so path, and key identifier must be specified to +enable loading a key using an OpenSSL engine. The key identifier is +an engine-specific field used to identify the key to load. + +Explicit private keys, if specified, take precedence over OpenSSL +engine-based keys. + +Signed-off-by: Andrew Beltrano +--- + src/common/dpp.c | 19 ++++++++ + src/common/dpp.h | 9 ++++ + src/common/dpp_crypto.c | 101 +++++++++++++++++++++++++++++++++++++++ + src/common/dpp_i.h | 7 +++ + wpa_supplicant/wpa_cli.c | 2 +- + 5 files changed, 137 insertions(+), 1 deletion(-) + +diff --git a/src/common/dpp.c b/src/common/dpp.c +index 3c8c7682d..4aa3f5c39 100644 +--- a/src/common/dpp.c ++++ b/src/common/dpp.c +@@ -180,6 +180,13 @@ void dpp_bootstrap_info_free(struct dpp_bootstrap_info *info) + os_free(info->info); + os_free(info->chan); + os_free(info->pk); ++#ifdef CONFIG_OPENSSL_ENGINE ++ os_free(info->key_id); ++ os_free(info->engine_id); ++ os_free(info->engine_path); ++ if (info->engine) ++ ENGINE_finish(info->engine); ++#endif /* CONFIG_OPENSSL_ENGINE */ + EVP_PKEY_free(info->pubkey); + str_clear_free(info->configurator_params); + os_free(info); +@@ -3893,6 +3900,11 @@ int dpp_bootstrap_gen(struct dpp_global *dpp, const char *cmd) + info = get_param(cmd, " info="); + curve = get_param(cmd, " curve="); + key = get_param(cmd, " key="); ++#ifdef CONFIG_OPENSSL_ENGINE ++ bi->key_id = get_param(cmd, " key_id="); ++ bi->engine_id = get_param(cmd, " engine="); ++ bi->engine_path = get_param(cmd, " engine_path="); ++#endif /* CONFIG_OPENSSL_ENGINE */ + + if (key) { + privkey_len = os_strlen(key) / 2; +@@ -3901,6 +3913,13 @@ int dpp_bootstrap_gen(struct dpp_global *dpp, const char *cmd) + hexstr2bin(key, privkey, privkey_len) < 0) + goto fail; + } ++#ifdef CONFIG_OPENSSL_ENGINE ++ else if (bi->key_id) { ++ bi->engine = dpp_load_engine(bi->engine_id, bi->engine_path); ++ if (!bi->engine) ++ goto fail; ++ } ++#endif /* CONFIG_OPENSSL_ENGINE */ + + if (dpp_keygen(bi, curve, privkey, privkey_len) < 0 || + dpp_parse_uri_chan_list(bi, bi->chan) < 0 || +diff --git a/src/common/dpp.h b/src/common/dpp.h +index 65ee905a7..df0806361 100644 +--- a/src/common/dpp.h ++++ b/src/common/dpp.h +@@ -12,6 +12,9 @@ + + #ifdef CONFIG_DPP + #include ++#ifdef CONFIG_OPENSSL_ENGINE ++#include ++#endif /* CONFIG_OPENSSL_ENGINE */ + + #include "utils/list.h" + #include "common/wpa_common.h" +@@ -166,6 +169,12 @@ struct dpp_bootstrap_info { + int nfc_negotiated; /* whether this has been used in NFC negotiated + * connection handover */ + char *configurator_params; ++#ifdef CONFIG_OPENSSL_ENGINE ++ char *key_id; ++ char *engine_id; ++ char *engine_path; ++ ENGINE *engine; ++#endif /* CONFIG_OPENSSL_ENGINE */ + }; + + #define PKEX_COUNTER_T_LIMIT 5 +diff --git a/src/common/dpp_crypto.c b/src/common/dpp_crypto.c +index c75fc7871..34d4732c1 100644 +--- a/src/common/dpp_crypto.c ++++ b/src/common/dpp_crypto.c +@@ -19,6 +19,9 @@ + #include "utils/json.h" + #include "common/ieee802_11_defs.h" + #include "crypto/crypto.h" ++#ifdef CONFIG_OPENSSL_ENGINE ++#include "crypto/openssl_engine.h" ++#endif + #include "crypto/random.h" + #include "crypto/sha384.h" + #include "crypto/sha512.h" +@@ -363,6 +366,100 @@ int dpp_pbkdf2(size_t hash_len, const u8 *password, size_t password_len, + #endif /* CONFIG_DPP2 */ + + ++#ifdef CONFIG_OPENSSL_ENGINE ++static EVP_PKEY * dpp_load_keypair(const struct dpp_curve_params **curve, ++ ENGINE *engine, const char *key_id) ++{ ++ EVP_PKEY *pkey; ++ EC_KEY *eckey; ++ const EC_GROUP *group; ++ int nid; ++ ++ pkey = ENGINE_load_private_key(engine, key_id, NULL, NULL); ++ if (!pkey) { ++ wpa_printf(MSG_ERROR, "ENGINE: cannot load private key with id '%s' [%s]", ++ key_id, ERR_error_string(ERR_get_error(), NULL)); ++ return NULL; ++ } ++ ++ eckey = EVP_PKEY_get1_EC_KEY(pkey); ++ if (!eckey) { ++ EVP_PKEY_free(pkey); ++ return NULL; ++ } ++ ++ group = EC_KEY_get0_group(eckey); ++ if (!group) { ++ EC_KEY_free(eckey); ++ EVP_PKEY_free(pkey); ++ return NULL; ++ } ++ ++ nid = EC_GROUP_get_curve_name(group); ++ *curve = dpp_get_curve_nid(nid); ++ if (!*curve) { ++ wpa_printf(MSG_INFO, ++ "DPP: Unsupported curve (nid=%d) in pre-assigned key", ++ nid); ++ EC_KEY_free(eckey); ++ EVP_PKEY_free(pkey); ++ return NULL; ++ } ++ ++ EC_KEY_free(eckey); ++ return pkey; ++} ++ ++ ++static int dpp_openssl_engine_load_dynamic(const char *engine_id, ++ const char *engine_path) ++{ ++ const char *pre_cmd[] = { ++ "SO_PATH", engine_path, ++ "ID", engine_id, ++ "LIST_ADD", "1", ++ "LOAD", NULL, ++ NULL, NULL ++ }; ++ const char *post_cmd[] = { ++ NULL, NULL ++ }; ++ ++ if (!engine_id || !engine_path) ++ return 0; ++ ++ wpa_printf(MSG_DEBUG, "ENGINE: Loading %s Engine from %s", ++ engine_id, engine_path); ++ ++ return openssl_engine_load_dynamic_generic(pre_cmd, post_cmd, engine_id); ++} ++ ++ ++ENGINE * dpp_load_engine(const char *engine_id, const char *engine_path) ++{ ++ if (dpp_openssl_engine_load_dynamic(engine_id, engine_path) < 0) ++ return NULL; ++ ++ ENGINE *engine = ENGINE_by_id(engine_id); ++ if (!engine) { ++ wpa_printf(MSG_ERROR, "ENGINE: engine %s not available [%s]", ++ engine_id, ERR_error_string(ERR_get_error(), NULL)); ++ return NULL; ++ } ++ ++ if (ENGINE_init(engine) != 1) { ++ wpa_printf(MSG_ERROR, "ENGINE: engine init failed " ++ "(engine: %s) [%s]", engine_id, ++ ERR_error_string(ERR_get_error(), NULL)); ++ ENGINE_free(engine); ++ return NULL; ++ } ++ ++ return engine; ++} ++#endif /* CONFIG_OPENSSL_ENGINE */ ++ ++ + int dpp_bn2bin_pad(const BIGNUM *bn, u8 *pos, size_t len) + { + int num_bytes, offset; +@@ -730,6 +827,10 @@ int dpp_keygen(struct dpp_bootstrap_info *bi, const char *curve, + + if (privkey) + bi->pubkey = dpp_set_keypair(&bi->curve, privkey, privkey_len); ++#ifdef CONFIG_OPENSSL_ENGINE ++ else if (bi->engine) ++ bi->pubkey = dpp_load_keypair(&bi->curve, bi->engine, bi->key_id); ++#endif /* CONFIG_OPENSSL_ENGINE */ + else + bi->pubkey = dpp_gen_keypair(bi->curve); + if (!bi->pubkey) +diff --git a/src/common/dpp_i.h b/src/common/dpp_i.h +index af12467a5..ff8c16592 100644 +--- a/src/common/dpp_i.h ++++ b/src/common/dpp_i.h +@@ -12,6 +12,10 @@ + + #ifdef CONFIG_DPP + ++#ifdef CONFIG_OPENSSL_ENGINE ++#include ++#endif /* CONFIG_OPENSSL_ENGINE */ ++ + struct dpp_global { + void *msg_ctx; + struct dl_list bootstrap; /* struct dpp_bootstrap_info */ +@@ -139,6 +143,9 @@ char * dpp_sign_connector(struct dpp_configurator *conf, + const struct wpabuf *dppcon); + int dpp_test_gen_invalid_key(struct wpabuf *msg, + const struct dpp_curve_params *curve); ++#ifdef CONFIG_OPENSSL_ENGINE ++ENGINE * dpp_load_engine(const char *engine_id, const char *engine_path); ++#endif /* CONFIG_OPENSSL_ENGINE */ + + struct dpp_reconfig_id { + const EC_GROUP *group; +diff --git a/wpa_supplicant/wpa_cli.c b/wpa_supplicant/wpa_cli.c +index fea7b85e0..11bb63dc3 100644 +--- a/wpa_supplicant/wpa_cli.c ++++ b/wpa_supplicant/wpa_cli.c +@@ -3855,7 +3855,7 @@ static const struct wpa_cli_cmd wpa_cli_commands[] = { + "report a scanned DPP URI from a QR Code" }, + { "dpp_bootstrap_gen", wpa_cli_cmd_dpp_bootstrap_gen, NULL, + cli_cmd_flag_sensitive, +- "type= [chan=..] [mac=..] [info=..] [curve=..] [key=..] = generate DPP bootstrap information" }, ++ "type= [chan=..] [mac=..] [info=..] [curve=..] [key=..] [key_id=..] [engine=..] [engine_path=..] = generate DPP bootstrap information" }, + { "dpp_bootstrap_remove", wpa_cli_cmd_dpp_bootstrap_remove, NULL, + cli_cmd_flag_none, + "*| = remove DPP bootstrap information" }, +-- +2.20.1 + diff --git a/external/hostap/CMakeLists.txt b/external/hostap/CMakeLists.txt new file mode 100644 index 0000000..f35cab7 --- /dev/null +++ b/external/hostap/CMakeLists.txt @@ -0,0 +1,30 @@ + +find_program(MAKE_EXE + NAMES gmake nmake make + REQUIRED +) + +file(GLOB + HOSTAP_PATCHES ${CMAKE_CURRENT_SOURCE_DIR}/*.patch +) + +set(HOSTAP_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/hostap-prefix/src/hostap/wpa_supplicant) + +ExternalProject_Add(hostap + GIT_REPOSITORY "http://w1.fi/hostap.git" + GIT_TAG "5f082c158c49a23614b69e4465e9a7299f004f5f" + CONFIGURE_COMMAND cp -n ${CMAKE_CURRENT_SOURCE_DIR}/.config ${HOSTAP_PREFIX}/.config + PATCH_COMMAND ${GIT_EXECUTABLE} config user.name "ztp-builder" + COMMAND ${GIT_EXECUTABLE} config user.email "ztp-builder@microsoft.com" + COMMAND ${GIT_EXECUTABLE} am -p1 --ignore-whitespace ${HOSTAP_PATCHES} + BINARY_DIR ${HOSTAP_PREFIX} + BUILD_COMMAND $(MAKE) libwpa_client.so + INSTALL_DIR ${HOSTAP_PREFIX} + INSTALL_COMMAND $(MAKE) install +) + +set(LIBWPA_CLIENT + ${HOSTAP_PREFIX}/libwpa_client.so + CACHE FILEPATH + "wpa client shared object" +) diff --git a/samples/ztpd/config/config-bootstrapinfo.json b/samples/ztpd/config/config-bootstrapinfo.json new file mode 100644 index 0000000..32974eb --- /dev/null +++ b/samples/ztpd/config/config-bootstrapinfo.json @@ -0,0 +1,122 @@ +[ + { + "deviceId": "0ae290e2-3702-4ef0-a7ca-284947d896e5", + "status": "enabled", + "isEdge": true, + "deviceGroupName": "pub0", + "desiredHubDeviceId": "123", + "authTemplateName": "defaultTemplate", + "createdDateTimeUtc": "2020-02-01T12:34:56.789Z", + "optionalDeviceInformation": { + "WifiDpp": { + "BootstrapKeyEccPubHashChirp": "edb4037de869af5ad7d02665205ad5e26c3810a4d39d61e70ed34135ca7a496e", + "Uri": "DPP:V:2;K:MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgACE0vdn8KsfXKHusJPcEscx+naQyQJLSob1VjuqPsP6r8=;;" + } + } + }, + { + "deviceId": "6188b64d-0f87-486e-85c8-06e3d9af8817", + "status": "enabled", + "isEdge": true, + "deviceGroupName": "pub1", + "desiredHubDeviceId": "123", + "authTemplateName": "defaultTemplate", + "createdDateTimeUtc": "2020-02-01T12:34:56.789Z", + "optionalDeviceInformation": { + "WifiDpp": { + "BootstrapKeyEccPubHashChirp": "dfb32c76fde287a1911d6f1fb13310ea8b8019aa95fbe4211499f8e664d932c6", + "Uri": "DPP:V:2;K:MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgACiLN+2Rk4tRlwl4CKYkSEdheJIEbZO5UBr9SPoPFI394=;;" + } + } + }, + { + "deviceId": "28dd5122-cd2d-41f2-b89f-e71b798a2704", + "status": "enabled", + "isEdge": true, + "deviceGroupName": "pub2", + "desiredHubDeviceId": "123", + "authTemplateName": "defaultTemplate", + "createdDateTimeUtc": "2020-04-01T12:34:56.789Z", + "optionalDeviceInformation": { + "WifiDpp": { + "BootstrapKeyEccPubHashChirp": "dfb32c76fde287a1911d6f1fb13310ea8b8019aa95fbe4211499f8e664d932c6", + "Uri": "DPP:V:2;K:MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgACCcWFqRtN+f0loEUgGIXDnMXPrjl92u2pV97Ff6DjUD8=;;" + } + } + }, + { + "deviceId": "bd9a16e3-018c-4b09-81c6-b6c08ba86634", + "status": "disabled", + "isEdge": true, + "deviceGroupName": "pub3", + "desiredHubDeviceId": "123", + "authTemplateName": "defaultTemplate", + "createdDateTimeUtc": "2020-06-01T12:34:56.789Z", + "optionalDeviceInformation": { + "WifiDpp": { + "BootstrapKeyEccPubHashChirp": "d12f07cfeea3b0b16d97fb391c54eca597078016c2d777db8f98014b82f4c4dc", + "Uri": "DPP:V:2;K:MEYwEAYHKoZIzj0CAQYFK4EEACIDMgACZDO9w/1O5wfVAmxt9amwOi4utQSMd2l/rn4ru/9+03C/Ree8nXgQhYeontV9lKS3;;" + } + } + }, + { + "deviceId": "646aacb0-fd8b-403a-b2cd-634f13382707", + "status": "enabled", + "isEdge": false, + "deviceGroupName": "pub4", + "desiredHubDeviceId": "123", + "authTemplateName": "defaultTemplate", + "createdDateTimeUtc": "2020-08-01T12:34:56.789Z", + "optionalDeviceInformation": { + "WifiDpp": { + "BootstrapKeyEccPubHashChirp": "14aab6f7f95b5684645436d1c4d3ce03ed765edbfa5168fe0cef6cdcd2b6312f", + "Uri": "DPP:V:2;K:MEYwEAYHKoZIzj0CAQYFK4EEACIDMgADsNfU7L1hyWzyRqIUXNeX3B0P07NTmz6OKs5BHU7XNrlfKcc9UVy8HI6RhNEmNh+b;;" + } + } + }, + { + "deviceId": "9b3371f7-4551-4103-83b2-4560ed31b9a1", + "status": "enabled", + "isEdge": true, + "deviceGroupName": "pub5", + "desiredHubDeviceId": "123", + "authTemplateName": "defaultTemplate", + "createdDateTimeUtc": "2020-10-01T12:34:56.789Z", + "optionalDeviceInformation": { + "WifiDpp": { + "BootstrapKeyEccPubHashChirp": "626811eeb637fd63b29e279e411b9942947237fb0dcc1d103869fe61c87abc03", + "Uri": "DPP:V:2;K:MFgwEAYHKoZIzj0CAQYFK4EEACMDRAACARSJbFYU/AHogatLzs1qRlfKR8tlVSf+YO1OEqU05EL9p5Q43nvDo0UPE/xTGGNYyi4hf/pMViQuNANloMs0gTKB;;" + } + } + }, + { + "deviceId": "9b04f08d-59d6-40de-a532-d059051a9f12", + "status": "enabled", + "isEdge": true, + "deviceGroupName": "pub6", + "desiredHubDeviceId": "123", + "authTemplateName": "defaultTemplate", + "createdDateTimeUtc": "2020-10-01T12:34:56.789Z", + "optionalDeviceInformation": { + "WifiDpp": { + "BootstrapKeyEccPubHashChirp": "478101b3706af3c7a0fa0a3913256ac4147fe0a36643ff6a05ac8c6d26830792", + "Uri": "DPP:V:1;K:MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgADehJwKe6jTkGl4zenL39BNuEMf4VXwKXa6a3XOikZN0Y=;;" + } + } + }, + { + "deviceId": "d5dd2363-09d9-4a2b-b53f-910aea9e7ffc", + "status": "enabled", + "isEdge": true, + "deviceGroupName": "pub7", + "desiredHubDeviceId": "123", + "authTemplateName": "defaultTemplate", + "createdDateTimeUtc": "2020-10-01T12:34:56.789Z", + "optionalDeviceInformation": { + "WifiDpp": { + "BootstrapKeyEccPubHashChirp": "7ab40c2ee86a8297b3f2252dc82cffd38a5987852498bf54099aae1f460a0f86", + "Uri": "DPP:V:2;K:MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgACcafpagzR/jXJWFZzHemcRP859ukO0GG3qCSTKScftjg=;;" + } + } + } +] \ No newline at end of file diff --git a/samples/ztpd/config/config-configurator.json b/samples/ztpd/config/config-configurator.json new file mode 100644 index 0000000..7395402 --- /dev/null +++ b/samples/ztpd/config/config-configurator.json @@ -0,0 +1,65 @@ +{ + "bootstrap.info": { + "expirationTime": 1440, + "providers": [ + { + "type": "file", + "name": "azure-dps-records", + "expirationTime": 300, + "path": "/etc/ztpd/config-bootstrapinfo.json", + "decodingInfo": { + "pointer": "/", + "pointerBase": "/optionalDeviceInformation/WifiDpp", + "propertyMap": { + "dppUri": "Uri", + "publicKeyHash": "BootstrapKeyEccPubHashChirp" + } + } + }, + { + "type": "azuredps", + "name": "Azure DPS (SAS Authentication)", + "expirationTime": 600, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "connectionString": "HostName=wifizerotouch.azure-devices-provisioning.net;SharedAccessKeyName=serviceread;SharedAccessKey=ab+CD0/eFghIJkLm1NoP2qRsTU3PlaCeHOLdErGHIJk=" + } + }, + { + "type": "azuredps", + "name": "Azure DPS (OAuth2 Authentication)", + "expirationTime": 900, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "clientId": "fd9d9681-8b12-4971-854a-ae16a2fff32d", + "clientSecret": "3cgcxvQ9--PlaCeholdErnkeR_G3.7eD-2", + "resourceUri": "https://azure-devices-provisioning.net/.default" + } + } + ] + }, + "network.configuration": { + "default": { + "discovery": { + "ssid": "xmarksthespot", + "ssidCharset": 106 + }, + "credentials": [ + { + "akm": "psk", + "passphrase": "thetruthisoutthere" + }, + { + "akm": "psk", + "psk": "746865747275746869736f75747468657265" + }, + { + "akm": "sae", + "passphrase": "thetruthisoutthere" + } + ] + } + } +} \ No newline at end of file diff --git a/samples/ztpd/config/config-configurator.schema.json b/samples/ztpd/config/config-configurator.schema.json new file mode 100644 index 0000000..cb9fe48 --- /dev/null +++ b/samples/ztpd/config/config-configurator.schema.json @@ -0,0 +1,812 @@ +{ + "$id": "https://developer.microsoft.com/wifi-ztp/ztpd/config-configurator.schema.json", + "$schema": "http://json-schema.org/draft-07/schema", + "default": {}, + "description": "ztpd Device Provisioning Protocol (DPP) configurator role configuration.", + "examples": [ + { + "bootstrap.info": { + "expirationTime": 1440, + "providers": [ + { + "type": "file", + "name": "azure-dps-records", + "expirationTime": 300, + "path": "/etc/ztpd/config-bootstrapinfo.json", + "decodingInfo": { + "pointer": "/", + "pointerBase": "/optionalDeviceInformation/WifiDpp", + "propertyMap": { + "dppUri": "Uri", + "publicKeyHash": "BootstrapKeyEccPubHashChirp" + } + } + }, + { + "type": "azuredps", + "name": "Azure DPS (SAS Authentication)", + "expirationTime": 600, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "connectionString": "HostName=wifizerotouch.azure-devices-provisioning.net;SharedAccessKeyName=serviceread;SharedAccessKey=ab+CD0/eFghIJkLm1NoP2qRsTU3PlaCeHOLdErGHIJk=" + } + }, + { + "type": "azuredps", + "name": "Azure DPS (OAuth2 Authentication)", + "expirationTime": 900, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "clientId": "fd9d9681-8b12-4971-854a-ae16a2fff32d", + "clientSecret": "3cgcxvQ9--PlaCeholdErnkeR_G3.7eD-2", + "resourceUri": "https://azure-devices-provisioning.net/.default" + } + } + ] + }, + "network.configuration": { + "default": { + "discovery": { + "ssid": "xmarksthespot", + "ssidCharset": 106 + }, + "credentials": [ + { + "akm": "psk", + "passphrase": "thetruthisoutthere" + }, + { + "akm": "psk", + "psk": "746865747275746869736f75747468657265" + }, + { + "akm": "sae", + "passphrase": "thetruthisoutthere" + } + ] + } + } + } + ], + "required": [ + "bootstrap.info", + "network.configuration" + ], + "title": "ztpd configurator configuration.", + "type": "object", + "properties": { + "bootstrap.info": { + "$id": "#/properties/bootstrap.info", + "default": {}, + "description": "Device provisioning protocol bootstrapping information.", + "examples": [ + { + "expirationTime": 1440, + "providers": [ + { + "type": "file", + "name": "azure-dps-records", + "expirationTime": 300, + "path": "/etc/ztpd/config-bootstrapinfo.json", + "decodingInfo": { + "pointer": "/", + "pointerBase": "/optionalDeviceInformation/WifiDpp", + "propertyMap": { + "dppUri": "Uri", + "publicKeyHash": "BootstrapKeyEccPubHashChirp" + } + } + }, + { + "type": "azuredps", + "name": "Azure DPS (SAS Authentication)", + "expirationTime": 600, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "connectionString": "HostName=wifizerotouch.azure-devices-provisioning.net;SharedAccessKeyName=serviceread;SharedAccessKey=ab+CD0/eFghIJkLm1NoP2qRsTU3PlaCeHOLdErGHIJk=" + } + }, + { + "type": "azuredps", + "name": "Azure DPS (OAuth2 Authentication)", + "expirationTime": 900, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "clientId": "fd9d9681-8b12-4971-854a-ae16a2fff32d", + "clientSecret": "3cgcxvQ9--PlaCeholdErnkeR_G3.7eD-2", + "resourceUri": "https://azure-devices-provisioning.net/.default" + } + } + ] + } + ], + "required": [ + "expirationTime", + "providers" + ], + "title": "Device Provisioning Protocol (DPP) bootstrapping information.", + "type": "object", + "properties": { + "expirationTime": { + "$id": "#/properties/bootstrap.info/properties/expirationTime", + "default": 0, + "description": "The global expiration time in minutes of bootstrapping records. If a specific provider does not specify an expiration time, this value is used.", + "examples": [ + 1440 + ], + "title": "Bootstrapping record expiration time (seconds).", + "type": "integer" + }, + "providers": { + "$id": "#/properties/bootstrap.info/properties/providers", + "default": [], + "description": "Array of supported bootstrapping information providers. Each bootstrapping information provider manages records which describe the enrollee devices the configurator is capable of proivisioning.", + "examples": [ + [ + { + "type": "file", + "name": "azure-dps-records", + "expirationTime": 300, + "path": "/etc/ztpd/config-bootstrapinfo.json", + "decodingInfo": { + "pointer": "/", + "pointerBase": "/optionalDeviceInformation/WifiDpp", + "propertyMap": { + "dppUri": "Uri", + "publicKeyHash": "BootstrapKeyEccPubHashChirp" + } + } + }, + { + "type": "azuredps", + "name": "Azure DPS (SAS Authentication)", + "expirationTime": 600, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "connectionString": "HostName=wifizerotouch.azure-devices-provisioning.net;SharedAccessKeyName=serviceread;SharedAccessKey=ab+CD0/eFghIJkLm1NoP2qRsTU3PlaCeHOLdErGHIJk=" + } + } + ] + ], + "title": "Bootstrapping information providers.", + "type": "array", + "additionalItems": false, + "items": { + "$id": "#/properties/bootstrap.info/properties/providers/items", + "anyOf": [ + { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0", + "default": {}, + "description": "Bootstrapping information provider settings when type is 'file'.", + "examples": [ + { + "type": "file", + "name": "azure-dps-records", + "expirationTime": 300, + "path": "/etc/ztpd/config-bootstrapinfo.json", + "decodingInfo": { + "pointer": "/", + "pointerBase": "/optionalDeviceInformation/WifiDpp", + "propertyMap": { + "dppUri": "Uri", + "publicKeyHash": "BootstrapKeyEccPubHashChirp" + } + } + } + ], + "required": [ + "type", + "name", + "expirationTime", + "path", + "decodingInfo" + ], + "title": "File-based bootstrapping information provider configuration.", + "type": "object", + "properties": { + "type": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/type", + "const": "file", + "default": "", + "description": "The type of bootstrapping information provider.", + "examples": [ + "file" + ], + "title": "Bootstrapping information provider type.", + "type": "string" + }, + "name": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/name", + "default": "", + "description": "The name to associate with this instance of the bootstrapping information provider. This name will appear in log messages to identify the provider in question.", + "examples": [ + "azure-dps-records" + ], + "title": "Bootstrapping information provider name.", + "type": "string" + }, + "expirationTime": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/expirationTime", + "default": 0, + "description": "The expiration time in minutes of bootstrapping records. If no value is specified, the records never expire and consequently will never be updated.", + "examples": [ + 300 + ], + "title": "Bootstrapping information provider record expiration time (seconds).", + "type": "integer" + }, + "path": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/path", + "default": "", + "description": "The absolute filesystem path to the json file containing bootstrapping information.", + "examples": [ + "/etc/ztpd/config-bootstrapinfo.json" + ], + "title": "Bootstrapping information file path.", + "type": "string" + }, + "decodingInfo": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/decodingInfo", + "type": "object", + "title": "Bootstrapping provider information decoding map.", + "description": "Information describing how to map objects in the file to required Device Provisioning Protocol (DPP) enrollee identification information.", + "default": {}, + "examples": [ + { + "pointer": "/", + "pointerBase": "/optionalDeviceInformation/WifiDpp", + "propertyMap": { + "dppUri": "Uri", + "publicKeyHash": "BootstrapKeyEccPubHashChirp" + } + } + ], + "required": [ + "pointer", + "pointerBase", + "propertyMap" + ], + "properties": { + "pointer": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/decodingInfo/properties/pointer", + "default": "", + "description": "A JSON pointer referencing the array that contains Device Provisioning Protocol (DPP) enrollee identification.", + "examples": [ + "/" + ], + "title": "Enrollee bootstrapping information array JSON pointer.", + "type": "string" + }, + "pointerBase": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/decodingInfo/properties/pointerBase", + "default": "", + "description": "A JSON pointer, relative to the JSON pointer specified in the \"pointer\" property, referencing the sub-object containing the Device Provisioning Protocol (DPP) enrollee identification information.", + "examples": [ + "/optionalDeviceInformation/WifiDpp" + ], + "title": "Enrollee bootstrapping information object JSON pointer.", + "type": "string" + }, + "propertyMap": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/decodingInfo/properties/propertyMap", + "default": {}, + "description": "A table of properties mapping JSON property names to required Device Provisioning Protocol (DPP) values.", + "examples": [ + { + "dppUri": "Uri", + "publicKeyHash": "BootstrapKeyEccPubHashChirp" + } + ], + "required": [ + "dppUri", + "publicKeyHash" + ], + "title": "Device Provisioning Protocol (DPP) Enrollee property map.", + "type": "object", + "properties": { + "dppUri": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/decodingInfo/properties/propertyMap/properties/dppUri", + "default": "", + "description": "The name of the JSON property which holds the Device Provisioning Protocol bootstrapping URI.", + "examples": [ + "Uri" + ], + "title": "Device Provisioning Protocol (DPP) Bootstrapping URI property name.", + "type": "string" + }, + "publicKeyHash": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/0/properties/decodingInfo/properties/propertyMap/properties/publicKeyHash", + "default": "", + "description": "The name of the JSON property which holds the Device Provisioning Protocol (DPP) public bootstrapping key \"chirp\" hash.", + "examples": [ + "BootstrapKeyEccPubHashChirp" + ], + "title": "Device Provisioning Protocol (DPP) Bootstrapping public key chirp hash name.", + "type": "string" + } + } + } + } + } + } + }, + { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/1", + "default": {}, + "description": "Bootstrapping information provider settings when type is 'azuredps' and uses Shared Access Signature (SAS) token authentication.", + "examples": [ + { + "type": "azuredps", + "name": "Azure DPS (SAS Authentication)", + "expirationTime": 600, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "connectionString": "HostName=wifizerotouch.azure-devices-provisioning.net;SharedAccessKeyName=serviceread;SharedAccessKey=ab+CD0/eFghIJkLm1NoP2qRsTU3PlaCeHOLdErGHIJk=" + } + } + ], + "required": [ + "type", + "name", + "serviceEndpointUri", + "authentication" + ], + "title": "Azure Device Provisioning Service (DPS) based bootstrapping information provider configuration.", + "type": "object", + "properties": { + "type": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/1/properties/type", + "default": "", + "description": "The type of bootstrapping information provider.", + "examples": [ + "azuredps" + ], + "title": "Bootstrapping information provider type.", + "type": "string" + }, + "name": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/1/properties/name", + "default": "", + "description": "The name to associate with this instance of the bootstrapping information provider. This name will appear in log messages to identify the provider in question.", + "examples": [ + "Azure DPS (SAS Authentication)" + ], + "title": "Bootstrapping information provider name.", + "type": "string" + }, + "expirationTime": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/1/properties/expirationTime", + "default": 0, + "description": "The expiration time in minutes of bootstrapping records. If no value is specified, the records never expire and consequently will never be updated.", + "examples": [ + 600 + ], + "title": "Bootstrapping record expiration time (seconds).", + "type": "integer" + }, + "serviceEndpointUri": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/1/properties/serviceEndpointUri", + "default": "", + "description": "The service endpoint URI of the Device Provisioning Service (DPS) instance.", + "examples": [ + "wifizerotouch.azure-devices-provisioning.net" + ], + "title": "Azure Device Provisioning Service (DPS) endpoint URI.", + "type": "string" + }, + "authentication": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/1/properties/authentication", + "type": "object", + "title": "Shared Access Key (SAS) authentication configuration.", + "description": "An explanation about the purpose of this instance.", + "default": {}, + "examples": [ + { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "connectionString": "HostName=wifizerotouch.azure-devices-provisioning.net;SharedAccessKeyName=serviceread;SharedAccessKey=ab+CD0/eFghIJkLm1NoP2qRsTU3PlaCeHOLdErGHIJk=" + } + ], + "required": [ + "authorityUrl", + "connectionString" + ], + "properties": { + "authorityUrl": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/1/properties/authentication/properties/authorityUrl", + "default": "", + "description": "The OAuth2 authority url endpoint which grants authorization tokens.", + "examples": [ + "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token" + ], + "title": "SAS Authority URL.", + "type": "string" + }, + "connectionString": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/1/properties/authentication/properties/connectionString", + "default": "", + "description": "The connection string for the Device Provisioning Service (DPS) instance. See https://docs.microsoft.com/en-us/azure/iot-dps/quick-setup-auto-provision-cli#get-the-connection-string-for-the-iot-hub.", + "examples": [ + "HostName=wifizerotouch.azure-devices-provisioning.net;SharedAccessKeyName=serviceread;SharedAccessKey=ab+CD0/eFghIJkLm1NoP2qRsTU3PlaCeHOLdErGHIJk=" + ], + "title": "SAS connection string.", + "type": "string" + } + } + } + } + }, + { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2", + "default": {}, + "description": "Bootstrapping information provider settings when type is 'azuredps' and uses Client Credential grant type OAuth2 authentication.", + "examples": [ + { + "type": "azuredps", + "name": "Azure DPS (OAuth2 Authentication)", + "expirationTime": 900, + "serviceEndpointUri": "wifizerotouch.azure-devices-provisioning.net", + "authentication": { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "clientId": "fd9d9681-8b12-4971-854a-ae16a2fff32d", + "clientSecret": "3cgcxvQ9--PlaCeholdErnkeR_G3.7eD-2", + "resourceUri": "https://azure-devices-provisioning.net/.default" + } + } + ], + "required": [ + "type", + "name", + "serviceEndpointUri", + "authentication" + ], + "title": "Azure Device Provisioning Service (DPS) based bootstrapping information provider using OAuth2.", + "type": "object", + "properties": { + "type": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/type", + "default": "", + "description": "The type of bootstrapping information provider.", + "examples": [ + "azuredps" + ], + "title": "Bootstrapping information provider type.", + "type": "string" + }, + "name": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/name", + "default": "", + "description": "The name to associate with this instance of the bootstrapping information provider. This name will appear in log messages to identify the provider in question.", + "examples": [ + "Azure DPS (SAS Authentication)" + ], + "title": "Bootstrapping information provider name.", + "type": "string" + }, + "expirationTime": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/expirationTime", + "default": 0, + "description": "The expiration time in minutes of bootstrapping records. If no value is specified, the records never expire and consequently will never be updated.", + "examples": [ + 600 + ], + "title": "Bootstrapping record expiration time (seconds).", + "type": "integer" + }, + "serviceEndpointUri": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/serviceEndpointUri", + "default": "", + "description": "The service endpoint URI of the Device Provisioning Service (DPS) instance.", + "examples": [ + "wifizerotouch.azure-devices-provisioning.net" + ], + "title": "Azure Device Provisioning Service (DPS) endpoint URI.", + "type": "string" + }, + "authentication": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/authentication", + "type": "object", + "title": "OAuth2 client credential grant type authentication configuration.", + "description": "An explanation about the purpose of this instance.", + "default": {}, + "examples": [ + { + "authorityUrl": "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token", + "clientId": "fd9d9681-8b12-4971-854a-ae16a2fff32d", + "clientSecret": "3cgcxvQ9--PlaCeholdErnkeR_G3.7eD-2", + "resourceUri": "https://azure-devices-provisioning.net/.default" + } + ], + "required": [ + "authorityUrl", + "clientId", + "clientSecret", + "resourceUri" + ], + "properties": { + "authorityUrl": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/authentication/properties/authorityUrl", + "default": "", + "description": "The authority url endpoint which grants OAuth2 authorization tokens.", + "examples": [ + "https://login.microsoftonline.com/ceb07937-2105-4e4a-aade-3b8b5c8b1208/oauth2/v2.0/token" + ], + "title": "OAuth2 authority URL.", + "type": "string" + }, + "clientId": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/authentication/properties/clientId", + "default": "", + "description": "The 'client_id' for client credential grant type OAuth2 authentication.", + "examples": [ + "fd9d9681-8b12-4971-854a-ae16a2fff32d" + ], + "title": "OAuth2 client identifier.", + "type": "string" + }, + "clientSecret": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/authentication/properties/clientSecret", + "default": "", + "description": "The 'client_secret' for client credential grant type OAuth2 authentication.", + "examples": [ + "3cgcxvQ9--PlaCeholdErnkeR_G3.7eD-2" + ], + "title": "OAuth2 client secret.", + "type": "string" + }, + "resourceUri": { + "$id": "#/properties/bootstrap.info/properties/providers/items/anyOf/2/properties/authentication/properties/resourceUri", + "default": "The resource URI of the endpoint that can be accessed with the requested token.", + "description": "", + "examples": [ + "https://azure-devices-provisioning.net/.default" + ], + "title": "OAuth2 resource URI.", + "type": "string" + } + } + } + } + } + ] + } + } + } + }, + "network.configuration": { + "$id": "#/properties/network.configuration", + "default": {}, + "description": "Network provisioning information mapping network configuration information to enrollee identities.", + "examples": [ + { + "default": { + "discovery": { + "ssid": "xmarksthespot", + "ssidCharset": 106 + }, + "credentials": [ + { + "akm": "psk", + "passphrase": "thetruthisoutthere" + }, + { + "akm": "psk", + "psk": "746865747275746869736f75747468657265" + }, + { + "akm": "sae", + "passphrase": "thetruthisoutthere" + } + ] + } + } + ], + "required": [ + "default" + ], + "title": "Network configuration for provisioning.", + "type": "object", + "properties": { + "default": { + "$id": "#/properties/network.configuration/properties/default", + "default": {}, + "description": "The default network to provision when a specific network for an enrollee has not been configured.", + "examples": [ + { + "discovery": { + "ssid": "xmarksthespot", + "ssidCharset": 106 + }, + "credentials": [ + { + "akm": "psk", + "passphrase": "thetruthisoutthere" + }, + { + "akm": "psk", + "psk": "746865747275746869736f75747468657265" + }, + { + "akm": "sae", + "passphrase": "thetruthisoutthere" + } + ] + } + ], + "required": [ + "discovery", + "credentials" + ], + "title": "Default network configuration.", + "type": "object", + "properties": { + "discovery": { + "$id": "#/properties/network.configuration/properties/default/properties/discovery", + "default": {}, + "description": "Target network discovery properties.", + "examples": [ + { + "ssid": "xmarksthespot", + "ssidCharset": 106 + } + ], + "required": [ + "ssid", + "ssidCharset" + ], + "title": "Network discovery information.", + "type": "object", + "properties": { + "ssid": { + "$id": "#/properties/network.configuration/properties/default/properties/discovery/properties/ssid", + "default": "", + "description": "The service set identifier (SSID) of the target network to provision.", + "examples": [ + "xmarksthespot" + ], + "title": "Service Set Identifier (SSID).", + "type": "string" + }, + "ssidCharset": { + "$id": "#/properties/network.configuration/properties/default/properties/discovery/properties/ssidCharset", + "default": 0, + "description": "The value of the character set that would be used to convert the SSID octet string into characters. This must be an MIBenum value from https://www.iana.org/assignments/character-sets/character-sets.xhtml.", + "examples": [ + 106 + ], + "title": "Service Set Identifier (SSID) character set.", + "type": "integer" + } + } + }, + "credentials": { + "$id": "#/properties/network.configuration/properties/default/properties/credentials", + "default": [], + "description": "An explanation about the purpose of this instance.", + "examples": [ + [ + { + "akm": "psk", + "passphrase": "thetruthisoutthere" + }, + { + "akm": "psk", + "psk": "746865747275746869736f75747468657265" + }, + { + "akm": "sae", + "passphrase": "thetruthisoutthere" + } + ] + ], + "minItems": 1, + "title": "Network credentials.", + "type": "array", + "additionalItems": false, + "items": { + "$id": "#/properties/network.configuration/properties/default/properties/credentials/items", + "minItems": 1, + "uniqueItems": true, + "type": "object", + "anyOf": [ + { + "$id": "#/properties/network.configuration/properties/default/properties/credentials/items/anyOf/0", + "default": {}, + "description": "The network configuration when a passphrase is specified.", + "examples": [ + { + "akm": "psk", + "passphrase": "thetruthisoutthere" + } + ], + "required": [ + "akm", + "passphrase" + ], + "title": "Network configuration for a passphrase.", + "type": "object", + "properties": { + "akm": { + "$id": "#/properties/network.configuration/properties/default/properties/credentials/items/anyOf/0/properties/akm", + "default": "", + "description": "The authentication and key management scheme for the network.", + "enum": [ + "psk", + "sae" + ], + "examples": [ + "psk" + ], + "title": "Network authentication and key management scheme.", + "type": "string" + }, + "passphrase": { + "$id": "#/properties/network.configuration/properties/default/properties/credentials/items/anyOf/0/properties/passphrase", + "default": "", + "description": "The passphrase for the network, encoded as ASCII.", + "examples": [ + "thetruthisoutthere" + ], + "title": "Network passphrase.", + "maxLength": 63, + "minLength": 8, + "type": "string" + } + } + }, + { + "$id": "#/properties/network.configuration/properties/default/properties/credentials/items/anyOf/1", + "default": {}, + "description": "The network configuration when a pre-shared key (psk) is specified.", + "examples": [ + { + "akm": "psk", + "psk": "746865747275746869736f75747468657265" + } + ], + "required": [ + "akm", + "psk" + ], + "title": "Network configuration for a pre-shared key.", + "type": "object", + "properties": { + "akm": { + "$id": "#/properties/network.configuration/properties/default/properties/credentials/items/anyOf/1/properties/akm", + "default": "", + "description": "The authentication and key management scheme for the network.", + "examples": [ + "psk" + ], + "title": "Network authentication and key management scheme.", + "const": "psk", + "type": "string" + }, + "psk": { + "$id": "#/properties/network.configuration/properties/default/properties/credentials/items/anyOf/1/properties/psk", + "default": "", + "description": "The pre-shared key (psk) for the network, encoded as hexadecimal.", + "examples": [ + "746865747275746869736f75747468657265" + ], + "title": "Network pre-shared key.", + "maxLength": 126, + "minLength": 16, + "type": "string" + } + } + } + ] + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/samples/ztpd/config/config-enrollee.json b/samples/ztpd/config/config-enrollee.json new file mode 100644 index 0000000..662267b --- /dev/null +++ b/samples/ztpd/config/config-enrollee.json @@ -0,0 +1,8 @@ +{ + "bootstrap.info": { + "type": "qrcode", + "keyId": "0x81000009", + "engineId": "tpm2tss", + "enginePath": "/usr/lib/x86_64-linux-gnu/engines-1.1/libtpm2tss.so" + } +} \ No newline at end of file diff --git a/samples/ztpd/config/config-enrollee.schema.json b/samples/ztpd/config/config-enrollee.schema.json new file mode 100644 index 0000000..df685f3 --- /dev/null +++ b/samples/ztpd/config/config-enrollee.schema.json @@ -0,0 +1,93 @@ +{ + "$id": "https://developer.microsoft.com/wifi-ztp/ztpd/config-enrollee.schema.json", + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "title": "ztpd enrollee configuration.", + "description": "ztpd Device Provisioning Protocol (DPP) enrollee role configuration.", + "default": {}, + "examples": [ + { + "bootstrap.info": { + "type": "qrcode", + "keyId": "0x81000009", + "engineId": "tpm2tss", + "enginePath": "/usr/lib/x86_64-linux-gnu/engines-1.1/libtpm2tss.so" + } + } + ], + "required": [ + "bootstrap.info" + ], + "properties": { + "bootstrap.info": { + "$id": "#/properties/bootstrap.info", + "default": {}, + "description": "Device provisioning protocol bootstrapping information.", + "examples": [ + { + "type": "qrcode", + "keyId": "0x81000009", + "engineId": "tpm2tss", + "enginePath": "/usr/lib/x86_64-linux-gnu/engines-1.1/libtpm2tss.so" + } + ], + "required": [ + "type", + "keyId", + "engineId", + "enginePath" + ], + "title": "Device Provisioning Protocol (DPP) bootstrapping information.", + "type": "object", + "properties": { + "type": { + "$id": "#/properties/bootstrap.info/properties/type", + "default": "", + "description": "Device Provisioning Protocol (DPP) bootstrapping method.", + "enum": [ + "qrcode", + "pkex", + "nfc", + "bluetooth", + "cloud" + ], + "examples": [ + "qrcode" + ], + "title": "Device Provisioning Protocol (DPP) bootstrapping type.", + "type": "string" + }, + "keyId": { + "$id": "#/properties/bootstrap.info/properties/keyId", + "default": "", + "description": "A unique identifier for the device provisioning protocol bootstrapping key as it is represented in the configured OpenSSL engine. OpenSSL egnines may have specific key encoding rules and so the value format is specific to the engine being used.", + "examples": [ + "0x81000009" + ], + "title": "OpenSSL bootstrapping key identifier.", + "type": "string" + }, + "engineId": { + "$id": "#/properties/bootstrap.info/properties/engineId", + "default": "", + "description": "The OpenSSL engine (name) to use for device provisioning protocol bootstrapping private key operations.", + "examples": [ + "tpm2tss" + ], + "title": "OpenSSL engine identifier.", + "type": "string" + }, + "enginePath": { + "$id": "#/properties/bootstrap.info/properties/enginePath", + "default": "", + "description": "The absolute path to the OpenSSL engine shared object.", + "examples": [ + "/usr/lib/x86_64-linux-gnu/engines-1.1/libtpm2tss.so" + ], + "title": "OpenSSL engine shared object path.", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/samples/ztpd/config/config.json b/samples/ztpd/config/config.json new file mode 100644 index 0000000..910d33d --- /dev/null +++ b/samples/ztpd/config/config.json @@ -0,0 +1,23 @@ +{ + "ui.activation": [ "command", "gpio" ], + "ui.activation.unit": "hostapd.service", + "ui.activation.gpio": { + "chip": "gpiochip2", + "line": 24, + "debounceDelay": 50 + }, + "device.roles.exclusive": true, + "device.roles.activated": [ "enrollee" ], + "device.roles": [ + { + "role": "enrollee", + "interface": "wlan0", + "settingsPath": "/etc/ztpd/config-enrollee.json" + }, + { + "role": "configurator", + "interface": "wlan1", + "settingsPath": "/etc/ztpd/config-configurator.json" + } + ] +} \ No newline at end of file diff --git a/samples/ztpd/config/config.schema.json b/samples/ztpd/config/config.schema.json new file mode 100644 index 0000000..5d41ee6 --- /dev/null +++ b/samples/ztpd/config/config.schema.json @@ -0,0 +1,269 @@ +{ + "$id": "https://developer.microsoft.com/wifi-ztp/ztpd/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema", + "default": {}, + "description": "ztpd configuration.", + "examples": [ + { + "ui.activation": [ + "command", + "gpio" + ], + "ui.activation.unit": "hostapd.service", + "ui.activation.gpio": { + "chip": "gpiochip2", + "line": 24, + "debounceDelay": 50 + }, + "device.roles.exclusive": true, + "device.roles.activated": [ + "enrollee" + ], + "device.roles": [ + { + "role": "enrollee", + "interface": "wlan0", + "settingsPath": "/etc/ztpd/config-enrollee.json" + }, + { + "role": "configurator", + "interface": "wlan1", + "settingsPath": "/etc/ztpd/config-configurator.json" + } + ] + } + ], + "required": [], + "title": "ztp configuration.", + "type": "object", + "properties": { + "ui.activation": { + "$id": "#/properties/ui.activation", + "default": [], + "description": "The user interface activation types to enable.", + "examples": [ + [ + "command", + "gpio" + ] + ], + "title": "User interface activation methods.", + "uniqueItems": true, + "type": "array", + "additionalItems": true, + "items": { + "$id": "#/properties/ui.activation/items", + "enum": [ + "command", + "gpio" + ], + "type": "string", + "anyOf": [ + { + "$id": "#/properties/ui.activation/items/anyOf/0", + "type": "string", + "title": "Enabled user interface activation methods.", + "description": "The type of user interface activation.", + "default": "", + "examples": [ + "command", + "gpio" + ] + } + ] + } + }, + "ui.activation.unit": { + "$id": "#/properties/ui.activation.unit", + "default": "", + "description": "The name of the systemd service unit that represents the user interface for the ztpd daemon. This service unit will be toggled according to the enabled methods set in the ui.activation property.", + "examples": [ + "hostapd.service" + ], + "title": "User interface systemd activation unit.", + "type": "string" + }, + "ui.activation.gpio": { + "$id": "#/properties/ui.activation.gpio", + "default": {}, + "description": "Settings for when ui.activation contains \"gpio\". This configures a gpio to toggle the daemon user interface activation state.", + "examples": [ + { + "chip": "gpiochip2", + "line": 24, + "debounceDelay": 50 + } + ], + "required": [ + "chip", + "line", + "debounceDelay" + ], + "title": "User interface gpio activation settings.", + "type": "object", + "properties": { + "chip": { + "$id": "#/properties/ui.activation.gpio/properties/chip", + "default": "", + "description": "The owning gpio chip of the line to be configured.", + "examples": [ + "gpiochip2" + ], + "title": "GPIO chip.", + "type": "string" + }, + "line": { + "$id": "#/properties/ui.activation.gpio/properties/line", + "default": 0, + "description": "The gpio line (number) used to toggle user interface activation.", + "examples": [ + 24, + "line24" + ], + "title": "GPIO line.", + "type": [ + "integer", + "string" + ] + }, + "debounceDelay": { + "$id": "#/properties/ui.activation.gpio/properties/debounceDelay", + "default": 50, + "description": "The debounce delay in milliseconds (ms) to use on the gpio line when toggling the activation state. Each rising edge of the gpio line will reset the debounce, and each falling edge will ensure at least this duration has elapsed before signaling a change in activation state.", + "examples": [ + 50 + ], + "title": "GPIO line debounce delay (milliseconds)", + "type": "integer" + } + } + }, + "device.roles.exclusive": { + "$id": "#/properties/device.roles.exclusive", + "default": false, + "description": "Indicates whether only a single device role may be active at any one time. When enabled, this has the effect of disabling any device role upon activation a role. For example, if the \"configurator\" role is currently active and the \"enrollee\" role is newly activated, the \"configurator\" role will be de-activated.", + "examples": [ + true + ], + "title": "Device role exclusivity.", + "type": "boolean" + }, + "device.roles.activated": { + "$id": "#/properties/device.roles.activated", + "default": [], + "description": "The currently activated roles.", + "examples": [ + [ + "enrollee" + ] + ], + "title": "Device roles activated.", + "type": "array", + "additionalItems": true, + "items": { + "$id": "#/properties/device.roles.activated/items", + "enum": [ + "enrollee", + "configurator" + ], + "uniqueItems": true, + "type": "string", + "anyOf": [ + { + "$id": "#/properties/device.roles.activated/items/anyOf/0", + "type": "string", + "title": "Device role.", + "description": "The device role name.", + "default": "", + "examples": [ + "enrollee" + ] + } + ] + } + }, + "device.roles": { + "$id": "#/properties/device.roles", + "default": [], + "description": "Device Provisioning Protocol (DPP) role settings.", + "examples": [ + [ + { + "role": "enrollee", + "interface": "wlan0", + "settingsPath": "/etc/ztpd/config-enrollee.json" + }, + { + "role": "configurator", + "interface": "wlan1", + "settingsPath": "/etc/ztpd/config-configurator.json" + } + ] + ], + "title": "Device role settings.", + "type": "array", + "additionalItems": true, + "items": { + "$id": "#/properties/device.roles/items", + "type": "object", + "anyOf": [ + { + "$id": "#/properties/device.roles/items/anyOf/0", + "type": "object", + "title": "Device role settings.", + "description": "The device role settings.", + "default": {}, + "examples": [ + { + "role": "enrollee", + "interface": "wlan0", + "settingsPath": "/etc/ztpd/config-enrollee.json" + } + ], + "required": [ + "role", + "interface", + "settingsPath" + ], + "properties": { + "role": { + "$id": "#/properties/device.roles/items/anyOf/0/properties/role", + "default": "", + "description": "The role name.", + "enum": [ + "enrollee", + "configurator" + ], + "examples": [ + "enrollee" + ], + "title": "Device role name.", + "type": "string" + }, + "interface": { + "$id": "#/properties/device.roles/items/anyOf/0/properties/interface", + "default": "", + "description": "The device interface name associated with the role.", + "examples": [ + "wlan0" + ], + "title": "Device interface name.", + "type": "string" + }, + "settingsPath": { + "$id": "#/properties/device.roles/items/anyOf/0/properties/settingsPath", + "default": "", + "description": "The absolute path to the settings for the role.", + "examples": [ + "/etc/ztpd/config-enrollee.json" + ], + "title": "Device settings file path.", + "type": "string" + } + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/scripts/ztp-keyinfo.sh b/scripts/ztp-keyinfo.sh new file mode 100755 index 0000000..58875b2 --- /dev/null +++ b/scripts/ztp-keyinfo.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# +# Given an input file containing a base-64, DER encoded, ASN.1 +# SubjectPublicKeyInfo ECC public key, outputs the DPP "chirp" hash. The chirp +# hash is the SHA256 hash of the ASCII text "chirp" concatenated with the base64 +# encoding of the key. + +FILE= +DPP_EK_PUB= +DPP_VERSION= +VERBOSE=0 + +while getopts "vf:12" opt; do + case ${opt} in + f) + FILE="${OPTARG}" + ;; + 1) + DPP_VERSION=1 + ;; + 2) + DPP_VERSION=2 + ;; + v) + VERBOSE=1 + ;; + esac +done + +# if file not specified, use first arg, otherwise stdin +if [ -z "${FILE}" ]; then + while read line + do + DPP_EK_PUB+=${line} + done < "${1:-/dev/stdin}" +# otherwise ensure file exists and is regular +elif [ ! -f "${FILE}" ]; then + echo "error: base64 file ${FILE} does not exist" + exit 1 +else + DPP_EK_PUB=$(<${FILE}) +fi + +echo -e "\e[0;38;5;217m> key info\e[0m" + +# ASN.1 SubjectPublicKeyInfo +echo -ne "\e[0;94m dpp uri \e[0m-> " +echo "DPP:V:${DPP_VERSION:=2};K:${DPP_EK_PUB};;" +echo -e "\e[0;94m base64-der \e[0m-> ${DPP_EK_PUB}" + +# Public key sha256 hash +echo -ne "\e[0;94m sha256|plain \e[0m-> " +echo -n "${DPP_EK_PUB}" | base64 -d | sha256sum -b | head -c 64 +echo + +# Public key sha256 hash (chirp) +echo -ne "\e[0;94m sha256|chirp \e[0m-> " +{ echo -n "chirp" ; echo ${DPP_EK_PUB} | base64 -d ; } | sha256sum -b | head -c 64 +echo + +# ASN.1 details +if [ "${VERBOSE}" -ne 0 ]; then + echo -e "\n\e[0;38;5;217m> asn.1 decoding\e[0m" + echo "${DPP_EK_PUB}" | openssl asn1parse -dump +fi diff --git a/scripts/ztp-release.sh b/scripts/ztp-release.sh new file mode 100755 index 0000000..af0fa46 --- /dev/null +++ b/scripts/ztp-release.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +set -euf -o pipefail + +TEMPDIR= +DEBUG= +FEATURE=ztpd +REDIRECT=/dev/null +GIT_REPO_DEFAULT="git@github.com:microsoft/wifi-ztp.git" +RELEASE_BRANCH_SOURCE_DEFAULT="main" + +function usage() { + echo "$0 -v [-b ] [-r ] [-f] [-d]" + echo + echo " -v " + echo " The numerical version, in semver format, of the release." + echo " Eg. 0.3.5" + echo " -b " + echo " The name of the branch from which release source should be obtained." + echo " (default=${RELEASE_BRANCH_SOURCE_DEFAULT})" + echo " -r " + echo " The git repo url to use." + echo " (default=${GIT_REPO_DEFAULT})" + echo " -f" + echo " Determines whether or not release tags should be forcibly pushed." + echo " Note that this will overwrite any existing tags; use with caution." + echo " (default=disabled)" + echo " -d" + echo " Enable script debugging. This enables script output (stdout+stderr)." + echo +} + +function finish() { + if [ ! -z ${TEMPDIR+x} ]; then + rm -rf ${TEMPDIR} >& ${REDIRECT} + fi +} + +function main() { + local RELEASE_VERSION= + local RELEASE_BRANCH_SOURCE=${RELEASE_BRANCH_SOURCE_DEFAULT} + local FORCE= + local GIT_REPO=${GIT_REPO_DEFAULT} + + while getopts ":v:b:r:fd" opt; do + case ${opt} in + v) + RELEASE_VERSION="${OPTARG}" + ;; + b) + RELEASE_BRANCH_SOURCE="${OPTARG}" + ;; + r) + GIT_REPO="${OPTARG}" + ;; + f) + FORCE=" -f" + ;; + d) + DEBUG=1 + REDIRECT=/dev/tty + ;; + *) + ;; + esac + done + + if [ -z ${RELEASE_VERSION+x} ]; then + echo "missing release version" + usage + exit 1 + fi + + local RELEASE_VERSION_TAG=v${RELEASE_VERSION} + local RELEASE_NAME=${FEATURE}-${RELEASE_VERSION} + local RELEASE_DIR_SOURCE=${FEATURE}-release-${RELEASE_VERSION} + local RELEASE_DIR=${RELEASE_NAME} + local RELEASE_ARCHIVE=${RELEASE_NAME}.tar.xz + + TEMPDIR=$(mktemp -d) + pushd ${TEMPDIR} >& ${REDIRECT} + echo "checking out branch '${RELEASE_BRANCH_SOURCE}' from ${GIT_REPO}" + git clone ${GIT_REPO} --branch ${RELEASE_BRANCH_SOURCE} ${RELEASE_DIR_SOURCE} >& ${REDIRECT} + pushd ${RELEASE_DIR_SOURCE} >& ${REDIRECT} + echo "tagging as ${RELEASE_VERSION_TAG}" + git tag ${RELEASE_VERSION_TAG} ${FORCE} >& ${REDIRECT} + git push origin refs/tags/${RELEASE_VERSION_TAG} ${FORCE} >& ${REDIRECT} + + echo "preparing release archive" + git config tar.tar.xz "xz -c" + git archive --format=tar.xz --prefix=${RELEASE_NAME}/ ${RELEASE_VERSION_TAG} > ${RELEASE_ARCHIVE} + echo "created release archive ${RELEASE_ARCHIVE} -> $(sha256sum ${RELEASE_ARCHIVE} | awk '{print $1}')" +} + +main "$@" +finish \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..d4a1cec --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,8 @@ + +add_subdirectory(core) +add_subdirectory(dbus) +add_subdirectory(systemd) +add_subdirectory(utils) +add_subdirectory(wpas) +add_subdirectory(wifi) +add_subdirectory(ztpd) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..61b2527 --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,49 @@ + +project(ztpcore) + +add_library(ztpcore + STATIC + "" +) + +option(CONFIG_EXPLICIT_AUTH_ON_CHIRP "Initiate DPP authentication on first chirp rx" OFF) + +if (CONFIG_EXPLICIT_AUTH_ON_CHIRP) + target_compile_definitions(ztpcore + PRIVATE + CONFIG_EXPLICIT_AUTH_ON_CHIRP=1 + ) +endif(CONFIG_EXPLICIT_AUTH_ON_CHIRP) + +add_subdirectory(biproviders) + +target_sources(ztpcore + PRIVATE + ztp_configurator_config.c + ztp_configurator.c + ztp_enrollee_config.c + ztp_enrollee.c + ztp_settings.c + ztp_wpa_supplicant.c + ztp.c +) + +target_include_directories(ztpcore + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/biproviders + ${CMAKE_CURRENT_SOURCE_DIR}/../dbus + ${CMAKE_CURRENT_SOURCE_DIR}/../utils + ${CMAKE_CURRENT_SOURCE_DIR}/../wpas + ${CMAKE_CURRENT_SOURCE_DIR}/../wifi +) + +target_link_libraries(ztpcore + ${LIBSYSTEMD_TARGET} + ${LIBJSONC_TARGET} + azure_dps_service_client + ztpdbus + ztputils + ztpwpas + ztpwifi +) diff --git a/src/core/biproviders/CMakeLists.txt b/src/core/biproviders/CMakeLists.txt new file mode 100644 index 0000000..956b438 --- /dev/null +++ b/src/core/biproviders/CMakeLists.txt @@ -0,0 +1,18 @@ + +target_sources(ztpcore + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/bootstrap_info_provider.c + ${CMAKE_CURRENT_SOURCE_DIR}/bootstrap_info_provider_settings.c +) + +target_include_directories(ztpcore + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/azuredps + ${CMAKE_CURRENT_SOURCE_DIR}/file + ${CMAKE_CURRENT_SOURCE_DIR}/../../wifi + ${CMAKE_CURRENT_SOURCE_DIR}/../../utils +) + +add_subdirectory(azuredps) +add_subdirectory(file) diff --git a/src/core/biproviders/azuredps/CMakeLists.txt b/src/core/biproviders/azuredps/CMakeLists.txt new file mode 100644 index 0000000..701f359 --- /dev/null +++ b/src/core/biproviders/azuredps/CMakeLists.txt @@ -0,0 +1,78 @@ + +project(azuredps LANGUAGES CXX) + +add_library(azure_dps_service_client + STATIC + "" +) + +if (BUILD_CPPREST_EXTERNAL) + find_package(Threads REQUIRED) + + add_dependencies(azure_dps_service_client + cpprestsdk) + + add_library(cpprestsdk_external SHARED IMPORTED) + set_target_properties(cpprestsdk_external PROPERTIES + IMPORTED_LOCATION ${LIB_CPPREST_EXTERNAL} + ) + + target_link_libraries(azure_dps_service_client + PRIVATE + cpprestsdk_external + Threads::Threads + ) +else() + target_link_libraries(azure_dps_service_client + PRIVATE + cpprestsdk::cpprest + ) +endif() + +target_sources(azure_dps_service_client + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/azure_dps_service_client.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/azure_dps_service_client_proxy.cpp +) + +target_include_directories(azure_dps_service_client + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../../utils + ${CMAKE_CURRENT_SOURCE_DIR}/../../../wifi +) + +target_link_libraries(azure_dps_service_client + PRIVATE + OpenSSL::Crypto + ${Boost_LIBRARIES} +) + +target_sources(ztpcore + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/bootstrap_info_provider_azure_dps.c + ${CMAKE_CURRENT_SOURCE_DIR}/bootstrap_info_provider_azure_dps_config.c +) + +add_executable(sample_cpp + "" +) + +target_include_directories(sample_cpp + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../../utils + ${CMAKE_CURRENT_SOURCE_DIR}/../../../wifi +) + +target_sources(sample_cpp + PRIVATE + sample.cpp +) + +target_link_libraries(sample_cpp + PRIVATE + azure_dps_service_client +) diff --git a/src/core/biproviders/azuredps/azure_dps_service_client.cpp b/src/core/biproviders/azuredps/azure_dps_service_client.cpp new file mode 100644 index 0000000..8dcb382 --- /dev/null +++ b/src/core/biproviders/azuredps/azure_dps_service_client.cpp @@ -0,0 +1,478 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +extern "C" { +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps.h" +} + +#include "azure_dps_service_client.h" +#include "azure_dps_service_client_log.h" +#include "string_utils.hpp" + +using namespace web; +using namespace web::http; +using namespace web::http::client; +using namespace web::http::details; +using namespace web::http::oauth2::experimental; + +const std::string dps_api_version = "2021-06-01"; + +/** + * @brief Free function implementing case-insensitive string comparison. + * + * @param s1 The first string to compare. + * @param s2 The second string to compare. + * @return true If the first and second string contain the same letters in the same order in any case. + * @return false If the first and second strings differ in size or ordering of letters. + */ +static bool +string_iequals(const std::string& s1, const std::string& s2) +{ + return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char c1, char c2) { + return std::tolower(static_cast(c1)) == std::tolower(static_cast(c2)); + }); +} + +/** + * @brief Logs an http request. + * + * @param request The request to log. + */ +static void +log_http_request(const http_request& request) +{ + tlog(request.method() << " " << request.relative_uri().to_string() << " " << request.http_version().to_utf8string()); +} + +/** + * @brief Constructs an http request object for querying an azure dps instance + * for individual device records. + * + * @return http_request + */ +static http_request +build_device_records_query_request(const std::string& SAS_token) +{ + http::uri_builder encoded_uri("/enrollments/query"); + encoded_uri.append_query("api-version", dps_api_version); + + http_request request(methods::POST); + request.set_request_uri(encoded_uri.to_uri()); + request.headers().add(header_names::content_type, mime_types::application_json); + request.headers().add(header_names::authorization, SAS_token); + request.set_body("{\"query\":\"*\"}"); + + return request; +} + +/** + * @brief Indicates whether SAS-token based authentication is used. + * + * @return true If SAS-token based authentication is being used. + * @return false If SAS-token based authentication is not being used. + */ +bool +azure_dps_service_client::using_sas_token() const +{ + return bool(m_connection_string); +} + +/** + * @brief Construct a new azure dps service client::azure dps service client object. + * + * @param settings The settings to use to instantiate the object. + */ +azure_dps_service_client::azure_dps_service_client(const struct bootstrap_info_provider_azure_dps_settings* settings) : + m_dps_url(settings->service_endpoint_uri) +{ + if (strlen(settings->connection_string)) + m_connection_string = settings->connection_string; + else { + m_config.set_oauth2(oauth2_config( + settings->client_id, // client key + settings->client_secret, // client secret + U(""), // auth_endpoint + settings->authority_url, // token_endpoint + U(""), // redirect_url + settings->resource_uri)); // scope + } +} + +/** + * @brief Parses the connection string and writes the parsed values to hostname, + * shared_access_key_name, shared_access_key. + * + * @param connection_string The connection string to parse. + * @param hostname The hostname of the target system. + * @param shared_access_key_name The name of the shared access key to use. + * @param shared_access_key The shared access key value. + */ +static int +parse_connection_string(const std::string& connection_string, std::string& hostname, std::string& shared_access_key_name, std::string& shared_access_key) +{ + std::string key; + std::stringstream tokens(connection_string); + std::unordered_map results; + + static const std::array keynames = { + "HostName", + "SharedAccessKeyName", + "SharedAccessKey", + }; + + std::string token; + + for (const auto& keyname : keynames) { + if (std::getline(tokens, token, '=')) { + key = trim_copy(token); + if (key == keyname) { + std::getline(tokens, token, ';'); + results[key] = trim_copy(token); + } else + return -1; + } else + return -1; + } + + try { + hostname = results[keynames[0]]; + shared_access_key_name = results[keynames[1]]; + shared_access_key = results[keynames[2]]; + } catch (...) { + return -1; + } + + return 0; +} + +/** + * @brief Constructs a SAS token given a connection string, and saves it to 'sas_token'. + * + * @param connection_string The connection string. + * @param sas_token The destination to write the sas token to. + * @param expiry_seconds The amount of time in seconds the SAS token will be valid [default=90]. + * + * @return int 0 If the connection string can be properly parsed and the + * SAS token formed. Otherwise, -1 is returned. + */ +static int +gen_sas_token(const std::string& connection_string, std::string& sas_token, int expiry_seconds = 90) +{ + std::string hostname; + std::string sharedaccesskeyname; + std::string sharedaccesskey; + + int ret = parse_connection_string(connection_string, hostname, sharedaccesskeyname, sharedaccesskey); + if (ret < 0) { + tlog("connection string is invalid, could not form sas token"); + return -1; + } + + time_t expiry_time = time(0) + ((time_t)expiry_seconds); + + std::string hostname_url = web::uri::encode_data_string(hostname); + std::string string_to_sign = hostname_url + "\n" + std::to_string(expiry_time); + + //EVP_DecodeBlock outputs 3 bytes for every 4 in the input, rounded up + std::size_t decodedkey_len = ((sharedaccesskey.length() + 3) / 4) * 3; + std::vector decodedkey_buffer(decodedkey_len + 1); + + ret = EVP_DecodeBlock(decodedkey_buffer.data(), + (const unsigned char*)sharedaccesskey.c_str(), + (int)sharedaccesskey.length()); + if (ret == -1) + return ret; + + unsigned int digest_len = 0; + unsigned char* digest = HMAC(EVP_sha256(), + decodedkey_buffer.data(), + (int)decodedkey_len, + (const unsigned char*)string_to_sign.c_str(), + string_to_sign.length(), + 0, + &digest_len); + + //EVP_EncodeBlock outputs 4 bytes for every 3 in the input, rounded up + std::vector::size_type signature_len = ((digest_len + 2) / 3) * 4; + std::vector signature(signature_len + 1); + + ret = EVP_EncodeBlock(signature.data(), + (const unsigned char*)digest, + static_cast(digest_len)); + if (ret == -1) + return ret; + + std::string signature_url_encoded = + web::uri::encode_data_string(std::string(signature.begin(), signature.end() - 1)); + + std::string sas_string = std::string("sr=") + hostname_url + + std::string("&sig=") + signature_url_encoded + + std::string("&se=") + std::to_string(expiry_time) + + std::string("&skn=") + sharedaccesskeyname; + + sas_token = std::string("SharedAccessSignature ") + sas_string; + + return ret; +} + +/** + * @brief Retrieve an oauth2 token for use with the DPS service. + * + * @return int 0 if a valid oauth2 token was obtained and stored in the http + * client configuration member object. Otherwise, a negtative error code is + * returned. + */ +int +azure_dps_service_client::authorize(void) +{ + static constexpr std::size_t auth_check_timeout_ms = 3'000; + + int ret; + try { + m_config.set_timeout(std::chrono::milliseconds(auth_check_timeout_ms)); + m_config.oauth2()->token_from_client_credentials().get(); + tlog("acquired dps oauth2 token, expires in " << m_config.oauth2()->token().expires_in() << " seconds"); + ret = 0; + } catch (const pplx::task_canceled&) { + tlog("timed out waiting for dps authentication to complete (" << auth_check_timeout_ms << " ms)"); + ret = -ETIMEDOUT; + } catch (const std::exception& e) { + tlog("exception occurred while acquiring oauth2 token :("); + tlog(e.what()); + ret = -EINTR; + } + + if (!m_config.oauth2()->token().is_valid_access_token()) { + tlog("oauth2 token invalid! oauth2_config->state() = " << m_config.oauth2()->state()); + if (ret == 0) + ret = -EPERM; + } else + tlog("The token is valid " << m_config.oauth2()->token().access_token()); + + return ret; +} + +/** + * @brief Synchronizes the local view of device records with the remote view. + * This will first get a new sas token. Then this will download all new records from the dps instance and save them + * locally. + * + * @return int 0 if the local view was successfully synchronized, non-zero + * otherwise. + */ +int +azure_dps_service_client::synchronize_dps_bi(void) +{ + static constexpr std::size_t http_request_timeout_ms = 5'000; + + if (using_sas_token()) { + int ret = gen_sas_token(*m_connection_string, m_sas_token); + if (ret < 0) { + tlog("something went wrong in creating sas token"); + return ret; + } + } else if (!m_config.oauth2()->token().is_valid_access_token()) { + int ret = authorize(); + if (ret < 0) { + tlog("failed to renew expired dps oauth2 token (%d)" << ret); + return ret; + } + } + + m_config.set_timeout(std::chrono::milliseconds(http_request_timeout_ms)); + http_client api(m_dps_url, m_config); + + auto device_bootstrapping_info_old = std::move(m_device_bootstrapping_info); + m_device_bootstrapping_info.clear(); + + json::value device_records_json; + + { + http_request request = build_device_records_query_request(m_sas_token); + pplx::task task_request = api.request(request); + log_http_request(request); + + try { + http_response response = task_request.get(); + device_records_json = response.extract_json().get(); + } catch (const pplx::task_canceled&) { + tlog("timed out waiting for dps device group query to complete (" << http_request_timeout_ms << " ms)"); + return -ETIMEDOUT; + } catch (const std::exception& e) { + tlog("exception caught in device group query request task"); + tlog(e.what()); + return -EINTR; + } + } + + std::vector device_bi_list = extract_device_bootstrapping_info(device_records_json); + + m_device_bootstrapping_info.insert(m_device_bootstrapping_info.end(), + std::make_move_iterator(std::begin(device_bi_list)), + std::make_move_iterator(std::end(device_bi_list))); + + int32_t changed = int32_t(m_device_bootstrapping_info.size()) - int32_t(device_bootstrapping_info_old.size()); + if (changed != 0) + tlog(std::showpos << changed << " records"); + return 0; +} + +/** + * @brief Finds the DPP URI matching the specified chirp hash. + * + * @param chirp_hash The chirp hash of the public bootstrapping key. + * @param matching_dpp_uri Output argument to hold the DPP URI matching the chirp hash. + * @return int 0 if a matching device bootstrap information device record has + * a public bootstrapping key whose chirp hash matches. Otherwise -1 is + * returned. + */ +int +azure_dps_service_client::lookup_dpp_uri(const std::string& chirp_hash, std::string& matching_dpp_uri) const +{ + const auto match = std::find_if(m_device_bootstrapping_info.cbegin(), m_device_bootstrapping_info.cend(), [&](const device_bi& bi_instance) { + return string_iequals(bi_instance.chirp_hash, chirp_hash); + }); + + if (match != m_device_bootstrapping_info.cend()) { + matching_dpp_uri = match->dpp_uri; + return 0; + } + + return -1; +} + +/** + * @brief Calculates the "chirp" hash of a DPP bootstrapping key. + * + * @param dpp_bootstrapping_key_base64 The encoded public key derived from the DPP + * URI. Specifically, the string must describe the DER of an ASN.1 + * SubjectPublicKeyInfo, encoded as base64. See section 4.1 'Public Keys' of + * the Wi-Fi Alliance Device Provisioning Protocol (DPP) Specification for more + * details. + * + * @return std::vector + */ +static std::vector +calculate_chirp_hash(const std::string& dpp_bootstrapping_key_base64) +{ + static const char chirp_prefix[] = "chirp"; + static constexpr std::size_t dpp_bootstrapping_key_base64_max = 4096; + + std::vector chirp_hash(SHA256_DIGEST_LENGTH); + uint8_t dpp_bootstrap_key_pub[dpp_bootstrapping_key_base64_max]; + const uint8_t* dpp_bootstrap_key_buffer = reinterpret_cast(dpp_bootstrapping_key_base64.c_str()); + int dpp_bootstrap_key_pub_length = EVP_DecodeBlock(dpp_bootstrap_key_pub, dpp_bootstrap_key_buffer, (int)dpp_bootstrapping_key_base64.length()); + if (dpp_bootstrap_key_pub_length < 0) { + tlog("failed to base64-decode dpp public key"); + return chirp_hash; + } + + EVP_MD_CTX* chirp_hash_context; + chirp_hash_context = EVP_MD_CTX_new(); + if (chirp_hash_context == nullptr) { + tlog("failed to allocate openssl sha256 evp hash context"); + return chirp_hash; + } + + unsigned chirp_hash_length; + EVP_DigestInit(chirp_hash_context, EVP_sha256()); + EVP_DigestUpdate(chirp_hash_context, chirp_prefix, sizeof chirp_prefix - 1); + EVP_DigestUpdate(chirp_hash_context, dpp_bootstrap_key_pub, static_cast(dpp_bootstrap_key_pub_length) - 1); + EVP_DigestFinal(chirp_hash_context, chirp_hash.data(), &chirp_hash_length); + + if (chirp_hash.size() != chirp_hash_length) + chirp_hash.resize(chirp_hash_length); + + return chirp_hash; +} + +/** + * @brief Extracts the public bootstrapping key encoding from the DPP URI. Note + * that the extracted key is a DER ASN.1 SubjectPublicKeyInfo object encoded as + * base64. + * + * @param dpp_uri The DPP URI to parse. + * @return std::string A string containing the public key, if one was found and + * valid. Otherwise and empty string is returned. + */ +static std::string +extract_bootstrapping_key_from_dpp_uri(const std::string& dpp_uri) +{ + std::size_t start = dpp_uri.find("K:"); + if (start != std::string::npos) { + start += 2; + std::size_t end = dpp_uri.find(';', start); + if (end != std::string::npos) + return dpp_uri.substr(start, end - start); + } + + return ""; +} + +/** + * @brief Convert a buffer to its hex string representation. + * + * @param buffer The buffer to convert. + * @return std::string A string containing the hex representation of the buffer. + */ +static std::string +buffer_to_hex_string(const std::vector& buffer) +{ + std::stringstream output; + output << std::hex << std::setfill('0'); + + for (const auto& byte_value : buffer) { + output << std::setw(2) << static_cast(byte_value); + } + + return output.str(); +} + +/** + * @brief Extracts device bootstrapping information from the device record json + * content returned from a dps device record query. + * + * @param device_records_json The json returned from a dps device record query. + * @return std::vector + */ +std::vector +azure_dps_service_client::extract_device_bootstrapping_info(json::value device_records_json) +{ + auto& device_records = device_records_json.as_array(); + std::vector extracted_device_bootstrapping_info; + + for (json::value& device_record : device_records) { + try { + json::array& interfaces_array = device_record.as_object()["optionalDeviceInformation"]["ZeroTouchProvisioning"]["WiFi"]["Interfaces"].as_array(); + const json::value& dpp_uri = interfaces_array[0].as_object()["DppUri"]; + + const auto& dpp_uri_string = dpp_uri.as_string(); + const auto dpp_bootstrap_key = extract_bootstrapping_key_from_dpp_uri(dpp_uri_string); + const auto dpp_chirp = calculate_chirp_hash(dpp_bootstrap_key); + const auto dpp_chirp_string = buffer_to_hex_string(dpp_chirp); + + tlog(" " << dpp_chirp_string.substr(0, 7) << " -> " << dpp_uri_string); + + extracted_device_bootstrapping_info.push_back({ dpp_chirp_string, dpp_uri_string }); + } catch (const std::exception& e) { + tlog("Invalid device_record object!"); + tlog(e.what()); + } + } + + return extracted_device_bootstrapping_info; +} diff --git a/src/core/biproviders/azuredps/azure_dps_service_client.h b/src/core/biproviders/azuredps/azure_dps_service_client.h new file mode 100644 index 0000000..039d91c --- /dev/null +++ b/src/core/biproviders/azuredps/azure_dps_service_client.h @@ -0,0 +1,105 @@ + +#ifndef __AZURE_DPS_SERVICE_CLIENT_H__ +#define __AZURE_DPS_SERVICE_CLIENT_H__ + +#include +#include +#include +#include + +#include +#include +#include + +/** + * @brief Azure DPS service client object. Provides functionality for + * interacting with the Azure DPS service, wrapping use of its REST API into a + * C++ class. + */ +class azure_dps_service_client +{ +public: + /** + * @brief Construct a new azure dps service client::azure dps service client object. + * + * @param settings The settings to use to instantiate the object. + */ + azure_dps_service_client(const struct bootstrap_info_provider_azure_dps_settings* settings); + ~azure_dps_service_client() = default; + + /** + * @brief Retrieve an oauth2 token for use with the DPS service. + * + * @return int 0 if a valid oauth2 token was obtained and stored in the http + * client configuration member object. Otherwise, a negtative error code is + * returned. + */ + int + authorize(void); + + /** + * @brief Synchronizes the local view of device records with the remote view. + * This will download all new records from the dps instance and save them + * locally. + * + * @return int 0 if the local view was successfully synchronized, non-zero otherwise. + */ + int + synchronize_dps_bi(void); + + /** + * @brief Finds the DPP URI matching the specified chirp hash. + * + * @param chirp_hash The chirp hash of the public bootstrapping key. + * @param matching_dpp_uri Output argument to hold the DPP URI matching the chirp hash. + * @return int 0 if a matching device bootstrap information device records has + * a public bootstrapping key whose chirp hash matches. Otherwise -1 is + * returned. + */ + int + lookup_dpp_uri(const std::string& chirp_hash, std::string& matching_dpp_uri) const; + + /** + * @brief Indicates whether SAS-token based authentication is used. + * + * @return true If SAS-token based authentication is being used. + * @return false If SAS-token based authentication is not being used. + */ + bool + using_sas_token() const; + +private: + std::string m_dps_url; + web::http::client::http_client_config m_config; + std::string m_sas_token; + std::optional m_connection_string; + + struct device_bi { + std::string chirp_hash; + std::string dpp_uri; + }; + + std::vector m_device_bootstrapping_info; + +protected: + /** + * @brief Extracts dps device group names from the json result obtained from a device group query. + * + * @param json_body The json contents returned from a dps device group query. + * @return std::vector + */ + static std::vector + extract_device_records(web::json::value json_body); + + /** + * @brief Extracts device bootstrapping information from the device record json + * content returned from a dps device record query. + * + * @param device_records_json The json returned from a dps device record query. + * @return std::vector + */ + static std::vector + extract_device_bootstrapping_info(web::json::value device_records_json); +}; + +#endif //__AZURE_DPS_SERVICE_CLIENT_H__ diff --git a/src/core/biproviders/azuredps/azure_dps_service_client_log.h b/src/core/biproviders/azuredps/azure_dps_service_client_log.h new file mode 100644 index 0000000..c544b68 --- /dev/null +++ b/src/core/biproviders/azuredps/azure_dps_service_client_log.h @@ -0,0 +1,11 @@ + +#ifndef __AZURE_DPS_SERVICE_CLIENT_LOG_H__ +#define __AZURE_DPS_SERVICE_CLIENT_LOG_H__ + +#include + +#include "ztp_log.h" + +#define tlog(x) std::cout << SYSTEMD_LOG_PRIORITY_DEBUG << x << std::endl + +#endif //__AZURE_DPS_SERVICE_CLIENT_LOG_H__ \ No newline at end of file diff --git a/src/core/biproviders/azuredps/azure_dps_service_client_proxy.cpp b/src/core/biproviders/azuredps/azure_dps_service_client_proxy.cpp new file mode 100644 index 0000000..04887f2 --- /dev/null +++ b/src/core/biproviders/azuredps/azure_dps_service_client_proxy.cpp @@ -0,0 +1,161 @@ + +#include +#include + +extern "C" { +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps.h" +} + +#include "azure_dps_service_client.h" +#include "azure_dps_service_client_log.h" +#include "azure_dps_service_client_proxy.h" + +/** + * @brief Cookies value used to check the sanity of a bare (void*) pointer that + * is being resolved to an instance pointer. + */ +#define AZURE_DPS_CLIENT_CONTEXT_COOKIE (0xAA55AA55) + +/** + * @brief Tracks context information associated with an azure dps service + * client. This structure also contains information needed to marshal data + * between the ztp bootstrap information provider (BIP) C interface and the + * service client C++ implementation. + */ +struct azure_dps_client_context { + const uint32_t cookie = AZURE_DPS_CLIENT_CONTEXT_COOKIE; + std::unique_ptr instance = nullptr; + + /** + * @brief Resolves a bare (void*) pointer to an azure_dps_client_context + * instance. If the specified bare pointer is invalid, nullptr is returned. + * Otherwise a properly typed pointer to the instance is returned. + * + * @param client_context + * @return struct azure_dps_client_context* + */ + static struct azure_dps_client_context * + resolve(void *client_context) + { + auto *context = reinterpret_cast(client_context); + if (!context || context->cookie != AZURE_DPS_CLIENT_CONTEXT_COOKIE) + return nullptr; + + return context; + } +}; + +/** + * @brief Initializes a new azure dps service client. + * + * @param settings The bootstrap info provider settings to use. + * @param client_context A pointer where arbitrary context information may be + * stored. + * @return int 0 if the client was successfully initialized, non-zero otherwise. + */ +int +azure_dps_client_initialize(const struct bootstrap_info_provider_azure_dps_settings *settings, void **client_context) +{ + auto context = std::make_unique(); + if (!context) + return -ENOMEM; + + context->instance = std::make_unique(settings); + + *client_context = context.release(); + + return 0; +} + +/** + * @brief Uninitializes an existing azure dps service client. + * + * @param client_context A pointer to the azure_dps_client_context instance + * created in azure_dps_client_initialize. + */ +void +azure_dps_client_uninitialize(void *client_context) +{ + auto *context = azure_dps_client_context::resolve(client_context); + if (!context) + return; + + std::unique_ptr(context).reset(nullptr); +} + +/** + * @brief Synchronizes the view of the bootstrap information between dps and ztp. + * + * @param client_context A pointer to the azure_dps_client_context instance + * created in azure_dps_client_initialize. + * @return int 0 if the in-memory contents are in sync with the DPS instance. + * Non-zero otherwise. + */ +int +azure_dps_client_synchronize(void *client_context) +{ + auto *context = azure_dps_client_context::resolve(client_context); + if (!context) + return -EBADF; + + int ret = context->instance->synchronize_dps_bi(); + if (ret < 0) + return ret; + + return 0; +} + +/** + * @brief Queries the provider for bootstrap info record(s). + * + * @param client_context A pointer to the azure_dps_client_context instance + * created in azure_dps_client_initialize. + * @param query The query describing which records are requested. + * @param result The structure to store record results. + * @return int 0 if the query was successful and results provider in 'result', + * non-zero otherwise. + */ +int +azure_dps_client_query(void *client_context, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result) +{ + auto *context = azure_dps_client_context::resolve(client_context); + if (!context) + return -EBADF; + + std::string chirp_hash = query->criterion.data.pubkey_hash.hexstr; + std::string matched_dpp_uri; + + int ret = context->instance->lookup_dpp_uri(chirp_hash, matched_dpp_uri); + if (ret < 0) + return -ENOENT; + + ret = bootstrap_info_query_result_add(result, matched_dpp_uri.c_str()); + if (ret < 0) + return ret; + + return 0; +} + +/** + * @brief Performs authorization to the dps instance, refreshing its oauth2 + * token if necessary. + * + * @param client_context A pointer to the azure_dps_client_context instance + * created in azure_dps_client_initialize. + * @return int 0 if the provider was authorized to use the DPS instance, + * non-zero otherwise. + */ +int +azure_dps_client_authorize(void *client_context) +{ + auto *context = azure_dps_client_context::resolve(client_context); + if (!context) + return -EBADF; + + int ret = context->instance->authorize(); + if (ret < 0) + return ret; + + return 0; +} diff --git a/src/core/biproviders/azuredps/azure_dps_service_client_proxy.h b/src/core/biproviders/azuredps/azure_dps_service_client_proxy.h new file mode 100644 index 0000000..8437948 --- /dev/null +++ b/src/core/biproviders/azuredps/azure_dps_service_client_proxy.h @@ -0,0 +1,73 @@ + +#ifndef __BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_CLIENT_PROXY_H__ +#define __BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_CLIENT_PROXY_H__ + +struct bootstrap_info_provider_azure_dps_settings; +struct bootstrap_info_query; +struct bootstrap_info_query_result; + +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +/** + * @brief Initializes a new azure dps service client. + * + * @param settings The bootstrap info provider settings to use. + * @param client_context A pointer where arbitrary context information may be + * stored. + * @return int 0 if the client was successfully initialized, non-zero otherwise. + */ +int +azure_dps_client_initialize(const struct bootstrap_info_provider_azure_dps_settings *settings, void **client_context); + +/** + * @brief Uninitializes an existing azure dps service client. + * + * @param client_context A pointer to the azure_dps_client_context instance + * created in azure_dps_client_initialize. + */ +void +azure_dps_client_uninitialize(void *client_context); + +/** + * @brief Synchronizes the view of the bootstrap information between dps and ztp. + * + * @param client_context A pointer to the azure_dps_client_context instance + * created in azure_dps_client_initialize. + * @return int 0 if the in-memory contents are in sync with the DPS instance. + * Non-zero otherwise. + */ +int +azure_dps_client_synchronize(void *context); + +/** + * @brief Queries the provider for bootstrap info record(s). + * + * @param client_context A pointer to the azure_dps_client_context instance + * created in azure_dps_client_initialize. + * @param query The query describing which records are requested. + * @param result The structure to store record results. + * @return int 0 if the query was successful and results provider in 'result', + * non-zero otherwise. + */ +int +azure_dps_client_query(void *client_context, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result); + +/** + * @brief Performs authorization to the dps instance, refreshing its oauth2 + * token if necessary. + * + * @param client_context A pointer to the azure_dps_client_context instance + * created in azure_dps_client_initialize. + * @return int 0 if the provider was authorized to use the DPS instance, + * non-zero otherwise. + */ +int +azure_dps_client_authorize(void *client_context); + +#ifdef __cplusplus +} +#endif //__cplusplus + +#endif // __BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_CLIENT_PROXY_H__ diff --git a/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps.c b/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps.c new file mode 100644 index 0000000..dc4c0f3 --- /dev/null +++ b/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps.c @@ -0,0 +1,250 @@ + +#include +#include +#include + +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps.h" +#include "bootstrap_info_provider_azure_dps_config.h" +#include "bootstrap_info_provider_settings.h" +#include "ztp_log.h" + +#include "azure_dps_service_client_proxy.h" + +/** + * @brief Initialize a new provider instance. This will parse the settings, + * associate them with the instance and prepare the provider for use. + * + * @param instance The instance to initialize. It is assumed that object describes an uninitialized provider. + * @param json_settings The json-encoded settings for the provider. + * @return int 0 if the provider was successfully initialized, non-zero otherwise. + */ +static int +bootstrap_info_provider_azure_dps_initialize(struct bootstrap_info_provider_azure_dps_instance *instance, struct bootstrap_info_provider_azure_dps_settings *settings) +{ + instance->settings = settings; + + int ret = azure_dps_client_initialize(instance->settings, &instance->client_context); + if (ret < 0) { + zlog_error("azure dps client failed to initialize (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Creates and initializes a new instance of a azure dps-based bootstrap info provider. + * + * @param out The output pointer to write the new instance to. + * @param settings The settings to use for this instance. + * @return int 0 if the instance was created, *instance will hold the newly + * created object. Otherwise a non-zero value will be returned, with *instance + * having the value NULL. + */ +static int +bootstrap_info_provider_azure_dps_create(struct bootstrap_info_provider_azure_dps_instance **out, struct bootstrap_info_provider_azure_dps_settings *settings) +{ + *out = NULL; + + struct bootstrap_info_provider_azure_dps_instance *instance = calloc(1, sizeof *instance); + if (!instance) { + zlog_error("allocation failure creating azure dps-based bootstrap info provider"); + return -ENOMEM; + } + + int ret = bootstrap_info_provider_azure_dps_initialize(instance, settings); + if (ret < 0) { + free(instance); + return ret; + } + + *out = instance; + + return 0; +} + +/** + * @brief Uninitializes a provider. + * + * @param instance The instance to uninitialize. + */ +static void +bootstrap_info_provider_azure_dps_uninitialize(struct bootstrap_info_provider_azure_dps_instance *instance) +{ + __unused(instance); + // nothing to do +} + +/** + * @brief Uninitializes and destroys a provider. All owned resources will be + * freed and the memory associated with the provider itself is released. The + * 'instance' pointer must not be used beyond successfuly completion of this + * call. + * + * @param instance The instance to destroy. + */ +static void +bootstrap_info_provider_azure_dps_destroy(struct bootstrap_info_provider_azure_dps_instance *instance) +{ + bootstrap_info_provider_azure_dps_uninitialize(instance); + free(instance); +} + +/** + * @brief Wrapper to synchronize the DPS instance with the in-memory contents. + * + * Note that this is a 1-way synchronization only, from DPS to in-memory. + * + * @param instance The instance object. + * @return int 0 if the in-memory contents are in sync with the DPS instance. + * Non-zero otherwise. + */ +static int +bootstrap_info_provider_azure_dps_synchronize(struct bootstrap_info_provider_azure_dps_instance *instance) +{ + int ret = azure_dps_client_synchronize(instance->client_context); + if (ret < 0) { + zlog_error("azure dps client failed to synchronize (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Performs a query for bootstrapping information. + * + * @param instance The provider instance. + * @param query The query object describing the criteria to search for. + * @param result The result structure to add results to. + * @return int 0 if the query was performed successfully, non-zero otherwise. + * Note 0 is returned when no records matched; the return value indicates if + * the query was performed, not whether a record matched. + */ +static int +bootstrap_info_provider_azure_dps_query(struct bootstrap_info_provider_azure_dps_instance *instance, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result) +{ + assert(query->criterion.type == BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH); + + int ret = azure_dps_client_query(instance->client_context, query, result); + if (ret < 0) { + zlog_error("azure dps client failed to execute query (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Performs authorization against the configured DPS endpoint. + * + * @param instance The provider instance. + * @return int 0 if the provider was authorized to use the DPS instance, + * non-zero otherwise. + */ +int +bootstrap_info_provider_azure_dps_authorize(struct bootstrap_info_provider_azure_dps_instance *instance) +{ + int ret = azure_dps_client_authorize(instance->client_context); + if (ret < 0) { + zlog_debug("azure dps client failed authorization (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Initializes the provider for use. + * + * @param settings The bootstrap info provider settings to use. + * @param context A pointer where the provider can store arbitrary context. + * @return int 0 if the provider initialized successfully, non-zero otherwise. + */ +static int +bootstrap_info_provider_azure_dps_op_initialize(const struct bootstrap_info_provider_settings *settings, void **context) +{ + struct bootstrap_info_provider_azure_dps_instance *instance; + int ret = bootstrap_info_provider_azure_dps_create(&instance, settings->dps); + if (ret < 0) { + zlog_error("failed to create azure dps-based bootstrap provider instance (%d)", ret); + return ret; + } + + *context = instance; + return 0; +} + +/** + * @brief Uninitializes a provider. Frees all resources associated with the + * instance. + */ +void +bootstrap_info_provider_azure_dps_op_uninitialize(void *context) +{ + struct bootstrap_info_provider_azure_dps_instance *instance = (struct bootstrap_info_provider_azure_dps_instance *)context; + if (!instance) + return; + + bootstrap_info_provider_azure_dps_destroy(instance); +} + +/** + * @brief Synchronizes the provider's view of bootstrap information with its + * backing source, in this case, the configured DPS instance. + * + * @return int 0 if synchronization was successful, non-zero otherwise. + */ +static int +bootstrap_info_provider_azure_dps_op_synchronize(void *context, const struct bootstrap_info_sync_options *options) +{ + __unused(options); + + struct bootstrap_info_provider_azure_dps_instance *instance = (struct bootstrap_info_provider_azure_dps_instance *)context; + if (!instance) + return -EBADF; + + int ret = bootstrap_info_provider_azure_dps_synchronize(instance); + if (ret < 0) { + zlog_debug("failed to synchronize azure dps-based bootstrap info provider (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Queries the provider for bootstrap info record(s). + * + * @param query The query describing which records are requested. + * @param result The structure to store record results. + * @return int 0 if the query was successful and results provider in 'result', + * non-zero otherwise. + */ +static int +bootstrap_info_provider_azure_dps_op_query(void *context, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result) +{ + struct bootstrap_info_provider_azure_dps_instance *instance = (struct bootstrap_info_provider_azure_dps_instance *)context; + if (!instance) + return -EBADF; + + int ret = bootstrap_info_provider_azure_dps_query(instance, query, result); + if (ret < 0) { + zlog_debug("failed to query azure dps-based bootstrap info provider (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Azure DPS provider operation vector. This is provided to ztpd for + * each installed instance of the Azure DPS provider. + */ +struct bootstrap_info_provider_ops bootstrap_info_provider_azure_dps_ops = { + .initialize = bootstrap_info_provider_azure_dps_op_initialize, + .uninitialize = bootstrap_info_provider_azure_dps_op_uninitialize, + .synchronize = bootstrap_info_provider_azure_dps_op_synchronize, + .query = bootstrap_info_provider_azure_dps_op_query, +}; diff --git a/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps.h b/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps.h new file mode 100644 index 0000000..664beb6 --- /dev/null +++ b/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps.h @@ -0,0 +1,32 @@ + +#ifndef __BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_H__ +#define __BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_H__ + +struct bootstrap_info_provider_ops; + +#include "bootstrap_info_provider_azure_dps_config.h" + +/** + * @brief Represents an instance of a Azure DPS-based info provider. + */ +struct bootstrap_info_provider_azure_dps_instance { + struct bootstrap_info_provider_azure_dps_settings *settings; + void *client_context; +}; + +/** + * @brief Operations vector for Azure DPS-based bootstrap info provider. + */ +extern struct bootstrap_info_provider_ops bootstrap_info_provider_azure_dps_ops; + +/** + * @brief Requests the provider to authorize itself to the DPS instance. + * + * @param instance The azure dps instance to perform authorization for. + * @return int 0 if authorization was successful, -EPERM if access was denied, + * non-zero otherwise. + */ +int +bootstrap_info_provider_azure_dps_authorize(struct bootstrap_info_provider_azure_dps_instance *instance); + +#endif //__BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_H__ diff --git a/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps_config.c b/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps_config.c new file mode 100644 index 0000000..ccd03f0 --- /dev/null +++ b/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps_config.c @@ -0,0 +1,207 @@ + +#include +#include +#include +#include + +#include "bootstrap_info_provider_azure_dps.h" +#include "bootstrap_info_provider_azure_dps_config.h" +#include "json_parse.h" +#include "ztp_log.h" + +/** + * @brief Get the context for the string properties of the settings object and its children. + * + * @param context The parent context. Must be of type struct bootstrap_info_provider_azure_dps_settings. + * @param name The name of the child property to retrieve the context for. + * @return void* The context for the child property with key 'name'. + */ +static void * +bip_azure_dps_get_settings_string_context(void *context, const char *name) +{ + struct bootstrap_info_provider_azure_dps_settings *settings = (struct bootstrap_info_provider_azure_dps_settings *)context; + + if (strcmp(name, JSON_PROPERTY_NAME_SERVICE_ENDPOINT_URI) == 0) { + return &settings->service_endpoint_uri; + } else if (strcmp(name, JSON_PROPERTY_NAME_AUTHORITY_URL) == 0) { + return &settings->authority_url; + } else if (strcmp(name, JSON_PROPERTY_NAME_CLIENT_ID) == 0) { + return &settings->client_id; + } else if (strcmp(name, JSON_PROPERTY_NAME_CLIENT_SECRET) == 0) { + return &settings->client_secret; + } else if (strcmp(name, JSON_PROPERTY_NAME_RESOURCE_URI) == 0) { + return &settings->resource_uri; + } else if (strcmp(name, JSON_PROPERTY_NAME_CONNECTION_STRING) == 0) { + return &settings->connection_string; + } else { + return NULL; + } +} + +/** + * @brief "authentication" configuration options. + */ +static struct json_property_parser bip_azure_dps_configuration_authentication_properties[] = { + { + .name = JSON_PROPERTY_NAME_AUTHORITY_URL, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_azure_dps_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_CLIENT_ID, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_azure_dps_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_CLIENT_SECRET, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_azure_dps_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_RESOURCE_URI, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_azure_dps_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_CONNECTION_STRING, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_azure_dps_get_settings_string_context, + }, +}; + +/** + * @brief Parser for "authentication" property. + * + * @param parent The parent object. + * @param name Name of the json key. Must be "authentication". + * @param jobj The json object value. + * @param context The provider settings object. Must be of type struct + * bootstrap_info_provider_azure_dps_settings. + */ +static void +json_parse_authentication(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_parse_object_s(jobj, bip_azure_dps_configuration_authentication_properties, context); +} + +/** + * @brief Top-level configuration options. + */ +static struct json_property_parser bip_azure_dps_configuration_properties[] = { + { + .name = JSON_PROPERTY_NAME_SERVICE_ENDPOINT_URI, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_azure_dps_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_AUTHENTICATION, + .type = json_type_object, + .value = { + json_parse_authentication, + }, + }, +}; + +/** + * @brief Parses json configuration, writing it to the 'settings' object. + * + * @param jobj The json-c object with the configuration. + * @param settings The object to populate with settings. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +bootstrap_info_provider_azure_dps_config_parse_jobj(struct json_object *jobj, struct bootstrap_info_provider_azure_dps_settings *settings) +{ + json_parse_object_s(jobj, bip_azure_dps_configuration_properties, settings); + + // check for all required properties + if (!settings->service_endpoint_uri) { + zlog_error("missing required property '" JSON_PROPERTY_NAME_SERVICE_ENDPOINT_URI "'"); + return -ENOENT; + } else if (!settings->authority_url) { + zlog_error("missing required property '" JSON_PROPERTY_NAME_AUTHORITY_URL "'"); + return -ENOENT; + } else if (!settings->client_id) { + zlog_error("missing required property '" JSON_PROPERTY_NAME_CLIENT_ID "'"); + return -ENOENT; + } else if (!settings->client_secret) { + zlog_error("missing required property '" JSON_PROPERTY_NAME_CLIENT_SECRET "'"); + return -ENOENT; + } else if (!settings->resource_uri) { + zlog_error("missing required property '" JSON_PROPERTY_NAME_RESOURCE_URI "'"); + return -ENOENT; + } else if (!settings->connection_string) { + zlog_error("missing required property '" JSON_PROPERTY_NAME_CONNECTION_STRING "'"); + return -ENOENT; + } + + return 0; +} + +/** + * @brief Parses json configuration, writing it to the 'settings' object. + * + * @param json The json encoded configuration. + * @param settings The object to populate with settings. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +bootstrap_info_provider_azure_dps_config_parse(const char *json, struct bootstrap_info_provider_azure_dps_settings *settings) +{ + struct json_object *jobj = json_tokener_parse(json); + if (!jobj) { + zlog_error("invalid json found while parsing azure dps-based bootstrap info provider"); + return -EINVAL; + } + + int ret = bootstrap_info_provider_azure_dps_config_parse_jobj(jobj, settings); + json_object_put(jobj); + + return ret; +} + +/** + * @brief Uninitializes an Azure DPS provider settings object, releasing any + * resources. + * + * @param settings The settings object to uninitialize. + */ +void +bootstrap_info_provider_azure_dps_settings_uninitialize(struct bootstrap_info_provider_azure_dps_settings *settings) +{ + char **strs[] = { + &settings->service_endpoint_uri, + &settings->authority_url, + &settings->client_id, + &settings->client_secret, + &settings->resource_uri, + }; + + for (size_t i = 0; i < ARRAY_SIZE(strs); i++) { + if (*strs[i]) { + free(*strs[i]); + *strs[i] = NULL; + } + } +} diff --git a/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps_config.h b/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps_config.h new file mode 100644 index 0000000..9c4eb05 --- /dev/null +++ b/src/core/biproviders/azuredps/bootstrap_info_provider_azure_dps_config.h @@ -0,0 +1,60 @@ + +#ifndef __BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_CONFIG_H__ +#define __BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_CONFIG_H__ + +#include + +/** + * @brief Macros for JSON configuration property names. + */ +#define JSON_PROPERTY_NAME_SERVICE_ENDPOINT_URI "serviceEndpointUri" +#define JSON_PROPERTY_NAME_AUTHENTICATION "authentication" +#define JSON_PROPERTY_NAME_AUTHORITY_URL "authorityUrl" +#define JSON_PROPERTY_NAME_CLIENT_ID "clientId" +#define JSON_PROPERTY_NAME_CLIENT_SECRET "clientSecret" +#define JSON_PROPERTY_NAME_RESOURCE_URI "resourceUri" +#define JSON_PROPERTY_NAME_CONNECTION_STRING "connectionString" + +/** + * @brief Azure DPS bootstrap info provider settings. + */ +struct bootstrap_info_provider_azure_dps_settings { + char *service_endpoint_uri; + char *dps_api_version; + char *authority_url; + char *client_id; + char *client_secret; + char *resource_uri; + char *connection_string; +}; + +/** + * @brief Parses json configuration, writing it to the 'settings' object. + * + * @param jobj The json-c object with the configuration. + * @param settings The object to populate with settings. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +bootstrap_info_provider_azure_dps_config_parse_jobj(struct json_object *jobj, struct bootstrap_info_provider_azure_dps_settings *settings); + +/** + * @brief Parses json configuration, writing it to the 'settings' object. + * + * @param json The json encoded configuration. + * @param settings The object to populate with settings. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +bootstrap_info_provider_azure_dps_config_parse(const char *json, struct bootstrap_info_provider_azure_dps_settings *settings); + +/** + * @brief Uninitializes an Azure DPS provider settings object, releasing any + * resources. + * + * @param settings The settings object to uninitialize. + */ +void +bootstrap_info_provider_azure_dps_settings_uninitialize(struct bootstrap_info_provider_azure_dps_settings *settings); + +#endif //__BOOTSTRAP_INFO_PROVIDER_AZURE_DPS_CONFIG_H__ diff --git a/src/core/biproviders/azuredps/sample.cpp b/src/core/biproviders/azuredps/sample.cpp new file mode 100644 index 0000000..4a12fa3 --- /dev/null +++ b/src/core/biproviders/azuredps/sample.cpp @@ -0,0 +1,39 @@ + +#include + +#include "azure_dps_service_client.h" +#include "azure_dps_service_client_log.h" +#include "sample_input.h" + +int +main(int argc, char *argv[]) +{ + if (argc != 3 && argc != 4) { + tlog("USAGE: ./sample_cpp [api_version]"); + tlog("service_endpoint_uri must start with https:// or http://"); + return 0; + } + + struct bootstrap_info_provider_azure_dps_settings dps_settings = { + /* .service_endpoint_uri = */ strdup(argv[2]), + /* .dps_api_version = */ strdup(argc == 4 ? argv[3] : sample_dps_settings.dps_api_version), + /* .authority_url = */ NULL, + /* .client_id = */ NULL, + /* .client_secret = */ NULL, + /* .resource_uri = */ NULL, + /* .connection_string = */ strdup(argv[1]), + }; + + tlog(dps_settings.service_endpoint_uri); + + auto session = std::make_unique(&dps_settings); + + int ret = session->synchronize_dps_bi(); + tlog("synchronize_dps_bi() returned " << ret); + + free(dps_settings.service_endpoint_uri); + free(dps_settings.dps_api_version); + free(dps_settings.connection_string); + + return 0; +} \ No newline at end of file diff --git a/src/core/biproviders/azuredps/sample_input.h b/src/core/biproviders/azuredps/sample_input.h new file mode 100644 index 0000000..0a89f01 --- /dev/null +++ b/src/core/biproviders/azuredps/sample_input.h @@ -0,0 +1,24 @@ + +#ifndef __SAMPLE_INPUT_H__ +#define __SAMPLE_INPUT_H__ + +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +#include "../bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps.h" + +const struct { + const char *service_endpoint_uri = "https://wifiztp.azure-devices-provisioning.net"; + const char *dps_api_version = "2019-03-31"; +} sample_dps_settings; + +extern const struct dpp_bootstrap_publickey_hash sample_hash; +extern const struct bootstrap_info_query sample_query; + +#ifdef __cplusplus +} +#endif //__cplusplus + +#endif //__SAMPLE_INPUT_H__ diff --git a/src/core/biproviders/bootstrap_info_provider.c b/src/core/biproviders/bootstrap_info_provider.c new file mode 100644 index 0000000..bffb3b3 --- /dev/null +++ b/src/core/biproviders/bootstrap_info_provider.c @@ -0,0 +1,369 @@ + +#include +#include +#include + +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps.h" +#include "bootstrap_info_provider_file.h" +#include "bootstrap_info_provider_private.h" +#include "bootstrap_info_provider_settings.h" +#include "string_utils.h" +#include "ztp_log.h" + +/** + * @brief Converts a string to a bootstrap info provider type. + * + * @param type The string representing the bootstrap info provider type. + * @return enum bootstrap_info_provider_type The corresponding provider type, + * if valid. Otherwise, BOOTSTRAP_INFO_PROVIDER_INVALID is returned. + */ +enum bootstrap_info_provider_type +parse_bootstrap_info_provider_type(const char *type) +{ + if (strcmp(type, "file") == 0) { + return BOOTSTRAP_INFO_PROVIDER_FILE; + } else if (strcmp(type, "azuredps") == 0) { + return BOOTSTRAP_INFO_PROVIDER_AZUREDPS; + } else { + return BOOTSTRAP_INFO_PROVIDER_INVALID; + } +} + +/** + * @brief Converts a bootstrap info provider type to a string. + * + * @param type The type to convert. + * @return const char* A string representing the type. + */ +const char * +bootstrap_info_provider_type_str(enum bootstrap_info_provider_type type) +{ + switch (type) { + case BOOTSTRAP_INFO_PROVIDER_FILE: + return "file"; + case BOOTSTRAP_INFO_PROVIDER_AZUREDPS: + return "azuredps"; + default: + return "invalid"; + } +} + +/** + * @brief Initializes a bootstrap info provider. + * + * @param provider The provider instance to initialize. + * @param settings The settings to use for the provider. + * @return int 0 if the provider was successfully initialized, non-zero + * otherwise. + */ +static int +bootstrap_info_provider_initialize(struct bootstrap_info_provider *provider, struct bootstrap_info_provider_settings *settings) +{ + switch (settings->type) { + case BOOTSTRAP_INFO_PROVIDER_FILE: + provider->ops = &bootstrap_info_provider_file_ops; + break; + case BOOTSTRAP_INFO_PROVIDER_AZUREDPS: + provider->ops = &bootstrap_info_provider_azure_dps_ops; + break; + default: + return -EINVAL; + } + + char *name = strdup(settings->name); + if (!name) + return -ENOMEM; + + provider->name = name; + provider->type = settings->type; + provider->context = NULL; + provider->settings = settings; + provider->started = false; + + return 0; +} + +/** + * @brief Uninitialize a bootstrap info provider. + * + * @param provider The provider to uninitialize. + */ +static void +bootstrap_info_provider_uninitialize(struct bootstrap_info_provider *provider) +{ + bootstrap_info_provider_settings_uninitialize(provider->settings); + + provider->type = BOOTSTRAP_INFO_PROVIDER_INVALID; + provider->settings = NULL; + provider->ops = NULL; + provider->started = false; + + if (provider->name) { + free(provider->name); + provider->name = NULL; + } + + if (!list_empty(&provider->list)) + list_del(&provider->list); +} + +/** + * @brief Creates a new bootstrap info provider. + * + * @param settings The settings to use to initialize the provider with. + * @return struct bootstrap_info_provider* + */ +struct bootstrap_info_provider * +bootstrap_info_provider_create(struct bootstrap_info_provider_settings *settings) +{ + struct bootstrap_info_provider *provider = calloc(1, sizeof *provider); + if (!provider) { + zlog_error("failed to allocate memory for bootstrap info provider"); + return NULL; + } + + int ret = bootstrap_info_provider_initialize(provider, settings); + if (ret < 0) { + zlog_error("failed to initialize bootstrap info provider (%d)", ret); + free(provider); + return NULL; + } + + return provider; +} + +/** + * @brief Destroy a bootstrap info provider. This will uninitialize the + * provider and free its memory. + * + * @param provider The provider to destroy. + */ +void +bootstrap_info_provider_destroy(struct bootstrap_info_provider *provider) +{ + if (!provider) + return; + + bootstrap_info_provider_uninitialize(provider); + free(provider); +} + +/** + * @brief Get the containing bootstrap_info_record_result_entry structure + * pointer from a record. + * + * No validity checks are done on the returned pointer. It is assumed that the + * input pointer ('record') did in fact come from a proper container of type + * struct bootstrap_info_record_result_entry. + * + * @param record The record to get the parent container for. + * @return struct bootstrap_info_record_result_entry* A pointer to the + * containing struct bootstrap_info_record_result_entry structure. + */ +static struct bootstrap_info_record_result_entry * +record_to_entry(struct bootstrap_info_record *record) +{ + return container_of(record, struct bootstrap_info_record_result_entry, record); +} + +static void +bootstrap_info_query_result_entry_destroy(struct bootstrap_info_record_result_entry *entry); + +/** + * @brief Destroys a bootstrap_info_record that was obtained from + * bootstrap_info_record_alloc(). + * + * @param record The record to destroy. + */ +void +bootstrap_info_record_destroy(struct bootstrap_info_record *record) +{ + bootstrap_info_query_result_entry_destroy(record_to_entry(record)); +} + +/** + * @brief Uninitialize a bootstrap info record. + * + * @param record The record to uninitialize. + */ +static void +bootstrap_info_record_uninitialize(struct bootstrap_info_record *record) +{ + if (record->dpp_uri) { + free(record->dpp_uri); + record->dpp_uri = NULL; + } +} + +/** + * @brief Determines if the specified entry is valid. Specifically, this + * ensures the record was allocated as part of + * bootstrap_info_record_result_alloc(). + * + * @param result The result to check for validity. + * @return true If the result is valid. + * @return false If the result is invalid. + */ +static bool +bootstrap_info_record_result_entry_is_valid(struct bootstrap_info_record_result_entry *result) +{ + return (result && (result->cookie == BOOTSTRAP_INFO_RECORD_RESULT_ENTRY_COOKIE)); +} + +/** + * @brief Destroy a query result entry, releasing any owned resources. + * + * @param entry The entry to destroy. + */ +static void +bootstrap_info_query_result_entry_destroy(struct bootstrap_info_record_result_entry *entry) +{ + if (!entry) + return; + + if (!bootstrap_info_record_result_entry_is_valid(entry)) { + zlog_warning("invalid bootstrap_info_record specified (missing or invalid cookie)"); + return; + } + + bootstrap_info_record_uninitialize(&entry->record); + + if (!list_empty(&entry->list)) + list_del(&entry->list); + + free(entry); +} + +/** + * @brief Initializes a query result. + * + * @param result The result to initialize. + */ +void +bootstrap_info_query_result_initialize(struct bootstrap_info_query_result *result) +{ + INIT_LIST_HEAD(&result->records); +} + +/** + * @brief Uninitializes a query result, releasing any owned resources. + * + * @param result The result to uninitialize. + */ +void +bootstrap_info_query_result_uninitialize(struct bootstrap_info_query_result *result) +{ + struct bootstrap_info_record_result_entry *entry; + struct bootstrap_info_record_result_entry *entrytmp; + + list_for_each_entry_safe (entry, entrytmp, &result->records, list) { + bootstrap_info_query_result_entry_destroy(entry); + } +} + +/** + * @brief Initializes a bootstrap info query with required inputs. + * + * @param query The query to initialize. + * @param hash The public key hash to serve as the primary, required criteria. + */ +void +bootstrap_info_query_initialize(struct bootstrap_info_query *query, const struct dpp_bootstrap_publickey_hash *hash) +{ + query->options = 0; + query->criteria_extra_num = 0; + + struct bootstrap_info_query_criterion *criterion = &query->criterion; + criterion->type = BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH; + criterion->data.pubkey_hash.hash = hash; + + hex_encode(hash->data, sizeof hash->data, criterion->data.pubkey_hash.hexstr, sizeof query->criterion.data.pubkey_hash.hexstr); +} + +/** + * @brief Adds a record to the query result. + * + * @param record The record to add to the query result. + */ +int +bootstrap_info_query_result_add(struct bootstrap_info_query_result *result, const char *dpp_uri) +{ + struct bootstrap_info_record_result_entry *entry = calloc(1, sizeof *entry); + if (!entry) { + zlog_error("failed to allocate query result record"); + return -ENOMEM; + } + + INIT_LIST_HEAD(&entry->list); + entry->cookie = BOOTSTRAP_INFO_RECORD_RESULT_ENTRY_COOKIE; + entry->record.dpp_uri = strdup(dpp_uri); + if (!entry->record.dpp_uri) { + zlog_error("failed to allocate query result dpp_uri"); + return -ENOMEM; + } + + list_add(&entry->list, &result->records); + + return 0; +} + +/** + * @brief Synchronize the logical view of bootstrap information with its backing. + * + * Following successful execution of this function, the view of bootstrap + * information must be current. This means that all bootstrap information + * added since the last successful synchronization call must be available in response to a query. + * + * @param provider The provider to execute the operation on. + * @param options Options controlling how synchronization should be performed. + * @return int + */ +int +bootstrap_info_provider_synchronize(struct bootstrap_info_provider *provider, const struct bootstrap_info_sync_options *options) +{ + if (!provider->ops->synchronize) + return -EOPNOTSUPP; + if (!provider->context) + return -EINVAL; + + return provider->ops->synchronize(provider->context, options); +} + +/** + * @brief Queries the provider for bootstrapping information matching a set + * of criteria. + * + * Every query must match the fixed criteria in the 'criterion' field, + * which will always be of type BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH. + * Additional criteria may be specified by the framework in the + * 'criteria_extra' field. Providers may use this criteria, if specified, + * to improve query performance or to implement storage of records in a + * more efficient way. + * + * Matching records must be added to the 'result' argument. This is an + * opaque structure which cannot be interacted with directly. Instead, + * convenience functions are provided for allocating records and adding + * them to the result structure. + * + * To allocate a record for inclusion in a query result, providers should + * use bootstrap_info_record_alloc(). + * + * To include a record in a query result, use + * bootstrap_info_query_add_result(). + * + * @param provider The provider to execute the operation on. + * @param query The query to perform, containing the criteria and options + * controlling how to perform or filter the query. + * @param result The result of the query operation. + */ +int +bootstrap_info_provider_query(struct bootstrap_info_provider *provider, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result) +{ + if (!provider->ops->query) + return -EOPNOTSUPP; + if (!provider->context) + return -EINVAL; + + return provider->ops->query(provider->context, query, result); +} diff --git a/src/core/biproviders/bootstrap_info_provider.h b/src/core/biproviders/bootstrap_info_provider.h new file mode 100644 index 0000000..8b810b8 --- /dev/null +++ b/src/core/biproviders/bootstrap_info_provider.h @@ -0,0 +1,330 @@ + +#ifndef __BOOTSTRAP_INFO_PROVIDER_H__ +#define __BOOTSTRAP_INFO_PROVIDER_H__ + +#include +#include + +#include "dpp.h" +#include "userspace/linux/list.h" + +struct bootstrap_info_provider_settings; + +/** + * @brief Bootstrapping information record. + */ +struct bootstrap_info_record { + char *dpp_uri; +}; + +/** + * @brief Destroys a bootstrap_info_record that was obtained from + * bootstrap_info_record_alloc(). + * + * @param record The record to destroy. Must have been obtained from + * bootstrap_info_record_alloc(). + */ +void +bootstrap_info_record_destroy(struct bootstrap_info_record *record); + +/** + * @brief Bootstrap info provider type. + */ +enum bootstrap_info_provider_type { + BOOTSTRAP_INFO_PROVIDER_INVALID = 0, + BOOTSTRAP_INFO_PROVIDER_FILE, + BOOTSTRAP_INFO_PROVIDER_AZUREDPS, +}; + +/** + * @brief Converts a string to a bootstrap info provider type. + * + * @param type The string representing the bootstrap info provider type. + * @return enum bootstrap_info_provider_type The corresponding provider type, + * if valid. Otherwise, BOOTSTRAP_INFO_PROVIDER_INVALID is returned. + */ +enum bootstrap_info_provider_type +parse_bootstrap_info_provider_type(const char *type); + +/** + * @brief Converts a bootstrap info provider type to a string. + * + * @param type The type to convert. + * @return const char* A string representing the type. + */ +const char * +bootstrap_info_provider_type_str(enum bootstrap_info_provider_type type); + +/** + * @brief Bootstrap information query criterion type. + * + * Describes all the types of criteria that may be present in a bootstrapping + * information query of a provider. + */ +enum bootstrap_info_query_criterion_type { + BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH = 0, + BOOTSTRAP_INFO_QUERY_CRITERION_MAC, + BOOTSTRAP_INFO_QUERY_CRITERION_URI_INFO, +}; + +/** + * @brief Bootstrap information query criterion. This represents a single + * criterion, the type of which is specified by the 'type' field. The + * corresponding field of the 'data' union must be used. The following maps the + * type to the union fields: + * + * BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH -> pubkey_hash + * BOOTSTRAP_INFO_QUERY_CRITERION_MAC -> mac + * BOOTSTRAP_INFO_QUERY_CRITERION_URI_INFO -> uri_info + */ +struct bootstrap_info_query_criterion { + enum bootstrap_info_query_criterion_type type; + union { + struct { + const uint8_t (*data)[DPP_MAC_LENGTH]; + char hexstr[(DPP_MAC_LENGTH * 2) + 1]; + } mac; + struct { + const struct dpp_bootstrap_publickey_hash *hash; + char hexstr[(DPP_BOOTSTRAP_PUBKEY_HASH_LENGTH * 2) + 1]; + } pubkey_hash; + const char *uri_info; + } data; +}; + +/** + * @brief All criteria provided must match. By default, any matching criteria + * should produce a positive result/match. + */ +#define BOOTSTRAP_INFO_QUERY_OPTION_ALL_OR_NOTHING (0x00000001u) + +/** + * @brief The query should complete once (if) a single record has matched. This + * implies only one record should be returned, even if multiple records are + * available; the first matching record should be returned. For providers that + * match records in parallel, the following order of criteria precedence should + * be used to determine which record to return first: + * + * 1) BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH + * 2) BOOTSTRAP_INFO_QUERY_CRITERION_MAC + * 3) BOOTSTRAP_INFO_QUERY_CRITERION_URI_INFO + */ +#define BOOTSTRAP_INFO_QUERY_OPTION_SINGLE (0x00000002u) + +/** + * @brief Bootstrapping information query. Contains information a provider needs + * to lookup bootstrapping information from its backing source. + * + * The 'criterion' field will always be of type + * BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH. This criteria must always match, + * regardless of any option or extra criteria specified. + */ +struct bootstrap_info_query { + uint32_t options; + struct bootstrap_info_query_criterion criterion; + struct bootstrap_info_query_criterion *criteria_extra; + size_t criteria_extra_num; +}; + +/** + * @brief Initializes a bootstrap info query with required inputs. + * + * @param query The query to initialize. + * @param hash The public key hash to serve as the primary, required criteria. + */ +void +bootstrap_info_query_initialize(struct bootstrap_info_query *query, const struct dpp_bootstrap_publickey_hash *hash); + +/** + * @brief The result of querying a provider. This is a forward declaration as + * bootstrap info providers should not interact with the result structure + * directly. Instead, they should use the convenience functions + * bootstrap_info_query_result_*. + */ +struct bootstrap_info_query_result; + +/** + * @brief Adds a record to the query result. + * + * @param dpp_uri The dpp uri to add to the query result. + */ +int +bootstrap_info_query_result_add(struct bootstrap_info_query_result *result, const char *dpp_uri); + +/** + * @brief Initializes a query result. + * + * @param result The result to initialize. + */ +void +bootstrap_info_query_result_initialize(struct bootstrap_info_query_result *result); + +/** + * @brief Uninitializes a query result, releasing any owned resources. + * + * @param result The result to uninitialize. + */ +void +bootstrap_info_query_result_uninitialize(struct bootstrap_info_query_result *result); + +/** + * @brief Options controlling how synchronization should be performed. + * Currently none exist, however, to keep the interface stable, the structure + * is defined and included in the synchronize API. + */ +struct bootstrap_info_sync_options { + int sentinel; +}; + +/** + * @brief Bootstrap information provider operation vector. Contains the + * provider specific implementation of the bootstrap information provider + * interface. + */ +struct bootstrap_info_provider_ops { + /** + * @brief Initializes a provider for use. + * + * It is not expected that the provider ensures an initially synchronized + * view following initialization. The provider should only synchronize its + * view of bootstrapping information when the synchronization operation is + * invoked. + * + * The implementation must return 0 for the provider to be used by the + * controlling daemon. Any non-zero value returned indicates failure to + * initialize. + * + * @param settings The common settings for this provider. + * @param context An output pointer to store context associated with the + * instance. This context is passed to each provider operation and may be + * used to save state. + * @return Returns 0 if the provider successfully initialized, non-zero + * otherwise. + */ + int (*initialize)(const struct bootstrap_info_provider_settings *settings, void **context); + + /** + * @brief Uninitializes a provider, freeing all owned resources. + * + * @param context The instance context associated during initialize. + */ + void (*uninitialize)(void *context); + + /** + * @brief Synchronize the logical view of bootstrap information with its backing. + * + * Following successful execution of this function, the view of bootstrap + * information must be current. This means that all bootstrap information + * added since the last successful synchronization call must be available in response to a query. + * + * @param context The instance context associated during initialize. + * @param options Options controlling how synchronization should be performed. + */ + int (*synchronize)(void *context, const struct bootstrap_info_sync_options *options); + + /** + * @brief Queries the provider for bootstrapping information matching a set + * of criteria. + * + * Every query must match the fixed criteria in the 'criterion' field, + * which will always be of type BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH. + * Additional criteria may be specified by the framework in the + * 'criteria_extra' field. Providers may use this criteria, if specified, + * to improve query performance or to implement storage of records in a + * more efficient way. + * + * Matching records must be added to the 'result' argument. This is an + * opaque structure which cannot be interacted with directly. Instead, + * convenience functions are provided for allocating records and adding + * them to the result structure. + * + * To allocate a record for inclusion in a query result, providers should + * use bootstrap_info_record_alloc(). + * + * To include a record in a query result, use + * bootstrap_info_query_add_result(). + * + * @param context The instance context associated during initialize. + * @param query The query to perform, containing the criteria and options + * controlling how to perform or filter the query. + * @param result The result of the query operation. + */ + int (*query)(void *context, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result); +}; + +/** + * @brief Bootstrap information provider. + */ +struct bootstrap_info_provider { + struct list_head list; + char *name; + void *context; + bool started; + enum bootstrap_info_provider_type type; + struct bootstrap_info_provider_ops *ops; + struct bootstrap_info_provider_settings *settings; +}; + +/** + * @brief Creates a new bootstrap info provider. + * + * @param settings The settings to use to initialize the provider with. + * @return struct bootstrap_info_provider* + */ +struct bootstrap_info_provider * +bootstrap_info_provider_create(struct bootstrap_info_provider_settings *settings); + +/** + * @brief Destroy a bootstrap info provider. This will uninitialize the + * provider and free its memory. + * + * @param provider The provider to destroy. + */ +void +bootstrap_info_provider_destroy(struct bootstrap_info_provider *provider); + +/** + * @brief Synchronize the logical view of bootstrap information with its backing. + * + * Following successful execution of this function, the view of bootstrap + * information must be current. This means that all bootstrap information + * added since the last successful synchronization call must be available in response to a query. + * + * @param provider The provider to execute the operation on. + * @param options Options controlling how synchronization should be performed. + * @return int + */ +int +bootstrap_info_provider_synchronize(struct bootstrap_info_provider *provider, const struct bootstrap_info_sync_options *options); + +/** + * @brief Queries the provider for bootstrapping information matching a set + * of criteria. + * + * Every query must match the fixed criteria in the 'criterion' field, + * which will always be of type BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH. + * Additional criteria may be specified by the framework in the + * 'criteria_extra' field. Providers may use this criteria, if specified, + * to improve query performance or to implement storage of records in a + * more efficient way. + * + * Matching records must be added to the 'result' argument. This is an + * opaque structure which cannot be interacted with directly. Instead, + * convenience functions are provided for allocating records and adding + * them to the result structure. + * + * To allocate a record for inclusion in a query result, providers should + * use bootstrap_info_record_alloc(). + * + * To include a record in a query result, use + * bootstrap_info_query_add_result(). + * + * @param provider The provider to execute the operation on. + * @param query The query to perform, containing the criteria and options + * controlling how to perform or filter the query. + * @param result The result of the query operation. + */ +int +bootstrap_info_provider_query(struct bootstrap_info_provider *provider, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result); + +#endif // __BOOTSTRAP_INFO_PROVIDER_H__ diff --git a/src/core/biproviders/bootstrap_info_provider_private.h b/src/core/biproviders/bootstrap_info_provider_private.h new file mode 100644 index 0000000..42ac237 --- /dev/null +++ b/src/core/biproviders/bootstrap_info_provider_private.h @@ -0,0 +1,34 @@ + +#ifndef __BOOTSTRAP_INFO_PROVIDER_PRIVATE_H__ +#define __BOOTSTRAP_INFO_PROVIDER_PRIVATE_H__ + +#include +#include + +#include "bootstrap_info_provider.h" + +/** + * @brief Cookie value that ensures bootstrap info records are authentic (ie. + * obtained from bootstap_info_record_alloc()). + */ +#define BOOTSTRAP_INFO_RECORD_RESULT_ENTRY_COOKIE (0x0A0A0A0A) + +/** + * @brief Result structure to aid tracking results from bootstrap info + * providers. This allows the providers to use dedicated functions to safely + * allocate, destroy and add records to a query result. + */ +struct bootstrap_info_record_result_entry { + struct list_head list; + struct bootstrap_info_record record; + uint32_t cookie; +}; + +/** + * @brief The result of querying a provider. + */ +struct bootstrap_info_query_result { + struct list_head records; +}; + +#endif //__BOOTSTRAP_INFO_PROVIDER_PRIVATE_H__ diff --git a/src/core/biproviders/bootstrap_info_provider_settings.c b/src/core/biproviders/bootstrap_info_provider_settings.c new file mode 100644 index 0000000..7e4bc0d --- /dev/null +++ b/src/core/biproviders/bootstrap_info_provider_settings.c @@ -0,0 +1,56 @@ + +#include +#include + +#include "bootstrap_info_provider_settings.h" +#include "string_utils.h" + +/** + * @brief Allocates and initializes a new bootstrap info provider settings object. + * + * @return struct bootstrap_info_provider_settings* + */ +struct bootstrap_info_provider_settings * +bootstrap_info_provider_settings_alloc(void) +{ + struct bootstrap_info_provider_settings *settings = calloc(1, sizeof *settings); + if (!settings) + return NULL; + + INIT_LIST_HEAD(&settings->list); + settings->type = BOOTSTRAP_INFO_PROVIDER_INVALID; + settings->expiration_time = BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET; + + return settings; +} + +/** + * @brief Uninitializes provider settings, freeing any owned resources. + * + * @param settings The provider settings to uninitialize. + */ +void +bootstrap_info_provider_settings_uninitialize(struct bootstrap_info_provider_settings *settings) +{ + if (!settings) + return; + + switch (settings->type) { + case BOOTSTRAP_INFO_PROVIDER_FILE: + bootstrap_info_provider_file_settings_uninitialize(settings->file); + break; + case BOOTSTRAP_INFO_PROVIDER_AZUREDPS: + bootstrap_info_provider_azure_dps_settings_uninitialize(settings->dps); + break; + default: + break; + } + + if (settings->name) { + free(settings->name); + settings->name = NULL; + } + + if (!list_empty(&settings->list)) + list_del(&settings->list); +} diff --git a/src/core/biproviders/bootstrap_info_provider_settings.h b/src/core/biproviders/bootstrap_info_provider_settings.h new file mode 100644 index 0000000..2288ff2 --- /dev/null +++ b/src/core/biproviders/bootstrap_info_provider_settings.h @@ -0,0 +1,48 @@ + +#ifndef __BOOTSTRAP_INFO_PROVIDER_SETTINGS_H__ +#define __BOOTSTRAP_INFO_PROVIDER_SETTINGS_H__ + +#include +#include + +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps_config.h" +#include "bootstrap_info_provider_file_config.h" + +/** + * @brief Value indicating the provider expiration time is unset. + */ +#define BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET ((uint32_t)(INT32_MAX)) + +/** + * @brief Bootstrap info provider settings. All structure members describe + * settings that are shared by all providers. + */ +struct bootstrap_info_provider_settings { + struct list_head list; + char *name; + uint32_t expiration_time; + enum bootstrap_info_provider_type type; + union { + struct bootstrap_info_provider_file_settings *file; + struct bootstrap_info_provider_azure_dps_settings *dps; + }; +}; + +/** + * @brief Allocates and initializes a new bootstrap info provider settings object. + * + * @return struct bootstrap_info_provider_settings* + */ +struct bootstrap_info_provider_settings * +bootstrap_info_provider_settings_alloc(void); + +/** + * @brief Uninitializes provider settings, freeing any owned resources. + * + * @param settings The provider settings to uninitialize. + */ +void +bootstrap_info_provider_settings_uninitialize(struct bootstrap_info_provider_settings *settings); + +#endif // __BOOTSTRAP_INFO_PROVIDER_SETTINGS_H__ diff --git a/src/core/biproviders/file/CMakeLists.txt b/src/core/biproviders/file/CMakeLists.txt new file mode 100644 index 0000000..1d9adc5 --- /dev/null +++ b/src/core/biproviders/file/CMakeLists.txt @@ -0,0 +1,13 @@ + +target_sources(ztpcore + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/bootstrap_info_provider_file.c + ${CMAKE_CURRENT_SOURCE_DIR}/bootstrap_info_provider_file_config.c +) + +target_include_directories(ztpcore + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../../utils +) diff --git a/src/core/biproviders/file/bootstrap_info_provider_file.c b/src/core/biproviders/file/bootstrap_info_provider_file.c new file mode 100644 index 0000000..5fb1f63 --- /dev/null +++ b/src/core/biproviders/file/bootstrap_info_provider_file.c @@ -0,0 +1,411 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_file.h" +#include "bootstrap_info_provider_file_config.h" +#include "bootstrap_info_provider_settings.h" +#include "json_parse.h" +#include "time_utils.h" +#include "ztp_log.h" + +/** + * @brief Read all the bootstrap information from the file. This will place a + * pointer to the base array which contains the bootstrapping information into + * the 'jobj_bootstrap_info' field. + * + * @param instance The instance object. + * @return int 0 if successful, non-zero otherwise. + */ +static int +bootstrap_info_provider_file_read_all(struct bootstrap_info_provider_file_instance *instance) +{ + int ret; + const char *path = instance->settings->path; + const char *pointer = instance->settings->json_pointer_array; + + struct json_object *jobj_bootstrap_info = NULL; + struct json_object *jobj = json_object_from_file(path); + if (!jobj) { + const char *err = json_util_get_last_err(); + zlog_error("failed parsing bootstrap info provider record file %s (%s)", path, err); + return -1; + } + + json_type type_root = json_object_get_type(jobj); + if (type_root == json_type_array) { + jobj_bootstrap_info = jobj; + } else { + ret = json_pointer_get(jobj, pointer, &jobj_bootstrap_info); + if (ret < 0) { + zlog_error("failed to resolve boot info records from json pointer %s in file %s (%d)", pointer, path, ret); + goto out; + } + + json_type type_info = json_object_get_type(jobj_bootstrap_info); + if (type_info != json_type_array) { + zlog_error("json pointer does not refer to array type (actual=%s)", json_type_to_name(type_info)); + goto out; + } + } + + json_object_get(jobj_bootstrap_info); + instance->jobj_bootstrap_info = jobj_bootstrap_info; + + // Refresh the saved file modification time. If this fails, set the modification time manually such that it is + struct stat statbuf; + ret = stat(path, &statbuf); + if (ret < 0) { + zlog_warning("failed to retrieve last modidication time for %s", path); + } else { + instance->time_modified = statbuf.st_mtim; + } + +out: + json_object_put(jobj); + return ret; +} + +/** + * @brief Initialize a new provider instance. This will parse the settings, + * associate them with the instance and prepare the provider for use. + * + * @param instance The instance to initialize. It is assumed that object describes an uninitialized provider. + * @param settings The settings for the provider. + * @return int 0 if the provider was successfully initialized, non-zero otherwise. + */ +static int +bootstrap_info_provider_file_initialize(struct bootstrap_info_provider_file_instance *instance, struct bootstrap_info_provider_file_settings *settings) +{ + instance->settings = settings; + + // Set last modified time to 0 to force the first synchronization. + instance->time_modified.tv_sec = 0; + instance->time_modified.tv_nsec = 0; + + instance->ptr_base = instance->settings->json_pointer_object_base + ? instance->settings->json_pointer_object_base + : "/"; + + instance->ptr_dpp_uri = instance->settings->json_key_dpp_uri + ? instance->settings->json_key_dpp_uri + : "/" JSON_PROPERTY_NAME_DPP_URI; + + instance->ptr_publickeyhash = instance->settings->json_key_publickeyhash + ? instance->settings->json_key_publickeyhash + : "/" JSON_PROPERTY_NAME_PUBLIC_KEY_HASH; + + return 0; +} + +/** + * @brief Creates and initializes a new instance of a file-based bootstrap info provider. + * + * @param out The output pointer to write the new instance to. + * @param settings The settings to be associated with the instance. + * @return int 0 if the instance was created, *instance will hold the newly + * created object. Otherwise a non-zero value will be returned, with *instance + * having the value NULL. + */ +static int +bootstrap_info_provider_file_create(struct bootstrap_info_provider_file_instance **out, struct bootstrap_info_provider_file_settings *settings) +{ + *out = NULL; + + struct bootstrap_info_provider_file_instance *instance = calloc(1, sizeof *instance); + if (!instance) { + zlog_error("allocation failure creating file-based bootstrap info provider"); + return -ENOMEM; + } + + int ret = bootstrap_info_provider_file_initialize(instance, settings); + if (ret < 0) { + free(instance); + return ret; + } + + *out = instance; + + return 0; +} + +/** + * @brief Uninitializes a provider. + * + * @param instance The instance to uninitialize. + */ +static void +bootstrap_info_provider_file_uninitialize(struct bootstrap_info_provider_file_instance *instance) +{ + __unused(instance); + // nothing to do +} + +/** + * @brief Uninitializes and destroys a provider. All owned resources will be + * freed and the memory associated with the provider itself is released. The + * 'instance' pointer must not be used beyond successfuly completion of this + * call. + * + * @param instance The instance to destroy. + */ +static void +bootstrap_info_provider_file_destroy(struct bootstrap_info_provider_file_instance *instance) +{ + bootstrap_info_provider_file_uninitialize(instance); + free(instance); +} + +/** + * @brief Determines if the file data backing this provider is dirty. The file + * data is dirty if it has changed since the last time it was accessed. + * + * Note that this is not a strict check. The file's last modification time is + * used to examine if the contents have changed. It is possible that the file + * was opened and written back as-is. In this case, the file modification time + * will change but the contents will be identical. Similarly, the records + * stored in the file can stay the same but be re-written in a different order. + * This will be detected as a change despite the record content remaining + * unchanged. It is expected these scenarios will not often occur so this + * approach is acceptable. + * + * @param instance The instance to check for dirtiness. + * @return true If the file data has changed since the last time it was read. + * @return false If the file data has not changed since the last time is was read. + */ +static bool +bootstrap_info_provider_file_is_dirty(struct bootstrap_info_provider_file_instance *instance) +{ + struct stat statbuf; + + int ret = stat(instance->settings->path, &statbuf); + if (ret < 0) { + ret = errno; + zlog_warning("stat() dirty check of %s failed (%d), assuming dirty", instance->settings->path, ret); + return true; + } + + return timespeccmp(&statbuf.st_mtim, &instance->time_modified, !=); +} + +/** + * @brief Wrapper to synchronize the file contents with the in-memory contents. + * + * @param instance The instance object. + * @return int 0 if the in-memory contents are in sync with the file contents. + * Non-zero otherwise. + */ +static int +bootstrap_info_provider_file_synchronize(struct bootstrap_info_provider_file_instance *instance) +{ + if (!bootstrap_info_provider_file_is_dirty(instance)) + return 0; + + int ret = bootstrap_info_provider_file_read_all(instance); + if (ret < 0) { + zlog_warning("failed to synchronize bootstrap info (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief JSON parsing context that is passed to the query_bootstrap_info_entry + * function, which is used to process json array entries that contain + * bootstrapping information. + */ +struct query_bootstrap_info_entry_context { + const struct bootstrap_info_query *query; + struct bootstrap_info_query_result *result; + struct bootstrap_info_provider_file_instance *instance; + const char *ptr_publickey; + const char *ptr_publickeyhash; +}; + +/** + * @brief + * + * @param parent + * @apram array + * @param name + * @param value + * @param index + * @param type + * @param context + */ +static int +query_bootstrap_info_entry(struct json_object *parent, struct json_object *array, const char *name, struct json_object *value, uint32_t index, json_type type, void *context) +{ + __unused(parent); + __unused(array); + __unused(name); + __unused(index); + __unused(type); + + struct query_bootstrap_info_entry_context *opts = (struct query_bootstrap_info_entry_context *)context; + + struct json_object *jobj_pkhash = NULL; + int ret = json_pointer_getf(value, &jobj_pkhash, "%s/%s", opts->instance->ptr_base, opts->instance->ptr_publickeyhash); + if (ret < 0) { + zlog_warning("unable to find public key hash entry"); + return JSON_ITERATE_CONTINUE; + } + + json_type type_pkhash = json_object_get_type(jobj_pkhash); + if (type_pkhash != json_type_string) { + zlog_warning("unexpected type for public key hash (%s)", json_type_to_name(type_pkhash)); + return JSON_ITERATE_CONTINUE; + } + + const char *pkhashstr = json_object_get_string(jobj_pkhash); + if (strcmp(pkhashstr, opts->query->criterion.data.pubkey_hash.hexstr)) + return JSON_ITERATE_CONTINUE; + + struct json_object *jobj_dpp_uri = NULL; + ret = json_pointer_getf(value, &jobj_dpp_uri, "%s/%s", opts->instance->ptr_base, opts->instance->ptr_dpp_uri); + if (ret < 0) { + zlog_error("unable to retrieve public key for matching hash '%s' (%d)", pkhashstr, ret); + return JSON_ITERATE_CONTINUE; + } + + const char *dpp_uri = json_object_get_string(jobj_dpp_uri); + if (!dpp_uri) { + zlog_error("failed to allocate buffer for bootstrap info public key"); + return JSON_ITERATE_CONTINUE; + } + + ret = bootstrap_info_query_result_add(opts->result, dpp_uri); + if (ret < 0) { + zlog_error("failed to add matching bootstrap info record to result (%d)", ret); + return JSON_ITERATE_CONTINUE; + } + + return JSON_ITERATE_STOP; +} + +/** + * @brief Performs a query for bootstrapping information. + * + * @param instance The provider instance. + * @param query The query object describing the criteria to search for. + * @param result The result structure to add results to. + * @return int 0 if the query was performed successfully, non-zero otherwise. + * Note 0 is returned when no records matched; the return value indicates if + * the query was performed, not whether a record matched. + */ +static int +bootstrap_info_provider_file_query(struct bootstrap_info_provider_file_instance *instance, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result) +{ + assert(query->criterion.type == BOOTSTRAP_INFO_QUERY_CRITERION_PUBKEY_HASH); + + struct query_bootstrap_info_entry_context context = { + .query = query, + .result = result, + .instance = instance, + }; + + json_for_each_array_entry_ss(NULL, instance->jobj_bootstrap_info, "bip-file", query_bootstrap_info_entry, &context); + + return 0; +} + +/** + * @brief + * + * @param settings + * @param context + * @return int + */ +static int +bootstrap_info_provider_file_op_initialize(const struct bootstrap_info_provider_settings *settings, void **context) +{ + struct bootstrap_info_provider_file_instance *instance; + int ret = bootstrap_info_provider_file_create(&instance, settings->file); + if (ret < 0) { + zlog_error("failed to create file-based bootstrap provider instance (%d)", ret); + return ret; + } + + *context = instance; + return 0; +} + +/** + * @brief + * + */ +void +bootstrap_info_provider_file_op_uninitialize(void *context) +{ + struct bootstrap_info_provider_file_instance *instance = (struct bootstrap_info_provider_file_instance *)context; + if (!instance) + return; + + bootstrap_info_provider_file_destroy(instance); +} + +/** + * @brief + * + * @return int + */ +static int +bootstrap_info_provider_file_op_synchronize(void *context, const struct bootstrap_info_sync_options *options) +{ + __unused(options); + + struct bootstrap_info_provider_file_instance *instance = (struct bootstrap_info_provider_file_instance *)context; + if (!instance) + return -EBADF; + + int ret = bootstrap_info_provider_file_synchronize(instance); + if (ret < 0) { + zlog_debug("failed to synchronize file-based bootstrap info provider (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param query + * @param result + * @return int + */ +static int +bootstrap_info_provider_file_op_query(void *context, const struct bootstrap_info_query *query, struct bootstrap_info_query_result *result) +{ + struct bootstrap_info_provider_file_instance *instance = (struct bootstrap_info_provider_file_instance *)context; + if (!instance) + return -EBADF; + + int ret = bootstrap_info_provider_file_query(instance, query, result); + if (ret < 0) { + zlog_debug("failed to query file-based bootstrap info provider (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief File provider operation vector. This is provided to ztpd for each + * installed instance of the file provider. + */ +struct bootstrap_info_provider_ops bootstrap_info_provider_file_ops = { + .initialize = bootstrap_info_provider_file_op_initialize, + .uninitialize = bootstrap_info_provider_file_op_uninitialize, + .synchronize = bootstrap_info_provider_file_op_synchronize, + .query = bootstrap_info_provider_file_op_query, +}; diff --git a/src/core/biproviders/file/bootstrap_info_provider_file.h b/src/core/biproviders/file/bootstrap_info_provider_file.h new file mode 100644 index 0000000..7bc4396 --- /dev/null +++ b/src/core/biproviders/file/bootstrap_info_provider_file.h @@ -0,0 +1,30 @@ + +#ifndef __BOOTSTRAP_INFO_PROVIDER_FILE_H__ +#define __BOOTSTRAP_INFO_PROVIDER_FILE_H__ + +#include + +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_file_config.h" + +struct json_object; + +/** + * @brief Represents an instance of a file-based provider. + */ +struct bootstrap_info_provider_file_instance { + uint32_t flags; + struct bootstrap_info_provider_file_settings *settings; + struct timespec time_modified; + struct json_object *jobj_bootstrap_info; + const char *ptr_base; + const char *ptr_dpp_uri; + const char *ptr_publickeyhash; +}; + +/** + * @brief Operations vector for file-based bootstrap info provider. + */ +extern struct bootstrap_info_provider_ops bootstrap_info_provider_file_ops; + +#endif // __BOOTSTRAP_INFO_PROVIDER_FILE_H__ diff --git a/src/core/biproviders/file/bootstrap_info_provider_file_config.c b/src/core/biproviders/file/bootstrap_info_provider_file_config.c new file mode 100644 index 0000000..dc95bbc --- /dev/null +++ b/src/core/biproviders/file/bootstrap_info_provider_file_config.c @@ -0,0 +1,215 @@ + +#include +#include +#include +#include + +#include "bootstrap_info_provider_file.h" +#include "bootstrap_info_provider_file_config.h" +#include "json_parse.h" +#include "ztp_log.h" + +/** + * @brief Get the context for the string properties of the "decodingInfo" object. + * + * @param context The parent context. Must be of type struct bootstrap_info_provider_file_settings. + * @param name The name of the child property to retrieve the context for. + * @return void* The context for the child property with key 'name'. + */ +static void * +bip_file_get_settings_string_context(void *context, const char *name) +{ + struct bootstrap_info_provider_file_settings *settings = (struct bootstrap_info_provider_file_settings *)context; + + if (strcmp(name, JSON_PROPERTY_NAME_PATH) == 0) { + return &settings->path; + } else if (strcmp(name, JSON_PROPERTY_NAME_DPP_URI) == 0) { + return &settings->json_key_dpp_uri; + } else if (strcmp(name, JSON_PROPERTY_NAME_PUBLIC_KEY_HASH) == 0) { + return &settings->json_key_publickeyhash; + } else if (strcmp(name, JSON_PROPERTY_NAME_JSON_PTR) == 0) { + return &settings->json_pointer_array; + } else if (strcmp(name, JSON_PROPERTY_NAME_JSON_PTR_BASE) == 0) { + return &settings->json_pointer_object_base; + } else { + return NULL; + } +} + +/** + * @brief Decoding info "propertyMap" configuration file options. + */ +static struct json_property_parser bip_file_configuration_property_map_properties[] = { + { + .name = JSON_PROPERTY_NAME_DPP_URI, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_file_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_PUBLIC_KEY_HASH, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_file_get_settings_string_context, + }, +}; + +/** + * @brief Parser for "propertyMap" property. + * + * @param parent The parent object. + * @param name Name of the json key. Must be "propertyMap". + * @param jobj The json object value. + * @param context The provider settings object. Must be of type struct + * bootstrap_info_provider_file_settings. + */ +static void +json_parse_decoding_info_property_map(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_parse_object_s(jobj, bip_file_configuration_property_map_properties, context); +} + +/** + * @brief "decodingInfo" configuration file options. + */ +static struct json_property_parser bip_file_configuration_decoding_properties[] = { + { + .name = JSON_PROPERTY_NAME_JSON_PTR, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_file_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_JSON_PTR_BASE, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_file_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_PROPERTY_MAP, + .type = json_type_object, + .value = { + json_parse_decoding_info_property_map, + }, + }, +}; + +/** + * @brief Parser for "decodingInfo" property. + * + * @param parent The parent object. + * @param name Name of the json key. Must be "decodingInfo". + * @param jobj The json object value. + * @param context The provider settings object. Must be of type struct + * bootstrap_info_provider_file_settings. + */ +static void +json_parse_decoding_info(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_parse_object_s(jobj, bip_file_configuration_decoding_properties, context); +} + +/** + * @brief Top-level configuration file options. + */ +static struct json_property_parser bip_file_configuration_properties[] = { + { + .name = JSON_PROPERTY_NAME_PATH, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = bip_file_get_settings_string_context, + }, + { + .name = JSON_PROPERTY_NAME_DECODING_INFO, + .type = json_type_object, + .value = { + json_parse_decoding_info, + }, + } +}; + +/** + * @brief Parses json configuration, writing it to the 'settings' object. + * + * @param jobj The json-c object with the configuration. + * @param settings The object to populate with settings. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +bootstrap_info_provider_file_config_parse_jobj(struct json_object *jobj, struct bootstrap_info_provider_file_settings *settings) +{ + json_parse_object_s(jobj, bip_file_configuration_properties, settings); + + // check for all required properties + if (!settings->path) { + zlog_error("missing required property 'path'"); + return -ENOENT; + } else if (!settings->json_pointer_array) { + zlog_error("missing required property 'pointer'"); + return -ENOENT; + } + + return 0; +} + +/** + * @brief Parses json configuration, writing it to the 'settings' object. + * + * @param json The json encoded configuration. + * @param settings The object to populate with settings. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +bootstrap_info_provider_file_config_parse(const char *json, struct bootstrap_info_provider_file_settings *settings) +{ + struct json_object *jobj = json_tokener_parse(json); + if (!jobj) { + zlog_error("invalid json found while parsing file-based bootstrap info provider"); + return -EINVAL; + } + + int ret = bootstrap_info_provider_file_config_parse_jobj(jobj, settings); + json_object_put(jobj); + + return ret; +} + +/** + * @brief Uninitializes a file provider settings object. releasing any resources. + * + * @param settings The settings object to uninitialize. + */ +void +bootstrap_info_provider_file_settings_uninitialize(struct bootstrap_info_provider_file_settings *settings) +{ + char **strs[] = { + &settings->path, + &settings->json_key_dpp_uri, + &settings->json_key_publickeyhash, + &settings->json_pointer_array, + &settings->json_pointer_object_base, + }; + + for (size_t i = 0; i < ARRAY_SIZE(strs); i++) { + if (*strs[i]) { + free(*strs[i]); + *strs[i] = NULL; + } + } +} diff --git a/src/core/biproviders/file/bootstrap_info_provider_file_config.h b/src/core/biproviders/file/bootstrap_info_provider_file_config.h new file mode 100644 index 0000000..f36d055 --- /dev/null +++ b/src/core/biproviders/file/bootstrap_info_provider_file_config.h @@ -0,0 +1,57 @@ + +#ifndef __BOOTSTRAP_INFO_PROVIDER_FILE_CONFIG_H__ +#define __BOOTSTRAP_INFO_PROVIDER_FILE_CONFIG_H__ + +#include + +/** + * @brief Macros for JSON configuration property names. + */ +#define JSON_PROPERTY_NAME_PATH "path" +#define JSON_PROPERTY_NAME_DPP_URI "dppUri" +#define JSON_PROPERTY_NAME_PUBLIC_KEY_HASH "publicKeyHash" +#define JSON_PROPERTY_NAME_JSON_PTR "pointer" +#define JSON_PROPERTY_NAME_JSON_PTR_BASE "pointerBase" +#define JSON_PROPERTY_NAME_PROPERTY_MAP "propertyMap" +#define JSON_PROPERTY_NAME_DECODING_INFO "decodingInfo" + +/** + * @brief File bootstrap info provider settings. + */ +struct bootstrap_info_provider_file_settings { + char *path; + char *json_pointer_array; + char *json_pointer_object_base; + char *json_key_dpp_uri; + char *json_key_publickeyhash; +}; + +/** + * @brief Parses json configuration, writing it to the 'settings' object. + * + * @param jobj The json-c object with the configuration. + * @param settings The object to populate with settings. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +bootstrap_info_provider_file_config_parse_jobj(struct json_object *jobj, struct bootstrap_info_provider_file_settings *settings); + +/** + * @brief Parses json configuration, writing it to the 'settings' object. + * + * @param json The json encoded configuration. + * @param settings The object to populate with settings. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +bootstrap_info_provider_file_config_parse(const char *json, struct bootstrap_info_provider_file_settings *settings); + +/** + * @brief Uninitializes a file provider settings object. releasing any resources. + * + * @param settings The settings object to uninitialize. + */ +void +bootstrap_info_provider_file_settings_uninitialize(struct bootstrap_info_provider_file_settings *settings); + +#endif //__BOOTSTRAP_INFO_PROVIDER_FILE_CONFIG_H__ diff --git a/src/core/ztp.c b/src/core/ztp.c new file mode 100644 index 0000000..bcf0ebb --- /dev/null +++ b/src/core/ztp.c @@ -0,0 +1,31 @@ + +#include + +#include "ztp.h" + +/** + * @brief Converts a ZTP connectivity state into a string. The returned string + * is a literal and must be duplicated in some way; the caller does not own it. + * + * @param state The state to convert. + * @return const char* A string representation of the state, or "??" if state + * is invalid or unknown. + */ +const char* +ztp_connectivity_state_str(enum ztp_connectivity_state state) +{ + switch (state) { + case ZTP_CONNECTIVITY_STATE_INITIALIZING: + return "initializing"; + case ZTP_CONNECTIVITY_STATE_INACTIVE: + return "inactive"; + case ZTP_CONNECTIVITY_STATE_UNPROVISIONED: + return "unprovisioned"; + case ZTP_CONNECTIVITY_STATE_PROVISIONED: + return "provisioned"; + case ZTP_CONNECTIVITY_STATE_CONNECTED: + return "connected"; + default: + return "invalid"; + } +} diff --git a/src/core/ztp.h b/src/core/ztp.h new file mode 100644 index 0000000..c6e1a2d --- /dev/null +++ b/src/core/ztp.h @@ -0,0 +1,55 @@ +#ifndef __ZTP_H__ +#define __ZTP_H__ + +/** + * @brief Describes the states of connectivity for an interface. + */ +enum ztp_connectivity_state { + + /** + * @brief Initializing state. This is the initial state which determines + * the next state based on persistent Wi-Fi configuration on the device. If + * any networks have been configured, then the device will enter into + * “Connecting” state. If no networks have been configured, then the device + * will enter into “Chirping” state to indicate provisioning is needed. + */ + ZTP_CONNECTIVITY_STATE_INITIALIZING = 0, + + /** + * @brief Inactive state. This occurs when ztp is disabled. + */ + ZTP_CONNECTIVITY_STATE_INACTIVE = 1, + + /** + * @brief Unprovisioned state. This occurs when the interface has no + * provisioned Wi-Fi networks. The device will attempt to acquire Wi-Fi + * network configuration using the Device Provisioning Protocol (DPP). + */ + ZTP_CONNECTIVITY_STATE_UNPROVISIONED = 2, + + /** + * @brief Provisioned state. In the connecting state, at least one Wi-Fi + * network has been provisioned and the device is attempting to find and + * connect to a network. + */ + ZTP_CONNECTIVITY_STATE_PROVISIONED = 3, + + /** + * @brief Connected state. In connected state, the device has connected to + * a network it has been provisioned. + */ + ZTP_CONNECTIVITY_STATE_CONNECTED = 4, +}; + +/** + * @brief Converts a ZTP connectivity state into a string. The returned string + * is a literal and must be duplicated in some way; the caller does not own it. + * + * @param state The state to convert. + * @return const char* A string representation of the state, or "??" if state + * is invalid or unknown. + */ +const char* +ztp_connectivity_state_str(enum ztp_connectivity_state state); + +#endif //__ZTP_H__ diff --git a/src/core/ztp_configurator.c b/src/core/ztp_configurator.c new file mode 100644 index 0000000..929a50b --- /dev/null +++ b/src/core/ztp_configurator.c @@ -0,0 +1,577 @@ + +#include +#include +#include +#include +#include + +#include "bootstrap_info_provider_private.h" +#include "bootstrap_info_provider_settings.h" +#include "dpp.h" +#include "event_loop.h" +#include "string_utils.h" +#include "wpa_controller.h" +#include "wpa_controller_watcher.h" +#include "wpa_core.h" +#include "ztp_configurator.h" +#include "ztp_configurator_config.h" +#include "ztp_log.h" + +/** + * @brief Looks up bootstrap info given a public key "chirp" hash (distinct + * from the standard cryptographic hash). + * + * @param configurator The configurator instance. + * @param record The output pointer to write the record to. If bootstrap info + * was found, this will be non-NULL and and must be freed using + * bootstrap_info_record_destroy(). Otherwise, this will be NULL. + * @return int 0 if the find operation was successful, non-zero otherwise. + */ +static int +ztp_configurator_bootstrap_info_find(struct ztp_configurator *configurator, const struct dpp_bootstrap_publickey_hash *hash, struct bootstrap_info_record **record) +{ + struct bootstrap_info_query query; + struct bootstrap_info_query_result result; + + bootstrap_info_query_initialize(&query, hash); + bootstrap_info_query_result_initialize(&result); + + *record = NULL; + + struct bootstrap_info_provider *provider; + list_for_each_entry (provider, &configurator->bootstrap_info_providers, list) { + int ret = bootstrap_info_provider_query(provider, &query, &result); + if (ret != 0 || list_empty(&result.records)) + continue; + + struct bootstrap_info_record_result_entry *entry; + entry = list_first_entry(&result.records, struct bootstrap_info_record_result_entry, list); + list_del_init(&entry->list); + *record = &entry->record; + break; + } + + bootstrap_info_query_result_uninitialize(&result); + + return 0; +} + +/** + * @brief Returns the network configuration for the enrollee with the specified (chirp) hash. + * + * @param configurator The configurator instance. + * @param hash The chirp hash of the enrollee to find the network configuration for. + * @return struct dpp_network* A pointer to the network the enrollee should be + * provisioned for, if it exists. NULL if no network is configured. + */ +static struct dpp_network * +ztp_configurator_get_network_config(struct ztp_configurator *configurator, const struct dpp_bootstrap_publickey_hash *hash) +{ + __unused(hash); + + // Per-enrollee networks are not currently supported. Return the default network. + return configurator->settings->network_config_default; +} + +/** + * @brief Sets the default network to provision enrollees, if none has been specified. + * + * @param configurator The configurator to set the default network to provision for. + * @param network The default network to provision enrollees with. + * @return int 0 if the specified network will be provisioned as a default, non-zero otherwise. + */ +static int +ztp_configurator_set_default_network(struct ztp_configurator *configurator, struct dpp_network *network) +{ + char params[WPA_CONFIGURATOR_PARAMS_MAX_LENGTH]; + size_t params_length = sizeof params; + + int ret = dpp_network_to_wpa_configurator_params(network, params, ¶ms_length, DPP_NETWORK_ROLE_STATION); + if (ret < 0) { + zlog_error_if(configurator->interface, "failed to translate network configuration to wpa configurator params (%d)", ret); + return ret; + } + + ret = wpa_controller_set(configurator->ctrl, WPA_CFG_PROPERTY_DPP_CONFIGURATOR_PARAMS, params); + if (ret < 0) { + zlog_error_if(configurator->interface, "failed to set '" WPA_CFG_PROPERTY_DPP_CONFIGURATOR_PARAMS "' value with default network (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Registers an enrollee with the (hostapd) configurator. This includes + * providing its DPP URI (qrcode) and associating the target network + * provisioning information with the enrollee. + * + * @param configurator The configurator instance. + * @param hash The chirp has of the enrollee to register. + * @param record The bootstrap info record for the enrollee. + * @param peer_id The bootstrap identifier of the enrollee (peer) in hostapd. + * @return int 0 if the enrollee was successfully registered with hostapd. In + * this case, it should be provisioned the next time it is seen by the + * configurator. + */ +static int +ztp_configurator_register_enrollee(struct ztp_configurator *configurator, const struct dpp_bootstrap_publickey_hash *hash, struct bootstrap_info_record *record, uint32_t frequency, uint32_t *peer_id) +{ + __unused(frequency); + + int ret = wpa_controller_qrcode(configurator->ctrl, record->dpp_uri, peer_id); + if (ret < 0) { + zlog_error_if(configurator->interface, "unable to register qrcode for " DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_FMT " (%d)", + DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_TOSTRING(hash->data), ret); + return ret; + } + + struct dpp_network *network = ztp_configurator_get_network_config(configurator, hash); + if (!network) { + zlog_error_if(configurator->interface, "failed to get network configuration for " DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_FMT, + DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_TOSTRING(hash->data)); + return -ENOENT; + } + + char conf[WPA_CONFIGURATOR_PARAMS_MAX_LENGTH]; + size_t conf_length = sizeof conf; + + ret = dpp_network_to_wpa_configurator_params(network, conf, &conf_length, DPP_NETWORK_ROLE_STATION); + if (ret < 0) { + zlog_error_if(configurator->interface, "failed to translate network configuration to wpa configurator params for " DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_FMT " (%d)", + DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_TOSTRING(hash->data), ret); + return ret; + } + + ret = wpa_controller_dpp_bootstrap_set(configurator->ctrl, *peer_id, conf); + if (ret < 0) { + zlog_error_if(configurator->interface, "failed to associate wpa configurator params (network info) for " DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_FMT " (%d)", + DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_TOSTRING(hash->data), ret); + return ret; + } + +#if CONFIG_EXPLICIT_AUTH_ON_CHIRP + ret = wpa_controller_dpp_auth_init_with_conf(configurator->ctrl, *peer_id, frequency, conf); + if (ret < 0) { + zlog_warning_if(configurator->interface, "failed to initiate dpp auth for " DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_FMT " (%d); auth will be processed on next chirp rx", + DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_TOSTRING(hash->data), ret); + } +#endif // CONFIG_EXPLICIT_AUTH_ON_CHIRP + + return 0; +} + +/** + * @brief Initialize hostapd with any state that it needs to begin operating. + * + * @param configurator The configurator instance. + * @return int 0 if hostapd was successfully initialized with operating state, non-zero otherwise. + */ +static int +ztp_configurator_hostapd_initialize_state(struct ztp_configurator *configurator) +{ + if (configurator->settings->network_config_default) { + int ret = ztp_configurator_set_default_network(configurator, configurator->settings->network_config_default); + if (ret < 0) { + zlog_error_if(configurator->interface, "failed to set default provisioning network (%d)", ret); + return ret; + } + } + + zlog_debug_if(configurator->interface, "hostapd state initialized"); + return 0; +} + +/** + * @brief Resets any internal state related to the hostapd control interface. + * + * @param configurator The configurator instance. + */ +static void +ztp_configurator_hostapd_reset_state(struct ztp_configurator *configurator) +{ + zlog_debug_if(configurator->interface, "hostapd state reset"); +} + +/** + * @brief Control socket presence event handler. + * + * @param context The configurator instance. + * @param event The event that occurred. + */ +static void +on_control_interface_presence_changed(void *context, enum wpa_controller_presence_event event) +{ + struct ztp_configurator *configurator = (struct ztp_configurator *)context; + + switch (event) { + case WPA_CONTROLLER_ARRIVED: { + int ret = ztp_configurator_hostapd_initialize_state(configurator); + if (ret < 0) + zlog_error_if(configurator->interface, "failed to initialize hostapd state (%d)", ret); + break; + } + case WPA_CONTROLLER_DEPARTED: + ztp_configurator_hostapd_reset_state(configurator); + break; + default: + break; + } +} + +/** + * @brief Chirp received event handler function. This will attempt to lookup + * any associated bootstrapping information, and subsequently attempt to + * initiate a DPP provisioning exchange with the identified peer. + * + * @param userdata The configurator instance. + * @param mac The mac address of the source of the chirp. + * @param hash The "chirp" hash of the peer that wishes to be provisioned. + */ +static void +on_chirp_received(void *userdata, int32_t id, const char (*mac)[(DPP_MAC_LENGTH * 2) + (DPP_MAC_LENGTH - 1) + 1], uint32_t frequency, const struct dpp_bootstrap_publickey_hash *hash) +{ + __unused(mac); + + struct bootstrap_info_record *record; + struct ztp_configurator *configurator = (struct ztp_configurator *)userdata; + + char hashstr[(DPP_BOOTSTRAP_PUBKEY_HASH_LENGTH * 2) + 1]; + hex_encode(hash->data, sizeof hash->data, hashstr, sizeof hashstr); + hashstr[sizeof hashstr - 1] = '\0'; + + int ret = ztp_configurator_bootstrap_info_find(configurator, hash, &record); + if (ret < 0) { + zlog_error_if(configurator->interface, "chirp hash query failed (%d)", ret); + return; + } else if (!record) { + zlog_debug_if(configurator->interface, "bootstrap info not found for %.7s", hashstr); + return; + } + + zlog_info_if(configurator->interface, "bootstrap info found, %.7s -> %s", hashstr, record->dpp_uri); + + // If the peer is not known to hostapd (bootstrap id == -1), register its DPP URI. + uint32_t peer_id = 0; + if (id == -1) { + ret = ztp_configurator_register_enrollee(configurator, hash, record, frequency, &peer_id); + if (ret < 0) { + zlog_error_if(configurator->interface, "unable to register enrollee (%d)", ret); + return; + } + } else { + assert(id > 0); + peer_id = (uint32_t)id; + } +} + +/** + * @brief Synchronize a single bootstrap info provider. + * + * @param provider The provider to synchronize. + * @param options Options controlling how synchronization should be performed. + * @return int 0 if the synchronization completed successfully, non-zero otherwise. + */ +static int +ztp_configurator_bootstrap_info_provider_synchronize(struct bootstrap_info_provider *provider, const struct bootstrap_info_sync_options *options) +{ + zlog_debug("[%s] +synchronize", provider->name); + + int ret = bootstrap_info_provider_synchronize(provider, options); + if (ret < 0) { + zlog_debug("[%s] bootstrap info provider synchronize() failed (%d)", provider->name, ret); + goto out; + } + +out: + zlog_debug("[%s] -synchronize (%d)", provider->name, ret); + return ret; +} + +/** + * @brief Synchronizes the bootstrapping info for the configurator. This will + * request each registered bootstrap information provider synchronize its view + * immediately. + * + * @param configurator The configurator instance to synchronize. + * @param options Options controlling how synchronization should be performed. + * @return int The number of providers that successfully synchronized, or + * non-zero if synchronization could not be attempted for any of the providers. + */ +int +ztp_configurator_synchronize_bootstrapping_info(struct ztp_configurator *configurator, const struct bootstrap_info_sync_options *options) +{ + int num_succeeded = 0; + struct bootstrap_info_provider *provider; + + list_for_each_entry (provider, &configurator->bootstrap_info_providers, list) { + if (ztp_configurator_bootstrap_info_provider_synchronize(provider, options) == 0) + num_succeeded++; + } + + return num_succeeded; +} + +/** + * @brief Handler for bootstrap info provider synchronization timer timeouts. + * The timeout expires when the next synchronization must be performed, so this + * function invokes the provider's synchronize function. + * @param context The handler context. Must be of type struct + * bootstrap_info_provider, which is an instance of the provider whose sync + * timer expired. + */ +static void +ztp_configurator_on_provider_synchronize_timeout(void *context) +{ + struct bootstrap_info_provider *provider = (struct bootstrap_info_provider *)context; + zlog_debug("[%s] sync time expired", provider->name); + + ztp_configurator_bootstrap_info_provider_synchronize(provider, NULL); +} + +/** + * @brief Cancels periodic synchronization for the specified provider. + * + * @param configurator The configurator instance. + * @param provider The provider to cancel synchronization for. + */ +static void +ztp_configurator_bootstrap_info_provider_cancel_synchronization(struct ztp_configurator *configurator, struct bootstrap_info_provider *provider) +{ + event_loop_task_cancel(configurator->loop, ztp_configurator_on_provider_synchronize_timeout, provider); +} + +/** + * @brief Schedules a timer to perform synchronization according to the providers expiry time. + * + * @param configurator The configurator instance. + * @param provider The provider to schedule for synchronization. + * @return int 0 if synchronization was successfully scheduled, non-zero otherwise. + */ +static int +ztp_configurator_bootstrap_info_provider_schedule_synchronization(struct ztp_configurator *configurator, struct bootstrap_info_provider *provider) +{ + ztp_configurator_bootstrap_info_provider_cancel_synchronization(configurator, provider); + + if (provider->settings->expiration_time == 0) { + zlog_debug_if(configurator->interface, "[%s] expiration time not set (0), omitting sync timer", provider->name); + return 0; + } + + zlog_debug_if(configurator->interface, "[%s] scheduling sync timer for %us", provider->name, provider->settings->expiration_time); + + int ret = event_loop_task_schedule(configurator->loop, provider->settings->expiration_time, 0, TASK_PERIODIC, + ztp_configurator_on_provider_synchronize_timeout, provider); + if (ret < 0) { + zlog_error_if(configurator->interface, "[%s] failed to schedule synchronization timer (%d)", provider->name, ret); + return ret; + } + + return 0; +} + +/** + * @brief Start a bootstrap info provider. + * + * @param provider The provider to start. + * @return int 0 if the provider was started, non-zero otherwise. + */ +static int +ztp_configurator_bootstrap_info_provider_start(struct ztp_configurator *configurator, struct bootstrap_info_provider *provider) +{ + int ret = ztp_configurator_bootstrap_info_provider_schedule_synchronization(configurator, provider); + if (ret < 0) + return ret; + + ret = provider->ops->initialize(provider->settings, &provider->context); + if (ret < 0) { + zlog_warning_if(configurator->interface, "[%s] failed to start bootstrap info provider (%d)", provider->name, ret); + ztp_configurator_bootstrap_info_provider_cancel_synchronization(configurator, provider); + return ret; + } + + provider->started = true; + + if (event_loop_task_schedule_now(configurator->loop, ztp_configurator_on_provider_synchronize_timeout, provider) < 0) + zlog_warning_if(configurator->interface, "[%s] initial synchronization failed", provider->name); + + return 0; +} + +/** + * @brief Adds an existing, initialized bootstrap info provider to the configurator. + * + * @param configurator The configurator instance. + * @param provider The provider to add. + */ +static void +ztp_configurator_bootstrap_info_provider_add(struct ztp_configurator *configurator, struct bootstrap_info_provider *provider) +{ + list_add(&provider->list, &configurator->bootstrap_info_providers); + zlog_info_if(configurator->interface, "[%s] bootstrap info provider with type '%s' added", provider->name, bootstrap_info_provider_type_str(provider->type)); + ztp_configurator_bootstrap_info_provider_start(configurator, provider); +} + +/** + * @brief Uninitializes a configurator instance. + * + * @param configurator The configurator to uninitialize. + */ +static void +ztp_configurator_uninitialize(struct ztp_configurator *configurator) +{ + struct bootstrap_info_provider *provider; + struct bootstrap_info_provider *providertmp; + + list_for_each_entry_safe (provider, providertmp, &configurator->bootstrap_info_providers, list) { + ztp_configurator_bootstrap_info_provider_cancel_synchronization(configurator, provider); + bootstrap_info_provider_destroy(provider); + } + + if (configurator->ctrl) { + wpa_controller_uninitialize(configurator->ctrl); + wpa_controller_destroy(&configurator->ctrl); + } + + if (configurator->interface) { + free(configurator->interface); + configurator->interface = NULL; + } +} + +/** + * @brief Event handler for wpa control socket events. + */ +static struct wpa_event_handler wpa_event_handler_configurator = { + .interface_presence_changed = on_control_interface_presence_changed, + .dpp_chirp_received = on_chirp_received, +}; + +/** + * @brief The default control socket path to use if none is specified. + */ +#define WPA_CTRL_PATH_BASE_DEFAULT "/var/run/hostapd" + +/** + * @brief Initializes a ztpd configurator instance. + * + * @param configurator The configurator instance to initialize. + * @param interface The name of the interface the configurator is running on. + * @param settings The settings to use to initialize the instance. + * @param loop The main ztpd event loop. + * @return int 0 if the configurator was successfully initialized, otherwise + * non-zero. + */ +static int +ztp_configurator_initialize(struct ztp_configurator *configurator, const char *interface, struct ztp_configurator_settings *settings, struct event_loop *loop) +{ + int ret; + + INIT_LIST_HEAD(&configurator->bootstrap_info_providers); + + configurator->ctrl = wpa_controller_alloc(); + if (!configurator->ctrl) { + ret = -ENOMEM; + zlog_error_if(interface, "failed to allocate wpa controller (%d)", ret); + goto fail; + } + + char ctrl_path[128]; + snprintf(ctrl_path, sizeof ctrl_path, "%s/%s", WPA_CTRL_PATH_BASE_DEFAULT, interface); + + ret = wpa_controller_initialize(configurator->ctrl, ctrl_path, loop); + if (ret < 0) { + zlog_error_if(interface, "failed to initialize wpa controller (%d)", ret); + goto fail; + } + + configurator->loop = loop; + configurator->settings = settings; + configurator->interface = strdup(interface); + + if (!configurator->interface) { + zlog_error(configurator->interface, "failed to allocate memory for interface name"); + ret = -ENOMEM; + goto fail; + } + + struct bootstrap_info_provider_settings *bisettings; + list_for_each_entry (bisettings, &settings->provider_settings, list) { + struct bootstrap_info_provider *provider = bootstrap_info_provider_create(bisettings); + if (provider) + ztp_configurator_bootstrap_info_provider_add(configurator, provider); + } + + ret = wpa_controller_register_event_handler(configurator->ctrl, &wpa_event_handler_configurator, configurator); + if (ret < 0) { + zlog_error(interface, "failed to register wpa event handler (%d)", ret); + goto fail; + } + + ret = 0; + +out: + return ret; +fail: + ztp_configurator_uninitialize(configurator); + goto out; +} + +/** + * @brief Creates and initializes a new configurator instance. + * + * @param interface The name of the interface to run on. + * @param settings The settings to initialize the configurator with. + * @param loop The ztpd event loop to run the configurator on. + * @param pconfigurator Output pointer to accept the configurator instance. + * @return int + */ +int +ztp_configurator_create(const char *interface, struct ztp_configurator_settings *settings, struct event_loop *loop, struct ztp_configurator **pconfigurator) +{ + int ret; + struct ztp_configurator *configurator = calloc(1, sizeof *configurator); + if (!configurator) { + zlog_error_if(interface, "failed to allocate memory to create configurator"); + ret = -ENOMEM; + goto fail; + } + + configurator->interface = strdup(interface); + if (!configurator->interface) { + zlog_error_if(interface, "failed to allocate memory for interface name"); + ret = -ENOMEM; + goto fail; + } + + ret = ztp_configurator_initialize(configurator, interface, settings, loop); + if (ret < 0) { + zlog_error_if(interface, "failed to initialize configurator (%d)", ret); + goto fail; + } + + *pconfigurator = configurator; +out: + return ret; +fail: + if (configurator) + ztp_configurator_destroy(&configurator); + goto out; +} + +/** + * @brief Destroys a configurator, freeing all owned resources. + * + * @param configurator The configurator to destroy. + */ +void +ztp_configurator_destroy(struct ztp_configurator **configurator) +{ + if (!configurator || !*configurator) + return; + + ztp_configurator_uninitialize(*configurator); + free(*configurator); + + *configurator = NULL; +} diff --git a/src/core/ztp_configurator.h b/src/core/ztp_configurator.h new file mode 100644 index 0000000..0f82ee3 --- /dev/null +++ b/src/core/ztp_configurator.h @@ -0,0 +1,59 @@ + +#ifndef __ZTP_CONFIGURATOR_H__ +#define __ZTP_CONFIGURATOR_H__ + +#include +#include +#include + +#include "bootstrap_info_provider.h" + +struct wpa_controller; +struct event_loop; +struct ztp_configurator_settings; + +/** + * @brief DPP configurator. + */ +struct ztp_configurator { + struct list_head bootstrap_info_providers; + struct ztp_configurator_settings *settings; + struct event_loop *loop; + struct wpa_controller *ctrl; + char *interface; +}; + +/** + * @brief Creates and initializes a new configurator instance. + * + * @param interface The name of the interface to run on. + * @param settings The settings to initialize the configurator with. + * @param loop The ztpd event loop to run the configurator on. + * @param pconfigurator Output pointer to accept the configurator instance. + * @return int + */ +int +ztp_configurator_create(const char *interface, struct ztp_configurator_settings *settings, struct event_loop *loop, struct ztp_configurator **pconfigurator); + +/** + * @brief Destroys a configurator, freeing all owned resources. + * + * @param configurator The configurator to desttroy. + */ +void +ztp_configurator_destroy(struct ztp_configurator **configurator); + +/** + * @brief Synchronizes the bootstrapping info for the configurator. This will + * request each registered bootstrap information provider synchronize its view + * immediately. + * + * @param configurator The configurator instance to synchronize. + * @param options Options controlling how synchronization should be performed. + * @return int The number of providers that successfully synchronized, or + * non-zero if synchronization could not be attempted for any of the providers. + */ +int +ztp_configurator_synchronize_bootstrapping_info(struct ztp_configurator *configurator, const struct bootstrap_info_sync_options *options); + +#endif //__ZTP_CONFIGURATOR_H__ diff --git a/src/core/ztp_configurator_config.c b/src/core/ztp_configurator_config.c new file mode 100644 index 0000000..b5dbbfb --- /dev/null +++ b/src/core/ztp_configurator_config.c @@ -0,0 +1,1701 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps_config.h" +#include "bootstrap_info_provider_file_config.h" +#include "bootstrap_info_provider_settings.h" +#include "file_utils.h" +#include "json_parse.h" +#include "string_utils.h" +#include "ztp_configurator.h" +#include "ztp_configurator_config.h" +#include "ztp_log.h" + +/** + * @brief Json object key names for bootstrap info. + */ +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO "bootstrap.info" +#define JSON_PROPERTY_NAME_BI_EXPIRATION_TIME "expirationTime" +#define JSON_PROPERTY_NAME_BI_PROVIDERS "providers" +#define JSON_PROPERTY_NAME_BI_COMMON_TYPE "type" +#define JSON_PROPERTY_NAME_BI_COMMON_NAME "name" +#define JSON_PROPERTY_NAME_BI_COMMON_EXPIRATION_TIME "expirationTime" + +/** + * @brief Json object key names for network configuration. + */ +#define JSON_PROPERTY_NAME_NET_CFG "network.configuration" +#define JSON_PROPERTY_NAME_NET_CFG_DEFAULT "default" +#define JSON_PROPERTY_NAME_NET_CFG_DISCOVERY "discovery" +#define JSON_PROPERTY_NAME_NET_CFG_DISCOVERY_SSID "ssid" +#define JSON_PROPERTY_NAME_NET_CFG_DISCOVERY_SSID_CHARSET "ssidCharset" +#define JSON_PROPERTY_NAME_NET_CFG_CREDENTIALS "credentials" +#define JSON_PROPERTY_NAME_CREDENTIAL_AKM "akm" +#define JSON_PROPERTY_NAME_CREDENTIAL_PSK "psk" +#define JSON_PROPERTY_NAME_CREDENTIAL_PASSPHRASE "passphrase" + +/** + * @brief Parser for the global "expirationTime" property for the "bootstrap.info" object. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "expirationTime". + * @param jobj The json property value. Must be of type 'json_type_int'. + * @param context The configurator settings object instance. + */ +static void +json_parse_expiration_time(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct ztp_configurator_settings *settings = (struct ztp_configurator_settings *)context; + + int32_t expiration_time = json_object_get_int(jobj); + if (expiration_time <= 0) { + zlog_warning("expirationTime must be > 0"); + return; + } + + settings->expiration_time = (uint32_t)expiration_time; +} + +/** + * @brief Parser for the "type" property of a bootstrapping information + * provider + * + * @param parent The parent object. + * @param name The name of the json property. Must be "type". + * @param jobj The json property value. Must be of type json_type_string. + * @param context The provider settings object instance. + */ +static void +json_parse_provider_type(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct bootstrap_info_provider_settings *settings = (struct bootstrap_info_provider_settings *)context; + + const char *value = json_object_get_string(jobj); + enum bootstrap_info_provider_type type = parse_bootstrap_info_provider_type(value); + if (type == BOOTSTRAP_INFO_PROVIDER_INVALID) { + zlog_warning("unrecognized bootstrap info provider type '%s', ignoring", value); + return; + } + + settings->type = type; +} + +/** + * @brief Parser for the "name" property of a bootstrapping information + * provider + * + * @param parent The parent object. + * @param name The name of the json property. Must be "name". + * @param jobj The json property value. Must be of type json_type_string. + * @param context The provider settings object instance. + */ +static void +json_parse_provider_name(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct bootstrap_info_provider_settings *settings = (struct bootstrap_info_provider_settings *)context; + + char *value = strdup(json_object_get_string(jobj)); + if (!value) { + zlog_warning("allocation failure for bootstrap info provider name"); + return; + } + + if (settings->name) + free(settings->name); + settings->name = value; +} + +/** + * @brief Parser for the "expirationTime" property of a bootstrapping + * information provider. + * + * @param parent The parent object. + * @param name The name of the json property. Must ne "expirationTime". + * @param jobj The json value. Must be of type 'json_type_int'. + * @param context The provider settings object instance. + */ +static void +json_parse_provider_expiration_time(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct bootstrap_info_provider_settings *settings = (struct bootstrap_info_provider_settings *)context; + + int32_t expiration_time = json_object_get_int(jobj); + if (expiration_time <= 0) { + zlog_warning("expirationTime must be > 0"); + return; + } + + settings->expiration_time = (uint32_t)expiration_time; +} + +/** + * @brief Configurator 'providers' entry configuration options. This describes + * the shared/common properties. All provider-specific properties are ignored. + */ +static struct json_property_parser configurator_config_provider_properties[] = { + { + .name = JSON_PROPERTY_NAME_BI_COMMON_TYPE, + .type = json_type_string, + .value = { + json_parse_provider_type, + }, + }, + { + .name = JSON_PROPERTY_NAME_BI_COMMON_NAME, + .type = json_type_string, + .value = { + json_parse_provider_name, + }, + }, + { + .name = JSON_PROPERTY_NAME_BI_EXPIRATION_TIME, + .type = json_type_int, + .value = { + json_parse_provider_expiration_time, + }, + } +}; + +/** + * @brief Parses a "providers" property value describing bootstrapping information provider settings. + * + * @param parent The parent object. + * @param array The containing array object. + * @param name The name of the parent json property. Must be "providers". + * @param jobj The json value for the array entry. + * @param index The index of the json value. + * @param type The type of the json value. Must be json_type_object. + * @param context The configurator settings object instance. + */ +static void +json_parse_providers(struct json_object *parent, struct json_object *array, const char *name, struct json_object *jobj, uint32_t index, json_type type, void *context) +{ + __unused(parent); + __unused(array); + __unused(name); + __unused(index); + __unused(type); + + struct ztp_configurator_settings *settings = (struct ztp_configurator_settings *)context; + + struct bootstrap_info_provider_settings *provider = bootstrap_info_provider_settings_alloc(); + if (!provider) { + zlog_warning("allocation failure for bootstrap info provider"); + return; + } + + json_parse_object_s(jobj, configurator_config_provider_properties, provider); + + if (provider->type == BOOTSTRAP_INFO_PROVIDER_INVALID || + provider->name == NULL || + provider->name[0] == '\0') { + zlog_warning("invalid bootstrap info provider found, ignoring"); + goto fail; + } + + switch (provider->type) { + case BOOTSTRAP_INFO_PROVIDER_FILE: { + struct bootstrap_info_provider_file_settings *file = calloc(1, sizeof *file); + if (!file) { + zlog_error("failed to allocate memory for file-based bootstrap info provider settings"); + goto fail; + } + + int ret = bootstrap_info_provider_file_config_parse_jobj(jobj, file); + if (ret < 0) { + zlog_warning("failed to parse file-based bootstrap info provider settings (%d)", ret); + free(file); + goto fail; + } + + provider->file = file; + break; + } + case BOOTSTRAP_INFO_PROVIDER_AZUREDPS: { + struct bootstrap_info_provider_azure_dps_settings *dps = calloc(1, sizeof *dps); + if (!dps) { + zlog_error("failed to allocate memory for azuredps-based bootstrap info provider settings"); + goto fail; + } + + int ret = bootstrap_info_provider_azure_dps_config_parse_jobj(jobj, dps); + if (ret < 0) { + zlog_warning("failed to parse azuredps-based bootstrap info provider settings (%d)", ret); + free(dps); + goto fail; + } + + provider->dps = dps; + break; + } + default: + break; + } + + ztp_configurator_settings_add_bi_provider_settings(settings, provider); + return; + +fail: + bootstrap_info_provider_settings_uninitialize(provider); + free(provider); +} + +/** + * @brief Configurator top-level configuration file options. + */ +static struct json_property_parser configurator_bootstrap_info_properties[] = { + { + .name = JSON_PROPERTY_NAME_BI_EXPIRATION_TIME, + .type = json_type_int, + .value = { + json_parse_expiration_time, + }, + }, + { + .name = JSON_PROPERTY_NAME_BI_PROVIDERS, + .type = json_type_array, + .array = { + json_parse_providers, + json_type_object, + }, + }, +}; + +/** + * @brief Parser for the "bootstrap.info" configuration option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "bootstrap.info". + * @param jobj The json value of the bootstrap info property. + * @param context The configurator settings object instance. + */ +static void +json_parse_bootstrap_info(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_parse_object_s(jobj, configurator_bootstrap_info_properties, context); +} + +/** + * @brief Encodes bootstrap info provider decoding info property map as json. + * + * @param file The bootstrap file info provider settings. + * @param jpropertymap Output argument that will hold the property map object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int + */ +static int +json_encode_bootstrap_info_provider_file_decodinginfo_propertymap(const struct bootstrap_info_provider_file_settings *file, struct json_object **jpropertymap) +{ + int ret; + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate new json object for file bootstrap info provider decoding info property map"); + return -ENOMEM; + } + + struct json_object *dpp_uri = json_object_new_string(file->json_key_dpp_uri); + if (!dpp_uri) { + zlog_error("failed to encode json dpp uri for file bootstrap info provider decoding info property map"); + json_object_put(jobj); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_DPP_URI, dpp_uri); + if (ret < 0) { + zlog_error("failed to add dpp uri to file bootstrap info provider decodoing info property map (%d)", ret); + json_object_put(dpp_uri); + goto fail; + } + + struct json_object *pkhash = json_object_new_string(file->json_key_publickeyhash); + if (!pkhash) { + zlog_error("failed to encode json public key hash for file bootstrap info provider decoding info property map"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_PUBLIC_KEY_HASH, pkhash); + if (ret < 0) { + zlog_error("failed to add public key hash to file bootstrap info provider decodoing info property map (%d)", ret); + json_object_put(pkhash); + goto fail; + } + + *jpropertymap = jobj; +out: + return ret; +fail: + if (jobj) + json_object_put(jobj); + goto out; +} + +/** + * @brief Encodes bootstrap file info provider decoding info as a json object. + * + * @param file The bootstrap file info provider settings. + * @param jdecoding_info Output argument that will hold the decoding info object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int 0 if encoding was successful, non-zero otherwise. + */ +static int +json_encode_bootstrap_info_provider_file_decodinginfo(const struct bootstrap_info_provider_file_settings *file, struct json_object **jdecoding_info) +{ + int ret; + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate new json object for file bootstrap info provider decoding info"); + return -ENOMEM; + } + + struct json_object *pointer_array = json_object_new_string(file->json_pointer_array); + if (!pointer_array) { + zlog_error("failed to encode json pointer array for file bootstrap info provider decoding info"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_JSON_PTR, pointer_array); + if (ret < 0) { + zlog_error("failed to add pointer array to file bootstrap info provider decodoing info json (%d)", ret); + goto fail; + } + + struct json_object *pointer_base = json_object_new_string(file->json_pointer_object_base); + if (!pointer_base) { + zlog_error("failed to encode json pointer base for file bootstrap info provider decoding info"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_JSON_PTR_BASE, pointer_base); + if (ret < 0) { + zlog_error("failed to add pointer base to file bootstrap info provider decoding info json (%d)", ret); + goto fail; + } + + struct json_object *property_map; + ret = json_encode_bootstrap_info_provider_file_decodinginfo_propertymap(file, &property_map); + if (ret < 0) { + zlog_error("failed to encode file bootstrap info provider decoding info property map (%d)", ret); + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_PROPERTY_MAP, property_map); + if (ret < 0) { + zlog_error("failed to add property map to file bootstrap info provider decoding info (%d)", ret); + json_object_put(property_map); + goto fail; + } + + *jdecoding_info = jobj; + +out: + return ret; +fail: + if (jobj) + json_object_put(jobj); + goto out; +} + +/** + * @brief Encodes file-base bootstrap info provider settings to json. + * + * @param file The file bootstrap info provider settings to encode. + * @param jobj The json object to append the settings to. + * @return int 0 if all settings were appended successfully, non-zero otherwise. + */ +static int +json_encode_bootstrap_info_provider_file(const struct bootstrap_info_provider_file_settings *file, struct json_object *jobj) +{ + struct json_object *path = json_object_new_string(file->path); + if (!path) { + zlog_error("failed to encode file bootstrap info provider name to json"); + return -EBADMSG; + } + + int ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_PATH, path); + if (ret < 0) { + zlog_error("failed to add path to file bootstrap info provider json (%d)", ret); + json_object_put(path); + return ret; + } + + struct json_object *decoding_info; + ret = json_encode_bootstrap_info_provider_file_decodinginfo(file, &decoding_info); + if (ret < 0) { + zlog_error("failed to encode file bootstrap info provider decoding info (%d)", ret); + return ret; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_DECODING_INFO, decoding_info); + if (ret < 0) { + zlog_error("failed to add decoding info to file bootstrap info provider (%d)", ret); + json_object_put(decoding_info); + return ret; + } + + return 0; +} + +/** + * @brief Encodes azure dps bootstrap info provider authentication info as json. + * + * @param dps The azure dps bootstrap info provider settings. + * @param jauth Output argument that will hold the authentication info object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int 0 if encoding succeeded, non-zero otherwise. + */ +static int +json_encode_bootstrap_info_provider_azuredps_auth(const struct bootstrap_info_provider_azure_dps_settings *dps, struct json_object **jauth) +{ + int ret; + const struct { + const char *name; + const char *value; + } props[] = { + { JSON_PROPERTY_NAME_AUTHORITY_URL, dps->authority_url }, + { JSON_PROPERTY_NAME_CLIENT_ID, dps->client_id }, + { JSON_PROPERTY_NAME_CLIENT_SECRET, dps->client_secret }, + { JSON_PROPERTY_NAME_RESOURCE_URI, dps->resource_uri }, + { JSON_PROPERTY_NAME_CONNECTION_STRING, dps->connection_string }, + }; + + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate memory for azure dps bootstrap info provider auth json"); + ret = -ENOMEM; + goto fail; + } + + for (size_t i = 0; i < ARRAY_SIZE(props); i++) { + struct json_object *value = json_object_new_string(props[i].value); + if (!value) { + zlog_error("failed to encode azure dps bootstrap info provider %s as json", props[i].name); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, props[i].name, value); + if (ret < 0) { + zlog_error("failed to add azure dps bootstrap info provider %s", props[i].name); + json_object_put(value); + goto fail; + } + } + + *jauth = jobj; + ret = 0; +out: + return ret; +fail: + if (jobj) + json_object_put(jobj); + goto out; +} + +/** + * @brief Encodes azure dps bootstrap info provider settings as json. + * + * @param dps The azure dps bootstrap info provider settings. + * @param jobj The json object to append to. + * @return int 0 if encoding succeeded, non-zero otherwise. + */ +static int +json_encode_bootstrap_info_provider_azuredps(const struct bootstrap_info_provider_azure_dps_settings *dps, struct json_object *jobj) +{ + json_object *service_endpoint_uri = json_object_new_string(dps->service_endpoint_uri); + if (!service_endpoint_uri) { + zlog_error("failed to encode azure dps bootstrap info provider service endpoint uri"); + return -EBADMSG; + } + + int ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_SERVICE_ENDPOINT_URI, service_endpoint_uri); + if (ret < 0) { + zlog_error("failed to add azure dps bootstrap info provider service endpoint uri to json (%d)", ret); + json_object_put(service_endpoint_uri); + return ret; + } + + struct json_object *auth; + ret = json_encode_bootstrap_info_provider_azuredps_auth(dps, &auth); + if (ret < 0) { + zlog_error("failed to encode azure dps bootstrap info provider auth (%d)", ret); + return ret; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_AUTHENTICATION, auth); + if (ret < 0) { + zlog_error("failed to add azure dps bootstrap info provider auth to json (%d)", ret); + json_object_put(auth); + return ret; + } + + return 0; +} + +/** + * @brief Encodes bootstrap info provider settings to json. + * + * @param provider The provider settings to encode. + * @param jprovider Output argument that will hold the bootstrap info object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int + */ +static int +json_encode_bootstrap_info_provider(const struct bootstrap_info_provider_settings *provider, struct json_object **jprovider) +{ + int ret; + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate json object for bootstrap info provider)"); + return -ENOMEM; + } + + struct json_object *type = json_object_new_string(bootstrap_info_provider_type_str(provider->type)); + if (!type) { + zlog_error("failed to encode bootstrap info provider type for json object"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_BI_COMMON_TYPE, type); + if (ret < 0) { + zlog_error("failed to add type to bootstrap info provider object (%d)", ret); + json_object_put(type); + goto fail; + } + + struct json_object *name = json_object_new_string(provider->name ? provider->name : "default"); + if (!name) { + zlog_error("failed to encode bootstrap info provider name for json object"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_BI_COMMON_NAME, name); + if (ret < 0) { + zlog_error("failed to add name to bootstrap info provider object (%d)", ret); + json_object_put(name); + goto fail; + } + + if (provider->expiration_time != BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET) { + struct json_object *expiration_time = json_object_new_int((int32_t)provider->expiration_time); + if (!expiration_time) { + zlog_error("failed to encode bootstrap info provider expiration time"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_BI_COMMON_EXPIRATION_TIME, expiration_time); + if (ret < 0) { + zlog_error("failed to add expiratin time to bootstrap info provider object (%d)", ret); + json_object_put(expiration_time); + goto fail; + } + } + + switch (provider->type) { + case BOOTSTRAP_INFO_PROVIDER_FILE: { + ret = json_encode_bootstrap_info_provider_file(provider->file, jobj); + if (ret < 0) { + zlog_error("failed to encode file bootstrap info provider settings as json (%d)", ret); + goto fail; + } + break; + } + case BOOTSTRAP_INFO_PROVIDER_AZUREDPS: { + ret = json_encode_bootstrap_info_provider_azuredps(provider->dps, jobj); + if (ret < 0) { + zlog_error("failed to encode azure dps bootstrap info provider settings as json (%d)", ret); + goto fail; + } + break; + } + default: { + zlog_error("unsupported bootstrap info provider type for json encoding"); + ret = -EOPNOTSUPP; + goto fail; + } + } + + *jprovider = jobj; +out: + return ret; +fail: + if (jobj) + json_object_put(jobj); + goto out; +} + +/** + * @brief Encodes bootstrap information providers to a json array. + * + * @param settings The ztp configurator settings to source the bootstrap info provider settings from. + * @param jbootstrap_info_providers Output argument that will hold the bootstrap info providers array. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int 0 if the encoding eas successful, non-zero otherwise. + */ +static int +json_encode_bootstrap_info_providers(const struct ztp_configurator_settings *settings, struct json_object **jproviders) +{ + int ret; + json_object *obj = json_object_new_array(); + if (!obj) { + zlog_error("failed to allocate new json array for bootstrap info providers"); + return -ENOMEM; + } + + struct json_object *jprovider; + struct bootstrap_info_provider_settings *provider; + list_for_each_entry (provider, &settings->provider_settings, list) { + ret = json_encode_bootstrap_info_provider(provider, &jprovider); + if (ret < 0) { + zlog_error("failed to encode bootstrap info provider for json (%d)", ret); + goto fail; + } + + ret = json_object_array_add(obj, jprovider); + if (ret < 0) { + zlog_error("failed to add bootstrap info provider to json array (%d)", ret); + json_object_put(jprovider); + goto fail; + } + } + + *jproviders = obj; +out: + return ret; +fail: + if (jproviders) + json_object_put(obj); + goto out; +} + +/** + * @brief Encodes the configurator bootstrap info provider details. + * + * @param settings The configurator object to source bootstrap info details from. + * @param jbootstrap_info Output argument that will hold the bootstrap info object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int 0 if the encoding was successful, non-zero otherwise. + */ +static int +json_encode_bootstrap_info(const struct ztp_configurator_settings *settings, struct json_object **jbootstrap_info) +{ + int ret; + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate new json bootstrap info object"); + return -ENOMEM; + } + + if (settings->expiration_time != BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET) { + struct json_object *expiration_time = json_object_new_int((int32_t)settings->expiration_time); + if (!expiration_time) { + zlog_error("failed to encode bootstrap info expiration time for json"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_BI_EXPIRATION_TIME, expiration_time); + if (ret < 0) { + zlog_error("failed to add expiration time to bootstrap info json object (%d)", ret); + json_object_put(expiration_time); + goto fail; + } + } + + struct json_object *jproviders; + ret = json_encode_bootstrap_info_providers(settings, &jproviders); + if (ret < 0) { + zlog_error("failed to encode bootstrap info providers for json (%d)", ret); + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_BI_PROVIDERS, jproviders); + if (ret < 0) { + zlog_error("failed to add bootstrap info providers array to bootstrap info object (%d)", ret); + json_object_put(jproviders); + goto fail; + } + + *jbootstrap_info = jobj; +out: + return ret; +fail: + json_object_put(jobj); + goto out; +} + +/** + * @brief Encodes a psk-based network credential as json, appending it to the + * passed in json object. + * + * @param psk The psk to encode. + * @param jobj The psk-based network credential to append to. + * @return int 0 if the psk-based credential fields were encoded, non-zero otherwise. + */ +static int +json_encode_network_credential_psk(const struct dpp_network_credential_psk *psk, struct json_object *jobj) +{ + int ret; + + switch (psk->type) { + case PSK_CREDENTIAL_TYPE_PSK: { + struct json_object *keyhex = json_object_new_string(psk->key.hex); + if (!keyhex) { + zlog_error("failed to encode psk value for psk network credential json object"); + return -EBADMSG; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_CREDENTIAL_PSK, keyhex); + if (ret < 0) { + zlog_error("failed to add psk value for psk network credential json object (%d)", ret); + json_object_put(keyhex); + return ret; + } + break; + } + case PSK_CREDENTIAL_TYPE_PASSPHRASE: { + struct json_object *passphrase = json_object_new_string(psk->passphrase.ascii); + if (!passphrase) { + zlog_error("failed to encode passphrase for psk network credential json object"); + return -EBADMSG; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_CREDENTIAL_PASSPHRASE, passphrase); + if (ret < 0) { + zlog_error("failed to add passphrase for psk network credential json object (%d)", ret); + json_object_put(passphrase); + return ret; + } + break; + } + default: { + zlog_error("unsupported psk type for json psk credential encoding"); + return -EOPNOTSUPP; + } + } + + return 0; +} + +/** + * @brief Encodes an sae-based network credential as json, appending it to the + * passed in json object. + * + * @param The sae to encode. + * @param jobj The sae-based network credential to append to. + * @return int 0 if the sae-based credential fields were encoded, non-zero otherwise. + */ +static int +json_encode_network_credential_sae(const struct dpp_network_credential_sae *sae, struct json_object *jobj) +{ + int ret; + + struct json_object *passphrase = json_object_new_string(sae->passphrase); + if (!passphrase) { + zlog_error("failed to encode passphrase for sae network credential json object"); + return -EBADMSG; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_CREDENTIAL_PASSPHRASE, passphrase); + if (ret < 0) { + zlog_error("failed to add passphrase for sae network credential json object (%d)", ret); + json_object_put(passphrase); + return ret; + } + + return 0; +} + +/** + * @brief Encodes a network credential as json. + * + * @param credential The credential to encode. + * @param jcredential Output argument that will hold the credential object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int + */ +static int +json_encode_network_credential(const struct dpp_network_credential *credential, struct json_object **jcredential) +{ + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate json object for network credential"); + return -ENOMEM; + } + + int ret; + if (!dpp_network_credential_is_valid(credential)) { + zlog_error("invalid credential found during network json encoding"); + ret = -EINVAL; + goto fail; + } + + struct json_object *akm = json_object_new_string(dpp_akm_str(credential->akm)); + if (!akm) { + zlog_error("failed to encode network credential akm for json object"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_CREDENTIAL_AKM, akm); + if (ret < 0) { + zlog_error("failed to add akm to network credential json object (%d)", ret); + json_object_put(akm); + goto fail; + } + + switch (credential->akm) { + case DPP_AKM_PSK: { + ret = json_encode_network_credential_psk(&credential->psk, jobj); + if (ret < 0) { + zlog_error("failed to encode network psk for json object (%d)", ret); + goto fail; + } + break; + } + case DPP_AKM_SAE: { + ret = json_encode_network_credential_sae(&credential->sae, jobj); + if (ret < 0) { + zlog_error("failed to encode network sae for json object (%d)", ret); + goto fail; + } + break; + } + default: { + zlog_error("unsupported network credential akm for network credential json encoding"); + ret = -EOPNOTSUPP; + goto fail; + } + } + + *jcredential = jobj; +out: + return ret; +fail: + json_object_put(jobj); + goto out; +} + +/** + * @brief Parser for the "akm" configuration option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "akm". + * @param jobj The json value of the 'akm' property. + * @param context The configurator settings object instance. + */ +static void +json_parse_credential_akm(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct dpp_network_credential *credential = (struct dpp_network_credential *)context; + + const char *value = json_object_get_string(jobj); + enum dpp_akm akm = parse_dpp_akm(value); + if (akm == DPP_AKM_INVALID) { + zlog_warning("invalid dpp akm '%s', ignoring credential", value); + return; + } + + credential->akm = akm; +} + +/** + * @brief Parser for the "psk" configuration option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "psk". + * @param jobj The json value of the 'psk' property. + * @param context The configurator settings object instance. + */ +static void +json_parse_credential_psk(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct dpp_network_credential *credential = (struct dpp_network_credential *)context; + struct dpp_network_credential_psk *psk = &credential->psk; + + const char *key_hex = json_object_get_string(jobj); + + int ret = dpp_credential_psk_set_key(psk, key_hex); + if (ret < 0) { + zlog_error("failed to set credential pre-shared key (%d)", ret); + return; + } + + psk->type = PSK_CREDENTIAL_TYPE_PSK; +} + +/** + * @brief Parser for the "passphrase" configuration option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "passphrase". + * @param jobj The json value of the 'passphrase' property. + * @param context The configurator settings object instance. + */ +static void +json_parse_credential_passphrase(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct dpp_network_credential *credential = (struct dpp_network_credential *)context; + const char *passphrase = json_object_get_string(jobj); + + switch (credential->akm) { + case DPP_AKM_PSK: { + struct dpp_network_credential_psk *psk = &credential->psk; + int ret = dpp_credential_psk_set_passphrase(psk, passphrase); + if (ret < 0) { + zlog_warning("failed to set passphrase for psk credential (%d)", ret); + return; + } + psk->type = PSK_CREDENTIAL_TYPE_PASSPHRASE; + break; + } + case DPP_AKM_SAE: { + struct dpp_network_credential_sae *sae = &credential->sae; + int ret = dpp_credential_sae_set_passphrase(sae, passphrase); + if (ret < 0) { + zlog_warning("failed to set passphrase for sae credential (%d)", ret); + return; + } + break; + } + default: { + zlog_warning("ignoring 'passphrase' property for non-psk/sae credential"); + return; + } + } +} + +/** + * @brief DPP "credential" network property configuration file options. + */ +static struct json_property_parser credential_properties[] = { + { + .name = JSON_PROPERTY_NAME_CREDENTIAL_AKM, + .type = json_type_string, + .value = { + json_parse_credential_akm, + }, + }, + { + .name = JSON_PROPERTY_NAME_CREDENTIAL_PSK, + .type = json_type_string, + .value = { + json_parse_credential_psk, + }, + }, + { + .name = JSON_PROPERTY_NAME_CREDENTIAL_PASSPHRASE, + .type = json_type_string, + .value = { + json_parse_credential_passphrase, + }, + }, +}; + +/** + * @brief Function to parse "credentials" network option. + * + * @param parent The parent object. + * @param array The containing array object. + * @param name The name of the parent object ("credentials"). + * @param jobj The array element value. + * @param index The array element index. + * @param type The type of the array element (json_type_object). + * @param context The configurator settings object instance. + */ +static void +json_parse_network_credentials(struct json_object *parent, struct json_object *array, const char *name, struct json_object *jobj, uint32_t index, json_type type, void *context) +{ + __unused(parent); + __unused(array); + __unused(name); + __unused(index); + __unused(type); + + struct dpp_network *network = (struct dpp_network *)context; + struct dpp_network_credential *credential = dpp_network_credential_alloc(); + if (!credential) { + zlog_warning("allocation failure for network credential"); + return; + } + + json_parse_object_s(jobj, credential_properties, credential); + + if (!dpp_network_credential_is_valid(credential)) { + zlog_warning("invalid network credential specified"); + dpp_network_credential_uninitialize(credential); + free(credential); + return; + } + + dpp_network_add_credential(network, credential); +} + +/** + * @brief Encodes network credentials as a json array. + * + * @param network The network with the credentials to encode. + * @param jcredentials Output argument that will hold the credentials object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int + */ +static int +json_encode_network_credentials(const struct dpp_network *network, struct json_object **jcredentials) +{ + int ret; + json_object *credentials = json_object_new_array(); + if (!credentials) { + zlog_error("failed to allocate json array for network credentials"); + return -ENOMEM; + } + + struct json_object *jcredential; + struct dpp_network_credential *credential; + list_for_each_entry (credential, &network->credentials, list) { + ret = json_encode_network_credential(credential, &jcredential); + if (ret < 0) { + zlog_error("failed to encode network credential for json network object (%d)", ret); + goto fail; + } + + ret = json_object_array_add(credentials, jcredential); + if (ret < 0) { + zlog_error("failed to add network credential to json credentials array (%d)", ret); + json_object_put(jcredential); + goto fail; + } + } + + *jcredentials = credentials; +out: + return ret; +fail: + json_object_put(credentials); + goto out; +} + +/** + * @brief Parser for the "ssid" network discovery configuration option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "ssid". + * @param jobj The json value of the ssid property. + * @param context The configurator settings object instance. + */ +static void +json_parse_network_discovery_ssid(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct dpp_network_discovery *discovery = (struct dpp_network_discovery *)context; + + const char *ssid = json_object_get_string(jobj); + size_t ssid_length = strlen(ssid) /* + 1 */; + if (ssid_length > (sizeof discovery->ssid)) { + zlog_warning("ssid length (%lu) exceeds maximum length (%lu)", ssid_length, sizeof discovery->ssid); + return; + } + + memcpy(discovery->ssid, ssid, ssid_length); + discovery->ssid_length = ssid_length; +} + +/** + * @brief Parser for the "ssidCharset" network discovery configuration option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "ssidCharset". + * @param jobj The json value. Must be of type 'json_type_int'. + * @param context The provider settings object instance. + */ +static void +json_parse_network_discovery_ssid_charset(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct dpp_network_discovery *discovery = (struct dpp_network_discovery *)context; + discovery->ssid_charset = json_object_get_int(jobj); +} + +/** + * @brief DPP "discovery" network property configuration file options. + */ +static struct json_property_parser network_discovery_properties[] = { + { + .name = JSON_PROPERTY_NAME_NET_CFG_DISCOVERY_SSID, + .type = json_type_string, + .value = { + json_parse_network_discovery_ssid, + }, + }, + { + .name = JSON_PROPERTY_NAME_NET_CFG_DISCOVERY_SSID_CHARSET, + .type = json_type_int, + .value = { + json_parse_network_discovery_ssid_charset, + }, + }, +}; + +/** + * @brief Parser for the "discovery" network option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "discovery". + * @param jobj The json value of the 'discovery' property. + * @param context The configurator settings object instance. + */ +static void +json_parse_network_discovery(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_parse_object_s(jobj, network_discovery_properties, context); +} + +/** + * @brief Encodes network discovery information as json. + * + * @param discovery The network discovery to encode. + * @param jdiscovery Output argument that will hold the discovery object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int + */ +static int +json_encode_network_discovery(const struct dpp_network_discovery *discovery, struct json_object **jdiscovery) +{ + int ret; + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate a new network discovery json object"); + return -ENOMEM; + } + + struct json_object *ssid = json_object_new_string((const char *)discovery->ssid); + if (!ssid) { + zlog_error("failed to allocate json string for network discovery ssid"); + ret = -ENOMEM; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_NET_CFG_DISCOVERY_SSID, ssid); + if (ret < 0) { + json_object_put(ssid); + zlog_error("failed to add ssid to network discovery json object (%d)", ret); + goto fail; + } + + struct json_object *ssid_charset = json_object_new_int(discovery->ssid_charset); + if (!ssid_charset) { + zlog_error("failed to encode ssid charset for network discovery json"); + ret = -EBADMSG; + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_NET_CFG_DISCOVERY_SSID_CHARSET, ssid_charset); + if (ret < 0) { + zlog_error("failed to add ssid charset to network discovery json object (%d)", ret); + json_object_put(ssid_charset); + goto fail; + } + + *jdiscovery = jobj; +out: + return ret; +fail: + if (jobj) + json_object_put(jobj); + goto out; +} + +/** + * @brief DPP "network" property configuration file options. + */ +static struct json_property_parser network_properties[] = { + { + .name = JSON_PROPERTY_NAME_NET_CFG_DISCOVERY, + .type = json_type_object, + .value = { + json_parse_network_discovery, + }, + }, + { + .name = JSON_PROPERTY_NAME_NET_CFG_CREDENTIALS, + .type = json_type_array, + .array = { + json_parse_network_credentials, + json_type_object, + }, + }, +}; + +/** + * @brief Parser for the "default" network configuration option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "default". + * @param jobj The json value of the 'default' property. + * @param context The configurator settings object instance. + */ +static void +json_parse_network_default(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct ztp_configurator_settings *settings = (struct ztp_configurator_settings *)context; + + struct dpp_network *network = dpp_network_alloc(); + if (!network) { + zlog_warning("allocation failure for defaultNetwork"); + return; + } + + json_parse_object_s(jobj, network_properties, network); + + if (!dpp_network_is_valid(network)) { + zlog_warning("invalid defaultNetwork specified"); + dpp_network_uninitialize(network); + free(network); + return; + } + + settings->network_config_default = network; +} + +/** + * @brief Encodes the default network as a json object. + * + * @param settings The configurator settings to source the default network object from. + * @param jnetwork Output argument that will hold the default network object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int 0 if the object was created, non-zero otherwise. + */ +static int +json_encode_network(const struct dpp_network *network, struct json_object **jnetwork) +{ + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate new network json object"); + return -ENOMEM; + } + + struct json_object *discovery; + int ret = json_encode_network_discovery(&network->discovery, &discovery); + if (ret < 0) { + zlog_error("failed to encode network discovery for network json object (%d)", ret); + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_NET_CFG_DISCOVERY, discovery); + if (ret < 0) { + zlog_error("failed to add network discovery to network json object (%d)", ret); + json_object_put(discovery); + goto fail; + } + + struct json_object *network_credentials; + ret = json_encode_network_credentials(network, &network_credentials); + if (ret < 0) { + zlog_error("failed to encode network credentials for network json object (%d)", ret); + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_NET_CFG_CREDENTIALS, network_credentials); + if (ret < 0) { + zlog_error("failed to add network credential to network jonect object (%d)", ret); + json_object_put(network_credentials); + goto fail; + } + + *jnetwork = jobj; +out: + return ret; +fail: + if (jobj) + json_object_put(jobj); + goto out; +} + +/** + * @brief Encodes ztp network configuration information as json. + * + * @param settings The ztp settings. + * @param jnetwork_configuration Output argument that will hold the network object. + * The caller owns the object and is responsible for freeing it by calling + * json_object_put. + * @return int + */ +static int +json_encode_network_configuration(const struct ztp_configurator_settings *settings, struct json_object **jnetwork_configuration) +{ + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to allocate new json object for network configuration"); + return -ENOMEM; + } + + struct json_object *default_network = NULL; + int ret = json_encode_network(settings->network_config_default, &default_network); + if (ret < 0) { + zlog_error("failed to encode default network (%d)", ret); + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_NET_CFG_DEFAULT, default_network); + if (ret < 0) { + zlog_error("failed to add '" JSON_PROPERTY_NAME_NET_CFG_DEFAULT "' property to configurator json (%d)", ret); + json_object_put(default_network); + goto fail; + } + + *jnetwork_configuration = jobj; +out: + return ret; +fail: + if (jobj) + json_object_put(jobj); + goto out; +} + +/** + * @brief Configurator "network" property configuration file options. + */ +static struct json_property_parser configurator_network_configuration_properties[] = { + { + .name = JSON_PROPERTY_NAME_NET_CFG_DEFAULT, + .type = json_type_object, + .value = { + json_parse_network_default, + }, + }, +}; + +/** + * @brief Parser for the "network" configuration option. + * + * @param parent The parent object. + * @param name The name of the json property. Must be "network". + * @param jobj The json value of the 'network' info property. + * @param context The configurator settings object instance. + */ +static void +json_parse_network_configuration(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_parse_object_s(jobj, configurator_network_configuration_properties, context); +} + +/** + * @brief Configurator top-level configuration file options. + */ +static struct json_property_parser configurator_config_properties[] = { + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO, + .type = json_type_object, + .value = { + json_parse_bootstrap_info, + }, + }, + { + .name = JSON_PROPERTY_NAME_NET_CFG, + .type = json_type_object, + .value = { + json_parse_network_configuration, + }, + }, +}; + +/** + * @brief Parses a json-formatted configurator configuration file. + * + * @param file The path of the file to parse. + * @param configurator The configurator settings to fill in. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +ztp_configurator_config_parse(const char *file, struct ztp_configurator_settings *settings) +{ + settings->expiration_time = BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET; + + int ret = json_parse_file_s(file, configurator_config_properties, settings, NULL); + if (ret < 0) + return ret; + + if (settings->expiration_time == BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET) + settings->expiration_time = 0; + + struct bootstrap_info_provider_settings *bisettings; + list_for_each_entry (bisettings, &settings->provider_settings, list) { + if (bisettings->expiration_time == BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET) + bisettings->expiration_time = settings->expiration_time; + } + + return 0; +} + +/** + * @brief Serialize ztp configurator settings to a json object. + * + * @param settings The settings to serialize. + * @param jsettings Output argument for the serialized json object. If this is + * populated, the caller must free it by passing it to json_object_put(). + * + * @return int + */ +static int +ztp_configurator_config_to_json(const struct ztp_configurator_settings *settings, struct json_object **jsettings) +{ + struct json_object *jobj = json_object_new_object(); + if (!jobj) { + zlog_error("failed to json object for configurator settings"); + return -ENOMEM; + } + + struct json_object *bootstrap_info = NULL; + int ret = json_encode_bootstrap_info(settings, &bootstrap_info); + if (ret < 0) { + zlog_error("failed to encode bootstrap info (%d)", ret); + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_BOOTSTRAP_INFO, bootstrap_info); + if (ret < 0) { + zlog_error("failed to add '" JSON_PROPERTY_NAME_BOOTSTRAP_INFO "' property to configurator json (%d)", ret); + goto fail; + } + + struct json_object *network_configuration; + ret = json_encode_network_configuration(settings, &network_configuration); + if (ret < 0) { + zlog_error("failed to encode network configuration (%d)", ret); + goto fail; + } + + ret = json_object_object_add(jobj, JSON_PROPERTY_NAME_NET_CFG, network_configuration); + if (ret < 0) { + zlog_error("failed to add network configuration to configurator json (%d)", ret); + json_object_put(network_configuration); + goto fail; + } + + *jsettings = jobj; +out: + return ret; +fail: + if (jobj) + json_object_put(jobj); + goto out; +} + +/** + * @brief Persists configurator settings to file descriptor. + * + * @param fd The file descriptor to write the settings to. + * @param settings The settings to write to file. + * @return int 0 if the settings were successfully written to file, non-zero otherwise. + */ +int +ztp_configurator_settings_persist_fd(int fd, const struct ztp_configurator_settings *settings) +{ + static const int JSON_C_SERIALIZE_FLAGS = (0 + | JSON_C_TO_STRING_NOSLASHESCAPE // don't escape paths + | JSON_C_TO_STRING_PRETTY // make it look good + | JSON_C_TO_STRING_SPACED // minimize whitespace + | JSON_C_TO_STRING_PRETTY_TAB // use a full tab character + ); + + struct json_object *jsettings; + int ret = ztp_configurator_config_to_json(settings, &jsettings); + if (ret < 0) { + zlog_error("failed to encode configurator settings to json (%d)", ret); + return ret; + } + + ret = json_object_to_fd(fd, jsettings, JSON_C_SERIALIZE_FLAGS); + if (ret < 0) { + zlog_error("failed to write settings to file descriptor (%d)", ret); + } + + json_object_put(jsettings); + return ret; +} + +/** + * @brief Macros to help defined the temporary path string for writing the + * configurator settings file. + */ +#define TMP_TEMPLATE_SUFFIX_STRING ".XXXXXX" + +/** + * @brief Persists configurator settings to file. + * + * @param filename The filename to write the settings to. + * @param settings The settings to write to file. + * @return int 0 if the settings were successfully written to file, non-zero otherwise. + */ +int +ztp_configurator_settings_persist(const char *filename, const struct ztp_configurator_settings *settings) +{ + int ret; + int fd = -1; + + char *filename_target = NULL; + ret = get_link_target(filename, &filename_target); + if (ret < 0) { + zlog_error("failed to resolve configurator settings file '%s' link target (%d)", filename, ret); + return ret; + } + + if (filename_target) + filename = filename_target; + + size_t filename_length = strlen(filename); + char *pathtmp = malloc(filename_length + ARRAY_SIZE(TMP_TEMPLATE_SUFFIX_STRING)); + if (!pathtmp) { + zlog_error("failed to allocate memory for temp configurator settings file path"); + ret = -ENOMEM; + goto out; + } + + memcpy(pathtmp, filename, filename_length); + memcpy(pathtmp + filename_length, TMP_TEMPLATE_SUFFIX_STRING, ARRAY_SIZE(TMP_TEMPLATE_SUFFIX_STRING)); + + fd = mkstemp(pathtmp); + if (fd < 0) { + ret = -errno; + zlog_error("failed to create temporary file for configurator settings (%d)", ret); + goto out; + } + + ret = ztp_configurator_settings_persist_fd(fd, settings); + if (ret < 0) { + zlog_error("failed to write settings to temporary file (%d)", ret); + goto out; + } + + fdatasync(fd); + + ret = rename(pathtmp, filename); + if (ret < 0) { + ret = -errno; + zlog_error("failed to move temporary configurator settings file to target file (%d)", ret); + goto out; + } + +out: + if (fd != -1) + close(fd); + if (filename_target) + free(filename_target); + if (pathtmp) + free(pathtmp); + + return ret; +} + +/** + * @brief Initializes the settings structure for use. + * + * @param settings + */ +void +ztp_configurator_settings_initialize(struct ztp_configurator_settings *settings) +{ + explicit_bzero(settings, sizeof *settings); + INIT_LIST_HEAD(&settings->provider_settings); + settings->expiration_time = BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET; +} + +/** + * @brief Uninitializes configurator settings, freeing any owned resources. + * + * @param settings The settings object to uninitialize. + */ +void +ztp_configurator_settings_uninitialize(struct ztp_configurator_settings *settings) +{ + if (!settings) + return; + + if (!list_empty(&settings->provider_settings)) { + struct bootstrap_info_provider_settings *bisettings; + struct bootstrap_info_provider_settings *bisettingstmp; + + list_for_each_entry_safe (bisettings, bisettingstmp, &settings->provider_settings, list) { + bootstrap_info_provider_settings_uninitialize(bisettings); + free(bisettings); + } + } + + if (settings->network_config_default) { + dpp_network_uninitialize(settings->network_config_default); + free(settings->network_config_default); + settings->network_config_default = NULL; + } +} + +/** + * @brief Add new bootstrap information provider settings to the configurator settings. + * + * @param settings The configurator settings to add the bootstrap info provider settings to. + * @param provider The bootstrap info provider settings to add. + */ +void +ztp_configurator_settings_add_bi_provider_settings(struct ztp_configurator_settings *settings, struct bootstrap_info_provider_settings *provider) +{ + list_add(&provider->list, &settings->provider_settings); +} diff --git a/src/core/ztp_configurator_config.h b/src/core/ztp_configurator_config.h new file mode 100644 index 0000000..eea2ff0 --- /dev/null +++ b/src/core/ztp_configurator_config.h @@ -0,0 +1,77 @@ + +#ifndef __ZTP_CONFIGURATOR_CONFIG_H__ +#define __ZTP_CONFIGURATOR_CONFIG_H__ + +#include + +/** + * @brief Forward declarations. + */ +struct dpp_network; +struct bootstrap_info_provider_settings; + +/** + * @brief DPP configurator settings. + */ +struct ztp_configurator_settings { + uint32_t expiration_time; + struct dpp_network *network_config_default; + struct list_head provider_settings; +}; + +/** + * @brief Parses a json-formatted configurator configuration file. + * + * @param file The path of the file to parse. + * @param configurator The configurator settings to fill in. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +ztp_configurator_config_parse(const char *file, struct ztp_configurator_settings *settings); + +/** + * @brief Persists configurator settings to file. + * + * @param filename The filename to write the settings to. + * @param settings The settings to write to file. + * @return int 0 if the settings were successfully written to file, non-zero otherwise. + */ +int +ztp_configurator_settings_persist(const char *filename, const struct ztp_configurator_settings *settings); + +/** + * @brief Persists configurator settings to file descriptor. + * + * @param fd The file descriptor to write the settings to. + * @param settings The settings to write to file. + * @return int 0 if the settings were successfully written to file, non-zero otherwise. + */ +int +ztp_configurator_settings_persist_fd(int fd, const struct ztp_configurator_settings *settings); + +/** + * @brief Initializes the settings structure for use. + * + * @param settings + */ +void +ztp_configurator_settings_initialize(struct ztp_configurator_settings *settings); + +/** + * @brief Uninitializes configurator settings, freeing any owned resources. + * + * @param settings The settings object to uninitialize. + */ +void +ztp_configurator_settings_uninitialize(struct ztp_configurator_settings *settings); + +/** + * @brief Add new bootstrap information provider settings to the configurator settings. + * + * @param settings The configurator settings to add the bootstrap info provider settings to. + * @param provider The bootstrap info provider settings to add. + */ +void +ztp_configurator_settings_add_bi_provider_settings(struct ztp_configurator_settings *settings, struct bootstrap_info_provider_settings *provider); + +#endif //__ZTP_CONFIGURATOR_CONFIG_H__ diff --git a/src/core/ztp_enrollee.c b/src/core/ztp_enrollee.c new file mode 100644 index 0000000..cdc70ee --- /dev/null +++ b/src/core/ztp_enrollee.c @@ -0,0 +1,1167 @@ + +#include +#include +#include +#include +#include + +#include "ztp_dbus_client.h" +#include "ztp_dbus_configurator.h" +#include "ztp_enrollee.h" + +static void +ztp_enrollee_update_connectivity_state(struct ztp_enrollee *enrollee, enum ztp_connectivity_state state); + +static void +ztp_enrollee_on_dpp_state_changed(struct ztp_enrollee *enrollee, enum dpp_state dpp_state); + +/** + * @brief Enumeration describing whether a network has been added or + * removed. + */ +enum ztp_network_change { + ZTP_NETWORK_ADDED = 0, + ZTP_NETWORK_REMOVED, +}; + +/** + * @brief Handler function invoked when the number of networks on an enrollee + * has changed from a wpa_supplicant perspective. + * + * This function updates the number of networks for the interface in question. + * + * There are cases where wpa_supplicant does not emit the NetworkAdded and + * NetworkRemoved signals, for example, when the configuration file is updated + * by hand and then reloaded using the 'reconfigure' command. To handle such + * cases, the number of networks is always explicitly read upon a change as + * opposed to tracking the counts based on the signals. This should ensure that + * an out-of-sync network count will synchronize to the correct value each time + * the signal is emitted. + * + * @param enrollee The enrollee for which the network list has changed. + * @param change The nature of the change (added, removed) + */ +static void +on_networks_changed(struct ztp_enrollee *enrollee, enum ztp_network_change change) +{ + static const char changechar[] = { + '+', + '-', + }; + + uint32_t num_networks = 0; + uint32_t num_networks_last = enrollee->num_networks; + int32_t num_networks_changed = 0; + + int ret = ztp_get_interface_network_count(enrollee->wpas, enrollee->path, &num_networks); + if (ret < 0) { + num_networks_changed = 1; + zlog_error_if(enrollee->interface, "failed to retrieve network count (%d)", ret); + + // Update the count differentially based on the nature of the change. + // This could result in an out-of-sync network count, however, it will + // be more accurate than not adjusting the count at all. + switch (change) { + case ZTP_NETWORK_ADDED: + enrollee->num_networks++; + break; + case ZTP_NETWORK_REMOVED: + if (enrollee->num_networks > 0) + enrollee->num_networks--; + break; + default: + break; + } + } else { + num_networks_changed = (int32_t)enrollee->num_networks - (int32_t)num_networks; + if (abs(num_networks_changed) != 1) + zlog_warning_if(enrollee->interface, "out-of-sync network count detected (%+d changed)", num_networks_changed); + + enrollee->num_networks = num_networks; + } + + zlog_info_if(enrollee->interface, "%cnetwork[%u]", changechar[change], enrollee->num_networks); + + if (num_networks_last == 0 && enrollee->num_networks > 0) { + ztp_enrollee_update_connectivity_state(enrollee, ZTP_CONNECTIVITY_STATE_PROVISIONED); + } else if (num_networks_last > 0 && enrollee->num_networks == 0) { + ztp_enrollee_update_connectivity_state(enrollee, ZTP_CONNECTIVITY_STATE_INITIALIZING); + } +} + +/** + * @brief Handler function invoked when a new network is added to an interface. + * + * The actual path of the added network is ignored since there is no current use for it. + * + * @param msg The sd-bus message containing the object path and propertirs of + * the network that was added. + * @param userdata Message context, of type 'struct ztp_enrollee', an + * instance describing the interface for which a network was added. + * @param ret_error + * @return int + */ +static int +on_network_added(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) +{ + __unused(msg); + __unused(ret_error); + + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + on_networks_changed(enrollee, ZTP_NETWORK_ADDED); + return 0; +} + +/** + * @brief Handler function invoked when a network is removed from an interface. + * + * The actual path of the removed network is ignored since there is no current use for it. + * + * @param msg The sd-bus message containing the object path of the network that was removed. + * @param userdata Message context, of type 'struct ztp_enrollee', an + * instance describing the interface for which a network was removed. + * @param ret_error + * @return int 0 + */ +static int +on_network_removed(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) +{ + __unused(msg); + __unused(ret_error); + + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + on_networks_changed(enrollee, ZTP_NETWORK_REMOVED); + return 0; +} + +/** + * @brief Determine if the interface is provisioned. + * + * @param enrollee The enrollee to check for provisioning status. + * @return true If the enrollee is provisioned. + * @return false If the enrollee is not provisioned. + */ +static bool +ztp_enrollee_is_provisioned(struct ztp_enrollee *enrollee) +{ + switch (enrollee->state) { + case ZTP_CONNECTIVITY_STATE_PROVISIONED: + case ZTP_CONNECTIVITY_STATE_CONNECTED: + return true; + default: + return false; + } +} + +/** + * @brief Determine if the interface has configured networks. + * + * @param enrollee The enrollee to check for configured networks. + * @return true If the enrollee has at least one configured network. + * @return false If the enrollee has no configured networks. + */ +static bool +ztp_enrollee_has_configured_networks(struct ztp_enrollee *enrollee) +{ + return (enrollee->num_networks > 0); +} + +/** + * @brief Determines if the enrollee is currently chirping. This is a passive + * check that uses the last read value from wpa supplicant. + * + * @param enrollee The enrollee to check. + * @return true If the enrollee is chirping. + * @return false If the enrollee is not chirping. + */ +static bool +is_dpp_in_progress(struct ztp_enrollee *enrollee) +{ + switch (enrollee->dpp_state) { + case DPP_STATE_CHIRPING: + case DPP_STATE_PROVISIONING: + case DPP_STATE_BOOTSTRAP_KEY_ACQUIRING: + case DPP_STATE_BOOTSTRAPPED: + case DPP_STATE_AUTHENTICATING: + case DPP_STATE_AUTHENTICATED: + return true; + default: + return false; + } +} + +/** + * @brief Determines if provisioning is inactive. + * + * @param enrollee The enrollee to check. + * @return true If the enrollee is not undergoing active provisioning, or if + * provisioning has succeeded. + * @return false Otherwise. + */ +static bool +is_dpp_inactive(struct ztp_enrollee *enrollee) +{ + switch (enrollee->dpp_state) { + case DPP_STATE_INACTIVE: + case DPP_STATE_TERMINATED: + case DPP_STATE_UNKNOWN: + return true; + default: + return false; + } +} + +/** + * @brief Determines if the enrollee is connected to a network. This does not + * imply any sort of network connectivity, only association. + * + * @param enrollee The enrollee to check. + * @return true If the enrollee is connected (associated) with an access point. + * @return false If the enrollee is not connected (associated) with an access point. + */ +static bool +ztp_enrollee_is_connected(struct ztp_enrollee *enrollee) +{ + return (enrollee->if_state == WPAS_INTERFACE_STATE_COMPLETED); +} + +/** + * @brief Updates all configured signals with the current provisioning status. + * Specifically, if an LED has been configured to display provisioning status, + * it will be configured with an appropriate pattern to indicate the current + * status. + * + * @param enrollee The enrollee to update provision status signals for. + */ +static void +ztp_enrollee_status_signals_update(struct ztp_enrollee *enrollee) +{ + // If no status is configured, there is nothing to be done. + struct led_ctrl *led = enrollee->led_status; + if (!led) + return; + + // Clear the led state. + int ret = led_ctrl_set_off(led); + if (ret < 0) { + zlog_warning_if(enrollee->interface, "failed to configure led '%s' to off (%d)", led->path, ret); + // non-critical, continue + } + + // There are two primary paths to determine signal status, defined by + // whether there is at least one (1) configured network. + + // At least 1 configured network. + if (ztp_enrollee_has_configured_networks(enrollee)) { + if (ztp_enrollee_is_connected(enrollee)) { + ret = led_ctrl_set_on(led); + if (ret < 0) { + zlog_warning_if(enrollee->interface, "failed to configure led '%s' to on (%d)", led->path, ret); + // non-critical, continue + } + } else { + ret = led_ctrl_set_repeating_pattern(led, 250 /* ms, 4Hz */); + if (ret < 0) { + zlog_warning_if(enrollee->interface, "failed to configure led '%s' fast blink pattern (%d)", led->path, ret); + // not critical, ignore and move on + } + } + // No configured networks + } else { + if (is_dpp_in_progress(enrollee)) { + ret = led_ctrl_set_repeating_pattern(led, 1000 /* ms, 1Hz */); + if (ret < 0) { + zlog_warning_if(enrollee->interface, "failed to configure led '%s' slow blink pattern (%d)", led->path, ret); + // not critical, continue + } + } + } +} + +/** + * @brief Initializes any/all provisioning status signals. + * + * @param enrollee The enrollee to initialize the status signals for. + * @return int 0 if the status + */ +static int +ztp_enrollee_status_signals_initialize(struct ztp_enrollee *enrollee) +{ + const char *led_path = enrollee->settings->status_signal_led_path; + if (!led_path) + return 0; + + int ret = led_ctrl_create(led_path, &enrollee->led_status); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to create led '%s' control object for provisioning status signaling (%d); ignoring", led_path, ret); + return ret; + } + + zlog_info_if(enrollee->interface, "configured '%s' for provisioning status signaling", led_path); + + return 0; +} + +/** + * @brief Uninitializes provisioning status signals. Note that active signals + * are not reset or cleared. For example, if an LED was configured and + * currently flashing a blink pattern, the blink pattern will be left as-is + * instead of turned off. + * + * @param enrollee The enrollee to uninitialize the signals for. + */ +static void +ztp_enrollee_status_signals_uninitialize(struct ztp_enrollee *enrollee) +{ + if (enrollee->led_status) { + led_ctrl_destroy(enrollee->led_status); + enrollee->led_status = NULL; + } +} + +/** + * @brief Creates a new bootstrap info/key in wpa_supplicant. + * + * @param enrollee The interface to create the bootstrap info for. + * @param id An output argument to hold a pointer to the newly created + * bootstrap info object identifier if this function succeeds. + * @return int Returns zero if successful, and non-zero error code otherwise. + */ +static int +ztp_enrollee_create_bootstrap_dpp_key(struct ztp_enrollee *enrollee, uint32_t *id) +{ + if (!enrollee->settings) { + zlog_error_if(enrollee->interface, "no bootstrap info available, missing interface settings"); + return -ENOENT; + } + + // Issue command to create new bootstrap info entry. + int ret = wpa_controller_dpp_bootstrap_gen(enrollee->ctrl, &enrollee->settings->bootstrap, id); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to bootstrap dpp key (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Retrieves the wpa_supplicant bootstrap info for use in chirping. + * + * If bootstrapping info hasn't been cached or created with wpa_supplicant, + * this function will first create it and cache it. + * + * @param interface The interface to retrieve the bootstrap key for. + * @param id An output argument to hold a pointer to the newly created + * bootstrap info object identifier if this function succeeds. + * @return int Returns 0 if bootstrap information is available. Non-zero otherwise. + */ +static int +ztp_enrollee_get_bootstrap_info(struct ztp_enrollee *enrollee, uint32_t *id) +{ + // First check if bootstrap info has already been setup and cached. + if (enrollee->bootstrap_id) + goto out; + + // Bootstrap info is stored in settings, so verify presence. + if (!enrollee->settings) { + zlog_error_if(enrollee->interface, "no bootstrap info available, missing interface settings"); + return -1; + } + + // No bootstrap entry is cached nor has ztpd created one with + // wpa_supplicant. Create it now and cache it in the interface structure. + int ret = ztp_enrollee_create_bootstrap_dpp_key(enrollee, &enrollee->bootstrap_id); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to create dpp bootstrap info (%d)", ret); + return ret; + } + +out: + *id = enrollee->bootstrap_id; + return 0; +} + +/** + * @brief Number of chirp iterations before giving up. According to the + * specification (v2.0), chirping should never stop so in theory the number of + * iterations should not need to be specified (indefinite), however, + * wpa_supplicant requires a value so one is specified here. + * + * The choice of this value was largely arbitrary. + */ +#define NUM_CHIRP_ITERATIONS (1u << 27) + +/** + * @brief Start DPP presence announcement (chirping). This will ensure the + * bootstrapping information is instrumented in wpa_supplicant and initiates + * the DPP protocol. No prior state is assumed or modified, so this may fail + * if there is an ongoing exchange. The caller is responsible for ensureing + * this is not the case, for example, by aborted any such in-flight exchanges. + * + * @param enrollee The enrollee instance. + * @return int 0 if presence announcement started successfully, non-zero + * otherwise. + */ +static int +dpp_chirp(struct ztp_enrollee *enrollee) +{ + // Get dpp bootstrap info that will be used for chirping. + uint32_t id; + int ret = ztp_enrollee_get_bootstrap_info(enrollee, &id); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to retrieve dpp bootstrap info (%d)", ret); + return ret; + } + + // Request to start chirping with the bootstrap info key id. + ret = wpa_controller_dpp_chirp(enrollee->ctrl, id, NUM_CHIRP_ITERATIONS); + if (ret < 0) + zlog_error_if(enrollee->interface, "failed to start chirping (%d)", ret); + else + ztp_enrollee_on_dpp_state_changed(enrollee, DPP_STATE_BOOTSTRAP_KEY_ACQUIRING); + + return ret; +} + +/** + * @brief Start DPP provisioning. + * + * @param enrollee The enrollee instance. + * @return int + */ +static int +dpp_start(struct ztp_enrollee *enrollee) +{ + // Explicitly stop listening. This effectively clears out any existing DPP + // exchange/state from wpa_supplicant. + wpa_controller_dpp_listen_stop(enrollee->ctrl); + return dpp_chirp(enrollee); +} + +/** + * @brief 'unprovisioned' state handler. + * + * @param interface The interface for which the 'unprovisioned' state was entered. + */ +static void +on_connectivity_state_unprovisioned(struct ztp_enrollee *enrollee) +{ + dpp_start(enrollee); +} + +/** + * @brief 'connecting' state handler. + * + * @param interface The interface for which the 'connecting' state was entered. + */ +static void +on_connectivity_state_provisioned(struct ztp_enrollee *enrollee) +{ + // We have entered the terminal state, so chirping is no longer needed. + if (wpa_controller_dpp_chirp_stop(enrollee->ctrl) < 0) + zlog_warning_if(enrollee->interface, "request to stop chirping failed"); +} + +/** + * @brief 'inactive' state handler. + * + * @param interface The interface for which the 'inactive' state was entered. + */ +static void +on_connectivity_state_inactive(struct ztp_enrollee *enrollee) +{ + __unused(enrollee); + // nothing to do +} + +/** + * @brief 'connected' state handler. + * + * @param interface The interface for which the 'connected' state was entered. + */ +static void +on_connectivity_state_connected(struct ztp_enrollee *enrollee) +{ + __unused(enrollee); + // nothing to do +} + +/** + * @brief 'initializing' state handler. + * + * @param interface The interface for which the 'initializing' state was entered. + */ +static void +on_connectivity_state_initializing(struct ztp_enrollee *enrollee) +{ + enum ztp_connectivity_state state = ztp_enrollee_has_configured_networks(enrollee) + ? ZTP_CONNECTIVITY_STATE_PROVISIONED + : ZTP_CONNECTIVITY_STATE_UNPROVISIONED; + + ztp_enrollee_update_connectivity_state(enrollee, state); +} + +/** + * @brief Array of connectivity state changed handlers. + */ +static void (*const on_connectivity_state_changed[])(struct ztp_enrollee *) = { + on_connectivity_state_initializing, + on_connectivity_state_inactive, + on_connectivity_state_unprovisioned, + on_connectivity_state_provisioned, + on_connectivity_state_connected, +}; + +/** + * @brief Updates the connectivity state of the interface, trigger any consequent actions. + * + * @param interface The interface to change the state for. + * @param state The new connectivity state of the interface. + */ +static void +ztp_enrollee_update_connectivity_state(struct ztp_enrollee *enrollee, enum ztp_connectivity_state state) +{ + if (enrollee->state == state) + return; + + enum ztp_connectivity_state state_old = enrollee->state; + enrollee->state = state; + zlog_info_if(enrollee->interface, "Δstate[connectivity] %s -> %s", ztp_connectivity_state_str(state_old), ztp_connectivity_state_str(state)); + + ztp_enrollee_status_signals_update(enrollee); + on_connectivity_state_changed[state](enrollee); +} + +/** + * @brief Interface-bound DPP state changed handler. This function will be + * invoked each time a DPP state changes occurs on the associated wireless + * interface in wpa_supplicant. + * + * @param interface The ztpd interface the DPP state change occurred on. + * @param dpp_state The new dpp state. + */ +static void +ztp_enrollee_on_dpp_state_changed(struct ztp_enrollee *enrollee, enum dpp_state dpp_state) +{ + enum dpp_state dpp_state_old = enrollee->dpp_state; + enrollee->dpp_state = dpp_state; + zlog_info_if(enrollee->interface, "Δstate[dpp] %s -> %s", dpp_state_str(dpp_state_old), dpp_state_str(dpp_state)); + + // dpp state only affects unprovisioned devices, so ignore other conditions + if (ztp_enrollee_is_provisioned(enrollee)) + return; + + if (is_dpp_inactive(enrollee)) + dpp_start(enrollee); + + ztp_enrollee_status_signals_update(enrollee); +} + +/** + * @brief Interface-bound state changed handler. This function will be + * invoked each time an interface state changes occurs on the associated + * wireless interface in wpa_supplicant. + * + * @param interface The ztpd interface the interface state change occurred on. + * @param if_state The new interface state. + */ +static void +on_interface_state_changed(struct ztp_enrollee *enrollee, enum wpas_interface_state if_state) +{ + if (enrollee->if_state == if_state) + return; + + enum wpas_interface_state if_state_old = enrollee->if_state; + enrollee->if_state = if_state; + zlog_info_if(enrollee->interface, "Δstate[interface] %s -> %s", wpas_interface_state_str(if_state_old), wpas_interface_state_str(if_state)); + + // interface state only affects provisioned devices, so ignore other conditions + if (!ztp_enrollee_is_provisioned(enrollee)) + return; + + // only concerned about association changes: connected <-> not connected + enum ztp_connectivity_state state; + if (if_state_old == WPAS_INTERFACE_STATE_COMPLETED) { + state = ZTP_CONNECTIVITY_STATE_INITIALIZING; + } else if (if_state == WPAS_INTERFACE_STATE_COMPLETED) { + state = ZTP_CONNECTIVITY_STATE_CONNECTED; + } else { + return; + } + + ztp_enrollee_update_connectivity_state(enrollee, state); +} + +/** + * @brief Handler for changes to interface dbus property changes. + * + * @param msg The dbus message containing a dictionary with the changed values. + * @param dbus_interface The dbus inteface the change occurred on. + * @param userdata The ztp_interface structure. + */ +static void +on_dbus_properties_changed(sd_bus_message *msg, const char *dbus_interface, void *userdata) +{ + __unused(dbus_interface); + + const char *property; + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + + for (;;) { + // Enter dictionary entry. + int ret = sd_bus_message_enter_container(msg, SD_BUS_TYPE_DICT_ENTRY, "sv"); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to enter dictionary entry container (%d)", ret); + return; + } else if (ret == 0) + break; + + // Read property identifier string. + ret = sd_bus_message_read_basic(msg, SD_BUS_TYPE_STRING, &property); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to read property name from PropertiesChanged dictionary (%d)", ret); + return; + } else if (ret == 0) { + break; + } + + // Determine contents of variant container. + char type; + const char *contents; + ret = sd_bus_message_peek_type(msg, &type, &contents); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to peek message type for PropertiesChanged (%d)", ret); + return; + } + + // Enter property value (variant) container. + ret = sd_bus_message_enter_container(msg, SD_BUS_TYPE_VARIANT, contents); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to enter variant container for property '%s' (%d)", property, ret); + return; + } + + // Check for properties of interest. + if (strcmp(property, "State") == 0) { + const char *state; + ret = sd_bus_message_read_basic(msg, SD_BUS_TYPE_STRING, &state); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to read 'State' property value (%d)", ret); + return; + } + + on_interface_state_changed(enrollee, parse_wpas_interface_state(state)); + } else { + ret = sd_bus_message_skip(msg, contents); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to skip unrecognized property '%s' (%d)", property, ret); + return; + } + } + + // Exit value (variant) container. + sd_bus_message_exit_container(msg); // variant + + // Exit dictionary entry container. + ret = sd_bus_message_exit_container(msg); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to exit dictionary entry container (%d)", ret); + return; + } + } // for each dictionary entry +} + +/** + * @brief De-activates monitoring of the specified enrollee instance. + * + * @param enrollee The enrollee instance to de-activate. + */ +static void +ztp_enrollee_deactivate(struct ztp_enrollee *enrollee) +{ + enrollee->bootstrap_id = 0; + + if (enrollee->slot_network_added) { + sd_bus_slot_unref(enrollee->slot_network_added); + enrollee->slot_network_added = NULL; + } + + if (enrollee->slot_network_removed) { + sd_bus_slot_unref(enrollee->slot_network_removed); + enrollee->slot_network_removed = NULL; + } + + ztp_enrollee_status_signals_uninitialize(enrollee); + enrollee->active = false; +} + +/** + * @brief Activates monitoring of the specified enrollee instance. This will + * set up handlers to listen for relevant changes to interface and dpp state, + * including provisioned networks. + * + * @param enrollee The enrollee instance to activate. + * @return int 0 if activation was successful, non-zero otherwise. + */ +static int +ztp_enrollee_activate(struct ztp_enrollee *enrollee) +{ + if (enrollee->active) + return 0; + + int ret = ztp_wpa_supplicant_get_interface_state(enrollee->wpas, enrollee->path, &enrollee->if_state); + if (ret < 0) { + zlog_warning_if(enrollee->interface, "failed to retreive initial interface state (%d); defaulting to unknown", ret); + enrollee->if_state = WPAS_INTERFACE_STATE_UNKNOWN; + } + + // Register for changes to the network list. + ret = sd_bus_match_signal(enrollee->dbus->bus, + &enrollee->slot_network_added, + WPAS_DBUS_SERVICE, + enrollee->path, + WPAS_DBUS_INTERFACE, + "NetworkAdded", + on_network_added, + enrollee); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to register handler for NetworkAdded signal (%d)", ret); + goto fail; + } + + ret = sd_bus_match_signal(enrollee->dbus->bus, + &enrollee->slot_network_removed, + WPAS_DBUS_SERVICE, + enrollee->path, + WPAS_DBUS_INTERFACE, + "NetworkRemoved", + on_network_removed, + enrollee); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to register handler for NetworkRemoved signal (%d)", ret); + goto fail; + } + + // Determine initial network count. + ret = ztp_get_interface_network_count(enrollee->wpas, enrollee->path, &enrollee->num_networks); + if (ret < 0) { + zlog_warning_if(enrollee->interface, "failed to retrieve initial network count (%d); assuming 0", ret); + enrollee->num_networks = 0; + } + + ret = ztp_enrollee_status_signals_initialize(enrollee); + if (ret < 0) + zlog_warning_if(enrollee->interface, "failed to initialize provisioning status signals (%d); ignoring", ret); + + enrollee->active = true; + enrollee->dpp_state = DPP_STATE_INACTIVE; + enrollee->state = ZTP_CONNECTIVITY_STATE_INACTIVE; + + zlog_info_if(enrollee->interface, "dpp enrollee activated"); + + // Finally, update the connectivity state, which will kickstart the state machine. + ztp_enrollee_update_connectivity_state(enrollee, ZTP_CONNECTIVITY_STATE_INITIALIZING); +out: + return ret; +fail: + ztp_enrollee_deactivate(enrollee); + goto out; +} + +/** + * @brief wpa_supplicant interface added callback. This will be invoked each + * time wpa_supplicant begins monitoring a new interface. + * + * @param enrollee The enrollee instance. + * @param interface The interface name that has been added to wpa_supplicant. + * @param path The d-bus path of the interface that has been added to + * wpa_supplicant. + */ +static void +on_interface_added(struct ztp_enrollee *enrollee, const char *interface, const char *path) +{ + if (enrollee->active) + ztp_enrollee_deactivate(enrollee); + + if (enrollee->path && strcmp(enrollee->path, path) != 0) { + zlog_info_if(interface, "d-bus path updated: %s -> %s", enrollee->path, path); + free(enrollee->path); + enrollee->path = NULL; + } + + if (!enrollee->path) { + enrollee->path = strdup(path); + if (!enrollee->path) { + zlog_panic_if(interface, "failed to allocate memory for interface path"); + return; + } + } + + int ret = ztp_enrollee_activate(enrollee); + if (ret < 0) { + zlog_panic_if(interface, "failed to activate upon arrival (%d)", ret); + return; + } +} + +/** + * @brief wpa_supplicant interface removed callback. This will be invoked each + * time wpa_supplicant stops monitoring an interface. + * + * @param enrollee The enrollee instance. + * @param interface The name of the interface that has been removed from wpa_supplicant + * @param path The path of the interface that has been removed from wpa_supplicant + */ +static void +on_interface_removed(struct ztp_enrollee *enrollee, const char *interface, const char *path) +{ + __unused(interface); + __unused(path); + + ztp_enrollee_deactivate(enrollee); +} + +/** + * @brief Handler function for interface presence changes. + * + * @param context The enrollee instance. + * @param presence The type of presence change that occurred. + * @param name The name of the interface the presence change occurred for. + * @param path The d-bus path of the interface the presence change occurred for. + */ +static void +on_interface_presence_changed(void *context, enum ztp_interface_presence presence, const char *interface, const char *path) +{ + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)context; + + if (strcmp(enrollee->interface, interface) != 0) + return; + + switch (presence) { + case ZTP_INTERFACE_ARRIVED: + on_interface_added(enrollee, interface, path); + break; + case ZTP_INTERFACE_DEPARTED: + on_interface_removed(enrollee, interface, path); + break; + default: + break; + } +} + +/** + * @brief Handler function for dpp frame rx. + * + * @param userdata The enrollee instance. + * @param type The type of public action frame received. + * @param frequency The radio frequency on which the frame was received. + */ +static void +on_dpp_frame_received(void *userdata, const char (*mac)[(DPP_MAC_LENGTH * 2) + (DPP_MAC_LENGTH - 1) + 1], enum dpp_public_action_frame_type type, uint32_t frequency) +{ + __unused(mac); + __unused(frequency); + + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + enum dpp_state dpp_state = DPP_STATE_UNKNOWN; + + switch (type) { + case DPP_PAF_AUTHENTICATION_REQUEST: + case DPP_PAF_AUTHENTICATION_RESPONSE: + dpp_state = DPP_STATE_AUTHENTICATED; + break; + default: + break; + } + + if (dpp_state != DPP_STATE_UNKNOWN) + ztp_enrollee_on_dpp_state_changed(enrollee, dpp_state); +} + +/** + * @brief Handler function for dpp frame tx. + * + * @param userdata The enrollee instance. + * @param type The type of public action frame sent. + * @param frequency The radio frequency on which the frame was sent. + */ +static void +on_dpp_frame_transmitted(void *userdata, const char (*mac)[(DPP_MAC_LENGTH * 2) + (DPP_MAC_LENGTH - 1) + 1], enum dpp_public_action_frame_type type, uint32_t frequency) +{ + __unused(mac); + __unused(frequency); + + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + enum dpp_state dpp_state = DPP_STATE_UNKNOWN; + + switch (type) { + case DPP_PAF_PRESENCE_ANNOUNCEMENT: + dpp_state = DPP_STATE_CHIRPING; + break; + default: + break; + } + + if (dpp_state != DPP_STATE_UNKNOWN) + ztp_enrollee_on_dpp_state_changed(enrollee, dpp_state); +} + +/** + * @brief Handler function for dpp chirp stopped event. + * + * @param userdata The enrollee instance. + */ +static void +on_dpp_chirp_stopped(void *userdata) +{ + __unused(userdata); + + // struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + // TODO: set a timer to re-check provisioning state after a short period +} + +/** + * @brief Handler function for dpp failures (generic). + * + * @param userdata The enrollee instance. + * @param details Details of the failure. + */ +static void +on_dpp_failure(void *userdata, const char *details) +{ + __unused(details); + + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + ztp_enrollee_on_dpp_state_changed(enrollee, DPP_STATE_TERMINATED); +} + +/** + * @brief Handler function for dpp authentication success. + * + * @param userdata The enrollee instance. + * @param is_initiator Indicates whether this device is the dpp initiator. + */ +static void +on_dpp_authentication_succeeded(void *userdata, bool is_initiator) +{ + __unused(is_initiator); + + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + ztp_enrollee_on_dpp_state_changed(enrollee, DPP_STATE_AUTHENTICATED); +} + +/** + * @brief Handler function for dpp authentication failures. + * + * @param userdata The enrollee instance. + */ +static void +on_dpp_authentication_failure(void *userdata) +{ + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + ztp_enrollee_on_dpp_state_changed(enrollee, DPP_STATE_TERMINATED); +} + +/** + * @brief Handler function for dpp configuration success. + * + * @param userdata The enrollee instance. + */ +static void +on_dpp_configuration_success(void *userdata) +{ + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + ztp_enrollee_on_dpp_state_changed(enrollee, DPP_STATE_PROVISIONED); +} + +/** + * @brief Handler function for dpp authentication failures. + * + * @param userdata The enrollee instance. + */ +static void +on_dpp_configuration_failure(void *userdata) +{ + struct ztp_enrollee *enrollee = (struct ztp_enrollee *)userdata; + ztp_enrollee_on_dpp_state_changed(enrollee, DPP_STATE_TERMINATED); +} + +/** + * @brief Event handler for wpa control socket events. + */ +static struct wpa_event_handler wpa_event_handler_enrollee = { + .dpp_chirp_stopped = on_dpp_chirp_stopped, + .dpp_failure = on_dpp_failure, + .dpp_authentication_failure = on_dpp_authentication_failure, + .dpp_authentication_success = on_dpp_authentication_succeeded, + .dpp_configuration_success = on_dpp_configuration_success, + .dpp_configuration_failure = on_dpp_configuration_failure, + .dpp_frame_received = on_dpp_frame_received, + .dpp_frame_transmitted = on_dpp_frame_transmitted, +}; + +/** + * @brief Uninitialize the enrollee device role. + * + * @param enrollee The enrollee to uninitialize. + */ +void +ztp_enrollee_uninitialize(struct ztp_enrollee *enrollee) +{ + ztp_enrollee_deactivate(enrollee); + ztp_wpa_supplicant_unregister_interface_presence_changed_callback(enrollee->wpas, on_interface_presence_changed, enrollee); + + if (enrollee->ctrl) { + wpa_controller_unregister_event_handler(enrollee->ctrl, &wpa_event_handler_enrollee, enrollee); + wpa_controller_uninitialize(enrollee->ctrl); + wpa_controller_destroy(&enrollee->ctrl); + } + + if (enrollee->dpp_properties_changed_handle) { + ztp_dbus_unregister_properties_changed_handler(enrollee->dpp_properties_changed_handle); + enrollee->dpp_properties_changed_handle = NULL; + } + + if (enrollee->properties_changed_handle) { + ztp_dbus_unregister_properties_changed_handler(enrollee->properties_changed_handle); + enrollee->properties_changed_handle = NULL; + } + + if (enrollee->interface) { + free(enrollee->interface); + enrollee->interface = NULL; + } + + if (enrollee->path) { + free(enrollee->path); + enrollee->path = NULL; + } + + if (enrollee->settings) { + enrollee->settings = NULL; + // 'settings' object is not owned, hence is not free()'ed. + } +} + +/** + * @brief Uninitialize and destroy an existing enrollee. + * + * @param enrollee The enrollee to uninitialize. + */ +void +ztp_enrollee_destroy(struct ztp_enrollee **enrollee) +{ + if (!enrollee || !*enrollee) + return; + + ztp_enrollee_uninitialize(*enrollee); + free(*enrollee); + + *enrollee = NULL; +} + +/** + * @brief The default control socket path to use if none is specified. + */ +#define WPA_CTRL_PATH_BASE_DEFAULT "/var/run/wpa_supplicant" + +/** + * @brief Create and initialize a new interface. + * + * @param interface The name of the interface. + * @param settings The settings to use. + * @param wpas A wpa_supplicant instance. + * @param dbus The d-bus client instance. + * @param loop The ztp event loop. + * @param penrollee Output argument to hold the enrollee instance. + * @return int + */ +int +ztp_enrollee_create(const char *interface, struct ztp_enrollee_settings *settings, struct ztp_wpa_supplicant *wpas, struct ztp_dbus_client *dbus, struct event_loop *loop, struct ztp_enrollee **penrollee) +{ + int ret; + struct ztp_enrollee *enrollee; + + enrollee = calloc(1, sizeof *enrollee); + if (!enrollee) + return -ENOMEM; + + enrollee->wpas = wpas; + enrollee->dbus = dbus; + enrollee->loop = loop; + enrollee->interface = strdup(interface); + enrollee->state = ZTP_CONNECTIVITY_STATE_INACTIVE; + enrollee->settings = settings; + enrollee->bootstrap_id = 0; + + if (!enrollee->interface) { + ret = -ENOMEM; + goto fail; + } + + enrollee->ctrl = wpa_controller_alloc(); + if (!enrollee->ctrl) { + zlog_error_if(enrollee->interface, "failed to allocare wpa controller (no memory)"); + ret = -ENOMEM; + goto fail; + } + + char ctrl_path[128]; + snprintf(ctrl_path, sizeof ctrl_path, "%s/%s", WPA_CTRL_PATH_BASE_DEFAULT, interface); + + ret = wpa_controller_initialize(enrollee->ctrl, ctrl_path, loop); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to initialize wpa controller (%d)", ret); + goto fail; + } + + ret = wpa_controller_register_event_handler(enrollee->ctrl, &wpa_event_handler_enrollee, enrollee); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to register wpa event handler (%d)", ret); + goto fail; + } + + // Register for interface property changes. + ret = ztp_dbus_register_properties_changed_handler(enrollee->dbus, + WPAS_DBUS_INTERFACE, + enrollee, + on_dbus_properties_changed, + &enrollee->properties_changed_handle); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to register 'PropertiesChanged' handler (%d)", ret); + goto fail; + } + + // Register for interface presence changes. + ret = ztp_wpa_supplicant_register_interface_presence_changed_callback(wpas, on_interface_presence_changed, enrollee); + if (ret < 0) { + zlog_error_if(enrollee->interface, "failed to register for interface presence changes (%d)", ret); + goto fail; + } + + ret = ztp_wpa_supplicant_get_interface_path(wpas, interface, &enrollee->path); + switch (ret) { + case -ENOENT: + zlog_warning_if(interface, "interface not present; deferring activation"); + break; + case 0: + ret = ztp_enrollee_activate(enrollee); + if (ret < 0) { + zlog_error_if(interface, "failed to activate enrollee (%d)", ret); + goto fail; + } + break; + default: + zlog_error_if(interface, "failed to retrieve d-bus path (%d)", ret); + goto fail; + } + + *penrollee = enrollee; + ret = 0; +out: + return ret; +fail: + ztp_enrollee_destroy(&enrollee); + goto out; +} diff --git a/src/core/ztp_enrollee.h b/src/core/ztp_enrollee.h new file mode 100644 index 0000000..330b9d2 --- /dev/null +++ b/src/core/ztp_enrollee.h @@ -0,0 +1,68 @@ + +#ifndef __ZTP_ENROLLEE_H__ +#define __ZTP_ENROLLEE_H__ + +#include +#include + +#include "dpp.h" +#include "led_ctrl.h" +#include "wpa_controller.h" +#include "wpa_supplicant.h" +#include "ztp.h" +#include "ztp_log.h" +#include "ztp_settings.h" +#include "ztp_wpa_supplicant.h" + +struct ztp_dbus_client; +struct ztp_dbus_properties_changed_handle; +struct ztp_enrollee_settings; + +/** + * @brief Describes a network interface that managed for zero touch + * provisioning. + */ +struct ztp_enrollee { + char *interface; + char *path; + bool active; + enum dpp_state dpp_state; + enum wpas_interface_state if_state; + enum ztp_connectivity_state state; + struct event_loop *loop; + struct ztp_enrollee_settings *settings; + struct ztp_dbus_client *dbus; + struct ztp_dbus_properties_changed_handle *dpp_properties_changed_handle; + struct ztp_dbus_properties_changed_handle *properties_changed_handle; + struct ztp_wpa_supplicant *wpas; + struct led_ctrl *led_status; + struct wpa_controller *ctrl; + sd_bus_slot *slot_network_added; + sd_bus_slot *slot_network_removed; + uint32_t num_networks; + uint32_t bootstrap_id; +}; + +/** + * @brief Create and initialize a new interface. + * + * @param interface The name of the interface. + * @param settings The settings to use. + * @param wpas A wpa_supplicant instance. + * @param dbus The d-bus client instance. + * @param loop The ztp event loop. + * @param penrollee Output argument to hold the enrollee instance. + * @return int + */ +int +ztp_enrollee_create(const char *interface, struct ztp_enrollee_settings *settings, struct ztp_wpa_supplicant *wpas, struct ztp_dbus_client *dbus, struct event_loop *loop, struct ztp_enrollee **penrollee); + +/** + * @brief Uninitialize and destroy an existing enrollee. + * + * @param enrollee The enrollee to uninitialize. + */ +void +ztp_enrollee_destroy(struct ztp_enrollee **enrollee); + +#endif //__ZTP_ENROLLEE_H__ diff --git a/src/core/ztp_enrollee_config.c b/src/core/ztp_enrollee_config.c new file mode 100644 index 0000000..2b71374 --- /dev/null +++ b/src/core/ztp_enrollee_config.c @@ -0,0 +1,328 @@ + +#include +#include +#include +#include +#include + +#include "json_parse.h" +#include "ztp_enrollee_config.h" +#include "ztp_log.h" + +/** + * @brief Json object key names for status.signals. + */ +#define JSON_PROPERTY_NAME_ENROLLEE_STATUS_SIGNALS "status.signals" +#define JSON_PROPERTY_NAME_ENROLLEE_STATUS_SIGNALS_LED "led" +#define JSON_PROPERTY_NAME_ENROLLEE_STATUS_SIGNALS_LED_NODE "sysfsNode" + +/** + * @brief Get the status signals led string context object + * + * @param context The enrollee settings object instance. + * @param name The name of the property. + * @return void* + */ +static void * +get_status_signals_led_string_context(void *context, const char *name) +{ + struct ztp_enrollee_settings *settings = (struct ztp_enrollee_settings *)context; + + if (strcmp(name, JSON_PROPERTY_NAME_ENROLLEE_STATUS_SIGNALS_LED_NODE) == 0) { + return &settings->status_signal_led_path; + } else { + return NULL; + } +} + +/** + * @brief Property map for enrollee status signals configuration. + */ +static struct json_property_parser enrollee_status_signals_led_properties[] = { + { + .name = JSON_PROPERTY_NAME_ENROLLEE_STATUS_SIGNALS_LED_NODE, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_status_signals_led_string_context, + }, +}; + +/** + * @brief Parser for the "led" property of the "status.signals" object + * + * @param parent The parent json object. + * @param name The name of the property. Must be "led". + * @param jobj The json object. + * @param context The ztp_enrollee_settings object to populate. + */ +static void +json_parse_status_signals_led(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_parse_object_s(jobj, enrollee_status_signals_led_properties, context); +} + +/** + * @brief Property map for enrollee bootstrap info configuration. + */ +static struct json_property_parser enrollee_status_signals_properties[] = { + { + .name = JSON_PROPERTY_NAME_ENROLLEE_STATUS_SIGNALS_LED, + .type = json_type_object, + .value = { + json_parse_status_signals_led, + }, + }, +}; + +/** + * @brief Parser for the "status.signals" property. + * + * @param parent The parent json object. + * @param name The name of the property. Must be "status.signals". + * @param jobj The json object. + * @param context The ztp_enrollee_settings object to populate. + */ +static void +json_parse_enrollee_status_signals(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_parse_object_s(jobj, enrollee_status_signals_properties, context); +} + +/** + * @brief Json object key names for bootstrap.info. + */ +#define JSON_PROPERTY_NAME_ENROLLEE_BOOTSTRAP_INFO "bootstrap.info" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_MAC "mac" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_KEY "key" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_KEY_ID "keyId" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_INFO "info" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_CURVE "curve" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_CHANNEL "channel" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_ENGINE_ID "engineId" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_ENGINE_PATH "enginePath" +#define JSON_PROPERTY_NAME_BOOTSTRAP_INFO_TYPE "type" + +/** + * @brief Get the context for the string properties of the bootstrap info object. + * + * @param context The parent context. Must be of type struct dpp_bootstrap_info. + * @param name The name of the child property to retrieve the context for. + * @return void* The context for the child property with key 'name'. + */ +static void * +get_bootstrap_info_string_context(void *context, const char *name) +{ + struct dpp_bootstrap_info *bi = (struct dpp_bootstrap_info *)context; + + if (strcmp(name, JSON_PROPERTY_NAME_BOOTSTRAP_INFO_MAC) == 0) { + return &bi->mac; + } else if (strcmp(name, JSON_PROPERTY_NAME_BOOTSTRAP_INFO_KEY) == 0) { + return &bi->key; + } else if (strcmp(name, JSON_PROPERTY_NAME_BOOTSTRAP_INFO_KEY_ID) == 0) { + return &bi->key_id; + } else if (strcmp(name, JSON_PROPERTY_NAME_BOOTSTRAP_INFO_INFO) == 0) { + return &bi->info; + } else if (strcmp(name, JSON_PROPERTY_NAME_BOOTSTRAP_INFO_CURVE) == 0) { + return &bi->curve; + } else if (strcmp(name, JSON_PROPERTY_NAME_BOOTSTRAP_INFO_CHANNEL) == 0) { + return &bi->channel; + } else if (strcmp(name, JSON_PROPERTY_NAME_BOOTSTRAP_INFO_ENGINE_ID) == 0) { + return &bi->engine_id; + } else if (strcmp(name, JSON_PROPERTY_NAME_BOOTSTRAP_INFO_ENGINE_PATH) == 0) { + return &bi->engine_path; + } else { + return NULL; + } +} + +/** + * @brief Parser for the "type" bootstrap info property. + * + * @param parent The parent json object. + * @param name The name of the property. Must be "type". + * @param jobj The json object. + * @param context The dpp_bootstrap_info object to populate. + */ +static void +json_parse_bootstrap_info_type(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct dpp_bootstrap_info *bi = (struct dpp_bootstrap_info *)context; + const char *typestr = json_object_get_string(jobj); + if (!typestr) + return; + + bi->type = parse_dpp_bootstrap_type(typestr); +} + +/** + * @brief Property map for enrollee bootstrap info configuration. + */ +static struct json_property_parser enrollee_bootstrap_info_properties[] = { + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_MAC, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_bootstrap_info_string_context, + }, + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_KEY, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_bootstrap_info_string_context, + }, + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_KEY_ID, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_bootstrap_info_string_context, + }, + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_INFO, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_bootstrap_info_string_context, + }, + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_CURVE, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_bootstrap_info_string_context, + }, + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_CHANNEL, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_bootstrap_info_string_context, + }, + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_ENGINE_ID, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_bootstrap_info_string_context, + }, + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_ENGINE_PATH, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_bootstrap_info_string_context, + }, + { + .name = JSON_PROPERTY_NAME_BOOTSTRAP_INFO_TYPE, + .type = json_type_string, + .value = { + json_parse_bootstrap_info_type, + }, + }, +}; + +/** + * @brief Parser for the "bootstrap.info" property. + * + * @param parent The parent json object. + * @param name The name of the property. Must be "bootstrap.info". + * @param jobj + * @param context + */ +static void +json_parse_enrollee_bootstrap_info(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct ztp_enrollee_settings *settings = (struct ztp_enrollee_settings *)context; + json_parse_object_s(jobj, enrollee_bootstrap_info_properties, &settings->bootstrap); +} + +/** + * @brief Property map for enrollee configuration. + */ +static struct json_property_parser enrollee_properties[] = { + { + .name = JSON_PROPERTY_NAME_ENROLLEE_BOOTSTRAP_INFO, + .type = json_type_object, + .value = { + json_parse_enrollee_bootstrap_info, + }, + }, + { + .name = JSON_PROPERTY_NAME_ENROLLEE_STATUS_SIGNALS, + .type = json_type_object, + .value = { + json_parse_enrollee_status_signals, + }, + }, +}; + +/** + * @brief Parses a json-formatted enrollee configuration file. + * + * @param file The path of the file to parse. + * @param settings The enrollee settings to fill in. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +ztp_enrollee_config_parse(const char *file, struct ztp_enrollee_settings *settings) +{ + int ret = json_parse_file_s(file, enrollee_properties, settings, NULL); + if (ret < 0) { + zlog_error("failed to parse enrollee settings file '%s' (%d)", file, ret); + return ret; + } + + return 0; +} + +/** + * @brief Initializes the settings structure for use. + * + * @param settings The settings to initialize. + */ +void +ztp_enrollee_settings_initialize(struct ztp_enrollee_settings *settings) +{ + explicit_bzero(settings, sizeof *settings); + settings->bootstrap.type = DPP_BOOTSTRAP_UNKNOWN; +} + +/** + * @brief Uninitializes enrollee settings, freeing any owned resources. + * + * @param settings The settings object to uninitialize. + */ +void +ztp_enrollee_settings_uninitialize(struct ztp_enrollee_settings *settings) +{ + if (settings->status_signal_led_path) { + free(settings->status_signal_led_path); + settings->status_signal_led_path = NULL; + } + + dpp_bootstrap_info_uninitialize(&settings->bootstrap); +} diff --git a/src/core/ztp_enrollee_config.h b/src/core/ztp_enrollee_config.h new file mode 100644 index 0000000..2c7d5ec --- /dev/null +++ b/src/core/ztp_enrollee_config.h @@ -0,0 +1,41 @@ + +#ifndef __ZTP_ENROLLEE_CONFIG_H__ +#define __ZTP_ENROLLEE_CONFIG_H__ + +#include "dpp.h" + +/** + * @brief Enrollee DPP device role settings. + */ +struct ztp_enrollee_settings { + char *status_signal_led_path; + struct dpp_bootstrap_info bootstrap; +}; + +/** + * @brief Parses a json-formatted enrollee configuration file. + * + * @param file The path of the file to parse. + * @param settings The enrollee settings to fill in. + * @return int 0 if parsing was successful, non-zero otherwise. + */ +int +ztp_enrollee_config_parse(const char *file, struct ztp_enrollee_settings *settings); + +/** + * @brief Initializes the settings structure for use. + * + * @param settings + */ +void +ztp_enrollee_settings_initialize(struct ztp_enrollee_settings *settings); + +/** + * @brief Uninitializes enrollee settings, freeing any owned resources. + * + * @param settings The settigns object to uninitialize. + */ +void +ztp_enrollee_settings_uninitialize(struct ztp_enrollee_settings *settings); + +#endif //__ZTP_ENROLLEE_CONFIG_H__ diff --git a/src/core/ztp_settings.c b/src/core/ztp_settings.c new file mode 100644 index 0000000..bebc49b --- /dev/null +++ b/src/core/ztp_settings.c @@ -0,0 +1,981 @@ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "file_utils.h" +#include "json_parse.h" +#include "ztp_log.h" +#include "ztp_settings.h" + +/** + * @brief Settings changed event handler. + */ +struct ztp_settings_changed_event_handler { + struct list_head list; + ztp_settings_changed_fn callback; + void *context; +}; + +/** + * @brief Signals that the ztp settings have changed. + * + * @param settings The settings object that has changed. + * @param changed_item The setting item that changed. + */ +void +ztp_settings_signal_changed(struct ztp_settings *settings, enum ztp_settings_changed_item changed_item) +{ + struct ztp_settings_changed_event changed_event = { + .changed_item = changed_item, + }; + const struct ztp_settings_changed_event_handler *changed_handler; + const struct ztp_settings_changed_event_handler *changed_handler_tmp; + + list_for_each_entry_safe (changed_handler, changed_handler_tmp, &settings->change_handlers, list) { + changed_handler->callback(settings, &changed_event, changed_handler->context); + } +} + +/** + * @brief Finds a changed handler, given a callback and context argument. + * + * @param settings The ztp settings to search for the specified handler. + * @param callback The callback function associated with the handler. + * @param context The context argument assoicated with the callback. + * @return struct ztp_settings_changed_event_handler* If such a handler has + * been registered, otherwise NULL. + */ +struct ztp_settings_changed_event_handler * +find_settings_changed_handler(struct ztp_settings *settings, ztp_settings_changed_fn callback, void *context) +{ + struct ztp_settings_changed_event_handler *change_handler; + + list_for_each_entry (change_handler, &settings->change_handlers, list) { + if (change_handler->callback == callback && change_handler->context == context) { + return change_handler; + } + } + + return NULL; +} + +/** + * @brief Registers a settings changed handler. The specified callback function + * will be invoked with the context argument and a changed event structure each + * time the settings change. + * + * @param settings The ztp settings to monitor change events for. + * @param callback The callback function to invoke oin change. + * @param context An additional context-specific argument to be passed to the callback function. + * @return int 0 if the handler was successfully registered, non-zero otherwise. + */ +int +ztp_settings_register_change_handler(struct ztp_settings *settings, ztp_settings_changed_fn callback, void *context) +{ + if (find_settings_changed_handler(settings, callback, context)) + return 0; + + struct ztp_settings_changed_event_handler *changed_handler = malloc(sizeof *changed_handler); + if (!changed_handler) { + zlog_error("failed to allocate memory for ztp settings changed handler"); + return -ENOMEM; + } + + changed_handler->callback = callback; + changed_handler->context = context; + list_add(&changed_handler->list, &settings->change_handlers); + + return 0; +} + +/** + * @brief Unregisters a settings changed handler. + * + * @param settings The ztp settings to remove the handler from. + * @param callback The previously registered callback. + * @param context The context previously registered with the callback. + */ +void +ztp_settings_unregister_change_handler(struct ztp_settings *settings, ztp_settings_changed_fn callback, void *context) +{ + struct ztp_settings_changed_event_handler *change_handler = find_settings_changed_handler(settings, callback, context); + if (!change_handler) + return; + + list_del(&change_handler->list); + free(change_handler); +} + +/** + * @brief Uninitializes a role settings entry. + * + * @param settings The settings object to uninitialize. + */ +static void +ztp_device_role_settings_uninitialize(struct ztp_device_role_settings *settings) +{ + if (settings->activation_unit) { + free(settings->activation_unit); + settings->activation_unit = NULL; + } + + if (settings->interface) { + free(settings->interface); + settings->interface = NULL; + } + + if (settings->path) { + free(settings->path); + settings->path = NULL; + } + + switch (settings->role) { + case DPP_DEVICE_ROLE_ENROLLEE: { + ztp_enrollee_settings_uninitialize(&settings->enrollee); + break; + } + case DPP_DEVICE_ROLE_CONFIGURATOR: + ztp_configurator_settings_uninitialize(&settings->configurator); + break; + default: + break; + } +} + +/** + * @brief Create an initialize a new interface settings entry. + * + * @return struct ztp_interface_settings_entry* + */ +struct ztp_device_role_settings_entry * +ztp_device_role_settings_entry_initialize(void) +{ + struct ztp_device_role_settings_entry *entry = calloc(1, sizeof *entry); + if (!entry) { + zlog_warning("failed to allocate memory for role settings entry"); + return NULL; + } + + INIT_LIST_HEAD(&entry->list); + return entry; +} + +/** + * @brief Finds a device role settings entry, given an interface name. + * + * @param settings The ztp settings structure to search. + * @param interface The interface name to lookup settings for. + * @return struct ztp_device_role_settings_entry* + */ +static struct ztp_device_role_settings_entry * +ztp_settings_find_device_role_settings_entry(const struct ztp_settings *settings, const char *interface) +{ + struct ztp_device_role_settings_entry *entry; + + list_for_each_entry (entry, &settings->role_settings, list) { + if (strcmp(entry->settings.interface, interface) == 0) + return entry; + } + + return NULL; +} + +/** + * @brief Finds role settings, given an interface name. + * + * @param settings The ztp settings structure to search. + * @param interface The name of the interface to lookup settings for. + * @return struct ztp_device_role_settings* + */ +struct ztp_device_role_settings * +ztp_settings_find_device_role_settings(const struct ztp_settings *settings, const char *interface) +{ + struct ztp_device_role_settings_entry *entry = ztp_settings_find_device_role_settings_entry(settings, interface); + if (entry) + return &entry->settings; + + return NULL; +} + +/** + * @brief Uninitialize an interface settings entry. + * + * @param entry The interface settings entry to uninitialize. + */ +static void +ztp_device_role_settings_entry_uninitialize(struct ztp_device_role_settings_entry *entry) +{ + ztp_device_role_settings_uninitialize(&entry->settings); + list_del(&entry->list); +} + +/** + * @brief Adds a new interface settings entry to ztpd. + * + * @param ztpd The global ztpd instance. + * @param entry The entry to add. + */ +static void +ztp_settings_add_device_role_settings(struct ztp_settings *settings, struct ztp_device_role_settings_entry *entry) +{ + struct ztp_device_role_settings_entry *existing = ztp_settings_find_device_role_settings_entry(settings, entry->settings.interface); + if (existing) { + ztp_device_role_settings_entry_uninitialize(existing); + free(existing); + } + + list_add(&entry->list, &settings->role_settings); +} + +/** + * @brief Function to parse each "ui.activation.gpio" configuration option + * object entry. + * + * @param parent The parent object. + * @param name The name of the json entry. + * @param jobj The json object for the entry. + * @param context A pointer to a ztpd instance. Will be of type (struct ztpd *). + */ +static void +json_parse_ui_activation_gpio_entry(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct ztp_settings *settings = (struct ztp_settings *)context; + + if (strcmp(name, "chip") == 0) { + if (!json_validate_object_type(name, jobj, json_type_string)) + return; + const char *chip = json_object_get_string(jobj); + settings->ui_activation_gpio_chip = strdup(chip); + } else if (strcmp(name, "line") == 0) { + json_type type = json_object_get_type(jobj); + switch (type) { + case json_type_string: { + const char *line_name = json_object_get_string(jobj); + settings->ui_activation_gpio_line_name = strdup(line_name); + break; + } + case json_type_int: { + int32_t line = json_object_get_int(jobj); + settings->ui_activation_gpio_line = line; + break; + } + default: + return; + } + } else if (strcmp(name, "debounceDelay") == 0) { + int32_t debounce_delay = json_object_get_int(jobj); + settings->ui_activation_gpio_delay = debounce_delay; + } +} + +/** + * @brief Functon to parse "ui.activation.gpio" configuration option. + * + * @param parent The parent object. + * @param name The name of the json entry. Must be "ui.activation.gpio". + * @param jobj The json object value for "ui.activation.gpio". + * @param ztpd The global ztpd instance. + */ +static void +json_parse_ui_activation_gpio(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + json_for_each_object(jobj, json_parse_ui_activation_gpio_entry, context); +} + +/** + * @brief Functon to parse "ui.activation.unit" configuration option. + * + * @param parent The parent object. + * @param name The name of the json entry. Must be "ui.activation.unit". + * @param jobj The json object value for "ui.activation.unit". + * @param ztpd The global ztpd instance. + */ +static void +json_parse_ui_activation_unit(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct ztp_settings *settings = (struct ztp_settings *)context; + + const char *ui_activation_unit = json_object_get_string(jobj); + settings->ui_activation_unit = strdup(ui_activation_unit); +} + +/** + * @brief Function to parse "ui.activation" configuration option array entry. + * + * @param parent The parent object. + * @param array The containing array object. + * @param name The name of the parent object ("ui.activation"). + * @param jobj The array element value. + * @param index The array element index. + * @param type The type of the array element (json_type_string). + * @param context The ztpd global instance. + */ +static void +json_parse_ui_activation_array_entry(struct json_object *parent, struct json_object *array, const char *name, struct json_object *jobj, uint32_t index, json_type type, void *context) +{ + __unused(parent); + __unused(array); + __unused(name); + __unused(index); + __unused(type); + + struct ztp_settings *settings = (struct ztp_settings *)context; + + const char *value = json_object_get_string(jobj); + if (strcmp(value, "command") == 0) { + settings->ui_activation_command = true; + } else if (strcmp(value, "gpio") == 0) { + settings->ui_activation_gpio = true; + } +} + +/** + * @brief Json object key names for "device.roles" array and child objects. + */ +#define JSON_PROPERTY_NAME_DEVICE_ROLES "device.roles" +#define JSON_PROPERTY_NAME_DEVICE_ROLE_ROLE "role" +#define JSON_PROPERTY_NAME_DEVICE_ROLE_INTERFACE "interface" +#define JSON_PROPERTY_NAME_DEVICE_ROLE_SETTINGS_PATH "settingsPath" +#define JSON_PROPERTY_NAME_DEVICE_ROLE_ACTIVATION_UNIT "activationUnit" + +/** + * @brief Get the context for the string properties of the bootstrap info object. + * + * @param context The parent context. Must be of type struct dpp_bootstrap_info. + * @param name The name of the child property to retrieve the context for. + * @return void* The context for the child property with key 'name'. + */ +static void * +get_device_role_string_context(void *context, const char *name) +{ + struct ztp_device_role_settings *settings = (struct ztp_device_role_settings *)context; + + if (strcmp(name, JSON_PROPERTY_NAME_DEVICE_ROLE_INTERFACE) == 0) { + return &settings->interface; + } else if (strcmp(name, JSON_PROPERTY_NAME_DEVICE_ROLE_SETTINGS_PATH) == 0) { + return &settings->path; + } else if (strcmp(name, JSON_PROPERTY_NAME_DEVICE_ROLE_ACTIVATION_UNIT) == 0) { + return &settings->activation_unit; + } else { + return NULL; + } +} + +/** + * @brief Parses individual "role" property of a device role entry. + * + * @param parent The parent object. + * @param name The name of the json entry. Must be "role". + * @param jobj The json object value. + * @param ztpd The ztp_device_role_settings instance to populate. + */ +static void +json_parse_device_role_rolestr(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct ztp_device_role_settings *settings = (struct ztp_device_role_settings *)context; + + const char *device_role = json_object_get_string(jobj); + settings->role = dpp_device_role_parse(device_role); + + switch (settings->role) { + case DPP_DEVICE_ROLE_ENROLLEE: + ztp_enrollee_settings_initialize(&settings->enrollee); + break; + case DPP_DEVICE_ROLE_CONFIGURATOR: + ztp_configurator_settings_initialize(&settings->configurator); + break; + default: + break; + } +} + +/** + * @brief Property map for enrollee bootstrap info configuration. + */ +static struct json_property_parser device_role_entry_properties[] = { + { + .name = JSON_PROPERTY_NAME_DEVICE_ROLE_ROLE, + .type = json_type_string, + .value = { + json_parse_device_role_rolestr, + }, + }, + { + .name = JSON_PROPERTY_NAME_DEVICE_ROLE_INTERFACE, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_device_role_string_context, + }, + { + .name = JSON_PROPERTY_NAME_DEVICE_ROLE_SETTINGS_PATH, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_device_role_string_context, + }, + { + .name = JSON_PROPERTY_NAME_DEVICE_ROLE_ACTIVATION_UNIT, + .type = json_type_string, + .value = { + json_parse_string_generic, + }, + .get_context = get_device_role_string_context, + }, +}; + +/** + * @brief Function to parse an "device.roles" json object. + * + * If a valid device role object was found, it is added to the passed in + * ztp_settings structure. If settings already existed for the interface name + * in question, they will be replaced by the newly parsed settings. + * + * @param parent The parent object. + * @param array The containing array object. + * @param name The name of the json entry. + * @param jobj The json object value associated with the device role settings. + * @param index The array index of the object. + * @param type The type of the object. + * @param context The struct ztp_settings object to populate. + */ +static void +json_parse_device_roles(struct json_object *parent, struct json_object *array, const char *name, struct json_object *jobj, uint32_t index, json_type type, void *context) +{ + __unused(parent); + __unused(array); + __unused(name); + __unused(index); + __unused(type); + + struct ztp_settings *settings = (struct ztp_settings *)context; + struct ztp_device_role_settings_entry *entry = ztp_device_role_settings_entry_initialize(); + if (!entry) + return; + + json_parse_object_s(jobj, device_role_entry_properties, &entry->settings); + + if (!entry->settings.interface) { + zlog_warning("role settings entry missing interface name; ignoring"); + ztp_device_role_settings_entry_uninitialize(entry); + free(entry); + return; + } + + ztp_settings_add_device_role_settings(settings, entry); +} + +/** + * @brief Function to parse "device.roles.exclusive" configuration option. + * + * @param parent The parent object. + * @param name The name of the json entry. Must be "device.roles.exclusive". + * @param jobj The json object value for "device.roles.exclusive". + * @param ztpd The global ztpd instance. + */ +static void +json_parse_device_roles_exclusive(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct ztp_settings *settings = (struct ztp_settings *)context; + + bool exclusive = json_object_get_boolean(jobj); + settings->dpp_roles_exclusive = exclusive; +} + +/** + * @brief Parses the "device.roles.activated" object (not the entries). + * + * @param parent The parent object. + * @param name The name, which will always be NULL. + * @param jobj The array object. + * @param context The array context. + */ +static void +json_parse_device_roles_activated(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + __unused(name); + + struct ztp_settings *settings = (struct ztp_settings *)context; + + if (settings->dpp_roles_activated_json == NULL) { + settings->dpp_roles_activated_json = jobj; + json_object_get(jobj); + } +} + +/** + * @brief Parses individual "device.roles.activated" entries. + * + * @param parent The parent object. + * @param array The containing array object. + * @param name The name of the json object. + * @param jobj The json object value. + * @param index The index of the json value within the array. + * @param context A pointer to the ztpd global instance. This must be of type 'struct ztpd'. + */ +static void +json_parse_device_roles_activated_entry(struct json_object *parent, struct json_object *array, const char *name, struct json_object *jobj, uint32_t index, json_type type, void *context) +{ + __unused(parent); + __unused(array); + __unused(name); + __unused(index); + __unused(type); + + struct ztp_settings *settings = (struct ztp_settings *)context; + + const char *rolestr = json_object_get_string(jobj); + enum dpp_device_role role = dpp_device_role_parse(rolestr); + + if (role != DPP_DEVICE_ROLE_UNKNOWN) + settings->dpp_roles_activated[role] = true; +} + +/** + * @brief Array of supported configuration file options. + */ +static struct json_property_parser ztp_config_properties[] = { + { + .name = "ui.activation", + .type = json_type_array, + .array = { + json_parse_ui_activation_array_entry, + json_type_string, + }, + }, + { + .name = "ui.activation.gpio", + .type = json_type_object, + .value = { + json_parse_ui_activation_gpio, + }, + }, + { + .name = "ui.activation.unit", + .type = json_type_string, + .value = { + json_parse_ui_activation_unit, + }, + }, + { + .name = "device.roles", + .type = json_type_array, + .array = { + json_parse_device_roles, + json_type_object, + }, + }, + { + .name = "device.roles.exclusive", + .type = json_type_boolean, + .value = { + json_parse_device_roles_exclusive, + }, + }, + { + .name = "device.roles.activated", + .type = json_type_array, + .value = { + json_parse_device_roles_activated, + }, + .array = { + json_parse_device_roles_activated_entry, + json_type_string, + }, + }, +}; + +/** + * @brief Creates and initializes a new ztp settings intance. + * + * @param config_file The full path of the configuration file. + * @return struct ztp_settings* A pointer to the settings object. + */ +static struct ztp_settings * +ztp_settings_create(const char *config_file) +{ + size_t config_file_length = strlen(config_file) + 1; + struct ztp_settings *settings = malloc((sizeof *settings) * config_file_length); + if (!settings) { + zlog_error("failed to allocate memory for ztp settings"); + return NULL; + } + + explicit_bzero(settings, sizeof *settings); + INIT_LIST_HEAD(&settings->role_settings); + INIT_LIST_HEAD(&settings->change_handlers); + memcpy(settings->config_file, config_file, config_file_length); + + return settings; +} + +/** + * @brief Parses a ztp settings file. + * + * @param config_file The full path of the configuration file to parse. + * @param psettings An output pointer to receive a parsed settings object. The + * caller is responsible for calling ztp_settings_destroy on this object. + * @return int + */ +int +ztp_settings_parse(const char *config_file, struct ztp_settings **psettings) +{ + struct ztp_settings *settings = ztp_settings_create(config_file); + if (!settings) { + zlog_error("failed to allocate memory for ztp settings"); + return -ENOMEM; + } + + int ret = json_parse_file_s(config_file, ztp_config_properties, settings, &settings->json); + if (ret < 0) { + zlog_error("failed to parse ztp settings from %s (%d)", config_file, ret); + goto fail; + } + + struct ztp_device_role_settings_entry *entry; + list_for_each_entry (entry, &settings->role_settings, list) { + switch (entry->settings.role) { + case DPP_DEVICE_ROLE_ENROLLEE: + ret = ztp_enrollee_config_parse(entry->settings.path, &entry->settings.enrollee); + break; + case DPP_DEVICE_ROLE_CONFIGURATOR: + ret = ztp_configurator_config_parse(entry->settings.path, &entry->settings.configurator); + break; + default: + ret = -EINVAL; + break; + } + + if (ret < 0) { + zlog_error("failed to parse '%s' settings from '%s' (%d)", dpp_device_role_str(entry->settings.role), entry->settings.path, ret); + goto fail; + } + } + + *psettings = settings; + ret = 0; +out: + return ret; +fail: + if (settings) + ztp_settings_destroy(&settings); + goto out; +} + +/** + * @brief Persists settings to file descriptor. + * + * @param fd The file descriptor to write the settings to. + * @param settings The settings to write to file. + * @return int 0 if the settings were successfully written to file, non-zero otherwise. + */ +static int +ztp_settings_persist_fd(int fd, const struct ztp_settings *settings) +{ + static const int JSON_C_SERIALIZE_FLAGS = (0 + | JSON_C_TO_STRING_NOSLASHESCAPE // don't escape paths + | JSON_C_TO_STRING_PRETTY // make it look good + | JSON_C_TO_STRING_SPACED // minimize whitespace + | JSON_C_TO_STRING_PRETTY_TAB // use a full tab character + ); + + struct json_object *jsettings = settings->json; + if (!jsettings) { + zlog_error("serialized json settings not found"); + return -EINVAL; + } + + int ret = json_object_to_fd(fd, jsettings, JSON_C_SERIALIZE_FLAGS); + if (ret < 0) { + zlog_error("failed to write settings to file descriptor (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Macros to help defined the temporary path string for writing the + * settings file. + */ +#define TMP_TEMPLATE_SUFFIX_STRING ".XXXXXX" + +/** + * @brief Persists ztp settings to file. + * + * @param filename The filename to write the settings to. + * @param settings The settings to write to file. + * @return int 0 if the settings were successfully written to file, non-zero otherwise. + */ +int +ztp_settings_persist(const char *filename, const struct ztp_settings *settings) +{ + int ret; + int fd = -1; + + char *filename_target = NULL; + ret = get_link_target(filename, &filename_target); + if (ret < 0) { + zlog_error("failed to resolve ztp settings file '%s' link target (%d)", filename, ret); + return ret; + } + + if (filename_target) + filename = filename_target; + + size_t filename_length = strlen(filename); + char *pathtmp = malloc(filename_length + ARRAY_SIZE(TMP_TEMPLATE_SUFFIX_STRING)); + if (!pathtmp) { + zlog_error("failed to allocate memory for temp configurator settings file path"); + ret = -ENOMEM; + goto out; + } + + memcpy(pathtmp, filename, filename_length); + memcpy(pathtmp + filename_length, TMP_TEMPLATE_SUFFIX_STRING, ARRAY_SIZE(TMP_TEMPLATE_SUFFIX_STRING)); + + fd = mkstemp(pathtmp); + if (fd < 0) { + ret = -errno; + zlog_error("failed to create temporary file for ztp settings (%d)", ret); + goto out; + } + + ret = ztp_settings_persist_fd(fd, settings); + if (ret < 0) { + zlog_error("failed to write ztp settings to temporary file (%d)", ret); + goto out; + } + + fdatasync(fd); + + ret = rename(pathtmp, filename); + if (ret < 0) { + ret = -errno; + zlog_error("failed to move temporary settings file to target file (%d)", ret); + goto out; + } + + zlog_debug("ztp settings persisted to '%s'", filename); +out: + if (fd != -1) + close(fd); + if (filename_target) + free(filename_target); + if (pathtmp) + free(pathtmp); + + return ret; +} + +/** + * @brief Uninitialize ztpd daemon options. + * + * @param settings The options to uninitialize. + */ +void +ztp_settings_uninitialize(struct ztp_settings *settings) +{ + if (settings->ui_activation_gpio_chip) { + free(settings->ui_activation_gpio_chip); + settings->ui_activation_gpio_chip = NULL; + } + + if (settings->ui_activation_gpio_line_name) { + free(settings->ui_activation_gpio_line_name); + settings->ui_activation_gpio_line_name = NULL; + } + + if (settings->ui_activation_unit) { + free(settings->ui_activation_unit); + settings->ui_activation_unit = NULL; + } + + if (settings->dpp_roles_activated_json) { + json_object_put(settings->dpp_roles_activated_json); + settings->dpp_roles_activated_json = NULL; + } + + if (settings->json) { + json_object_put(settings->json); + settings->json = NULL; + } + + struct ztp_device_role_settings_entry *entry; + struct ztp_device_role_settings_entry *entrytmp; + + list_for_each_entry_safe (entry, entrytmp, &settings->role_settings, list) { + ztp_device_role_settings_entry_uninitialize(entry); + free(entry); + } + + struct ztp_settings_changed_event_handler *change_handler; + struct ztp_settings_changed_event_handler *change_handler_tmp; + list_for_each_entry_safe (change_handler, change_handler_tmp, &settings->change_handlers, list) { + ztp_settings_unregister_change_handler(settings, change_handler->callback, change_handler->context); + } +} + +/** + * @brief Uninitializes and destroys a ztp settings instance. + * + * @param settings A pointer to the settings instance to destroy. This function + * will set the memory this references to NULL, so the pointer must not be + * accessed following this call. + */ +void +ztp_settings_destroy(struct ztp_settings **settings) +{ + if (!settings || !*settings) + return; + + ztp_settings_uninitialize(*settings); + free(*settings); + + *settings = NULL; +} + +/** + * @brief Synchronizes the in-memory activated roles with those stored in the + * json object. This is a one-way function which will replace anything that + * currently exists in the json object. + * + * @param settings The ztp settings. + * @return int 0 if the json object associated with activated roles was + * succcessfully updated to reflect the in-memory activated roles. + */ +static int +ztp_settings_role_disposition_synchronize_json(struct ztp_settings *settings) +{ + int ret; + struct json_object *roles_activated = settings->dpp_roles_activated_json; + + size_t roles_activated_num = json_object_array_length(roles_activated); + if (roles_activated_num > 0) { + ret = json_object_array_del_idx(roles_activated, 0, roles_activated_num); + if (ret < 0) { + zlog_error("failed to clear activated roles json array (%d)", ret); + return ret; + } + } + + for (size_t i = 0, index = 0; i < ARRAY_SIZE(settings->dpp_roles_activated); i++) { + if (!settings->dpp_roles_activated[i]) + continue; + + const char *role = dpp_device_role_str((enum dpp_device_role)i); + struct json_object *value = json_object_new_string(role); + if (!value) { + zlog_error("failed to allocate memory for device role string"); + return -ENOMEM; + } + + ret = json_object_array_put_idx(roles_activated, index, value); + if (ret < 0) { + zlog_error("failed to add activated role to activated role json array (%d)", ret); + json_object_put(value); + return ret; + } + + zlog_debug("device role '%s' added to activated json array", role); + index++; + } + + return 0; +} + +/** + * @brief Set the disposition of a role. + * + * @param settings The settings instance to update. + * @param role The role to set the disposition for. + * @param activate The role disposition. + * @return true If the role disposition was changed + * @return false If the role disposition was not changed. + */ +static bool +set_role_disposition(struct ztp_settings *settings, enum dpp_device_role role, bool activate) +{ + if (settings->dpp_roles_activated[role] == activate) + return false; + + const char *rolestr = dpp_device_role_str(role); + settings->dpp_roles_activated[role] = activate; + zlog_debug("role '%s' disposition set to '%s'", rolestr, activate ? "activated" : "deactivated"); + + return true; +} + +/** + * @brief Sets the device role setting to activated or deactivated. Note that + * this does not necessarily affect the functionality of any running daemon; + * this only updates the setting. + * + * If exclusive mode is set, and the new role disposition is to activate, then + * the existing activated role (if one exists) will be changed to disabled. + * + * @param settings The settings instance to update. + * @param role The role to set the disposition for. + * @param activate The role disposition. + * @return int The number of roles that changed disposition successfully + * updated, otherwise a negative value indicating the cause for the failure. + */ +int +ztp_settings_set_device_role_disposition(struct ztp_settings *settings, enum dpp_device_role role, bool activate) +{ + int num_role_changes = 0; + + if (set_role_disposition(settings, role, activate)) + num_role_changes++; + + if (settings->dpp_roles_exclusive) { + enum dpp_device_role role_peer = dpp_device_role_peer(role); + if (dpp_device_role_is_valid(role_peer)) { + if (set_role_disposition(settings, role_peer, !activate)) + num_role_changes++; + } + } + + if (num_role_changes == 0) + return 0; + + int ret = ztp_settings_role_disposition_synchronize_json(settings); + if (ret < 0) { + zlog_error("failed to synchronize role dispoition change with json settings (%d)", ret); + return ret; + } + + ret = ztp_settings_persist(settings->config_file, settings); + if (ret < 0) { + zlog_error("failed to persist updated ztp settings to file (%d)", ret); + return ret; + } + + ztp_settings_signal_changed(settings, ZTP_SETTING_CHANGED_ITEM_DEVICE_ROLES); + return num_role_changes; +} diff --git a/src/core/ztp_settings.h b/src/core/ztp_settings.h new file mode 100644 index 0000000..62cb236 --- /dev/null +++ b/src/core/ztp_settings.h @@ -0,0 +1,170 @@ + +#ifndef __ZTP_SETTINGS_H__ +#define __ZTP_SETTINGS_H__ + +#include +#include + +#include + +#include "dpp.h" +#include "ztp_configurator_config.h" +#include "ztp_enrollee_config.h" + +/** + * @brief Type mapping a role and interface to its settings. + */ +struct ztp_device_role_settings { + enum dpp_device_role role; + char *path; + char *interface; + char *activation_unit; + union { + struct ztp_enrollee_settings enrollee; + struct ztp_configurator_settings configurator; + }; +}; + +/** + * @brief List/map entry for ztp role settings. + */ +struct ztp_device_role_settings_entry { + struct list_head list; + struct ztp_device_role_settings settings; +}; + +/** + * @brief Default location of the ztpd configuration file. The daemon will + * attempt to use this file if no configuration is specified as a command line + * argument (-c ). + */ +#define ZTP_DEFAULT_CONFIG_PATH "/etc/ztpd/config.json" + +/** + * @brief The daemon configuration options. + */ +struct ztp_settings { + // Global service options + bool dpp_roles_exclusive; + bool dpp_roles_supported[DPP_DEVICE_ROLE_COUNT]; + bool dpp_roles_activated[DPP_DEVICE_ROLE_COUNT]; + struct json_object *dpp_roles_activated_json; + struct json_object *json; + + // UI activation options + bool ui_activation_command; + bool ui_activation_gpio; + char *ui_activation_gpio_chip; + char *ui_activation_gpio_line_name; + int32_t ui_activation_gpio_line; + int32_t ui_activation_gpio_delay; + char *ui_activation_unit; + + // Per-interface settings + struct list_head role_settings; + struct list_head change_handlers; + + char config_file[]; +}; + +enum ztp_settings_changed_item { + ZTP_SETTING_CHANGED_ITEM_DEVICE_ROLES, + ZTP_SETTING_CHANGED_ITEM_CONFIGURATOR_SETTINGS, +}; + +/** + * @brief Event payload for when ztp settings change. + * + * There is no current need to denote which setting or group of settings + * changed, so this structure does not currently need a body. It is defined + * here to keep the API stable for clients in case such details become + * pertinent in the future. +*/ +struct ztp_settings_changed_event { + enum ztp_settings_changed_item changed_item; +}; + +/** + * @brief Callback prototype for a settings-changed event. + */ +typedef void (*ztp_settings_changed_fn)(struct ztp_settings *, struct ztp_settings_changed_event *, void *); + +/** + * @brief Registers a settings changed handler. The specified callback function + * will be invoked with the context argument and a changed event structure each + * time the settings change. + * + * @param settings The ztp settings to monitor change events for. + * @param callback The callback function to invoke oin change. + * @param context An additional context-specific argument to be passed to the callback function. + * @return int 0 if the handler was successfully registered, non-zero otherwise. + */ +int +ztp_settings_register_change_handler(struct ztp_settings *settings, ztp_settings_changed_fn callback, void *context); + +/** + * @brief Unregisters a settings changed handler. + * + * @param settings The ztp settings to remove the handler from. + * @param callback The previously registered callback. + * @param context The context previously registered with the callback. + */ +void +ztp_settings_unregister_change_handler(struct ztp_settings *settings, ztp_settings_changed_fn callback, void *context); + +/** + * @brief Signals that the ztp settings have changed. + * + * @param settings The settings object that has changed. + * @param changed_item The setting item that changed. + */ +void +ztp_settings_signal_changed(struct ztp_settings *settings, enum ztp_settings_changed_item changed_item); + +/** + * @brief Parses a ztp settings file. + * + * @param config_file The full path of the configuration file to parse. + * @param psettings An output pointer to receive a parsed settings object. The + * caller is responsible for calling ztp_settings_destroy on this object. + * @return int + */ +int +ztp_settings_parse(const char *path, struct ztp_settings **psettings); + +/** + * @brief Uninitialize ztpd daemon options. + * + * @param psettings Pointer to the settings to destroy. + */ +void +ztp_settings_destroy(struct ztp_settings **psettings); + +/** + * @brief Finds role settings, given an interface name. + * + * @param settings The ztp settings structure to search. + * @param interface The name of the interface to lookup settings for. + * @return struct ztp_device_role_settings* + */ +struct ztp_device_role_settings * +ztp_settings_find_device_role_settings(const struct ztp_settings *settings, const char *interface); + +/** + * @brief Sets the device role setting to activated or deactivated. Note that + * this does not necessarily affect the functionality of any running daemon; + * this only updates the setting. + * + * If exclusive mode is set, and the new role diposition is to activate, then + * the existing activated role (if one exists) will be changed to disabled. + * + * @param settings The settings instance to update. + * @param role The role to set the disposition for. + * @param activate The role disposition. + * @return int 0 if the role disposition was successfully updated, -EOPNOTSUP + * if the role isn't supported. + */ +int +ztp_settings_set_device_role_disposition(struct ztp_settings *settings, enum dpp_device_role role, bool activate); + +#endif //__ZTP_SETTINGS_H__ diff --git a/src/core/ztp_wpa_supplicant.c b/src/core/ztp_wpa_supplicant.c new file mode 100644 index 0000000..934e161 --- /dev/null +++ b/src/core/ztp_wpa_supplicant.c @@ -0,0 +1,756 @@ + +#include +#include +#include + +#include +#include + +#include "dbus_common.h" +#include "string_utils.h" +#include "ztp.h" +#include "ztp_log.h" +#include "ztp_wpa_supplicant.h" + +/** + * @brief Helper function to track registered callbacks for the interface + * presence changed event. + */ +struct interface_presence_changed_callback { + struct list_head list; + interface_presence_changed_fn handler; + void *userdata; +}; + +/** + * @brief Invoke the registered callback handler for the interface presence + * changed event. + * + * @param callback The callback context for the event. + * @param name the name of the interface whose presence changed + * @param path the d-bus path of the interface whose presence changed. + */ +static void +invoke_interface_presence_changed_callback(struct interface_presence_changed_callback *callback, enum ztp_interface_presence presence, const char *name, const char *path) +{ + callback->handler(callback->userdata, presence, name, path); +} + +/** + * @brief Invoke all registered callback handlers for the interface added event. + * + * @param wpas the wpa supplicant instance. + * @param name the name of the interface whose presence changed + * @param path the d-bus path of the interface whose presence changed. + */ +static void +invoke_interface_presence_changed_callbacks(struct ztp_wpa_supplicant *wpas, enum ztp_interface_presence presence, const char *name, const char *path) +{ + struct interface_presence_changed_callback *callback; + list_for_each_entry (callback, &wpas->interface_presence_changed, list) { + invoke_interface_presence_changed_callback(callback, presence, name, path); + } +} + +/** + * @brief Cleans up resources owned by a wpa_supplicant_interface_entry. + * + * @param entry The entry to uninitialize. + */ +static void +wpa_supplicant_interface_entry_uninitialize(struct wpa_supplicant_interface_entry *entry) +{ + if (!entry) + return; + + list_del(&entry->list); + + if (entry->path) + free(entry->path); + if (entry->name) + free(entry->name); + free(entry); +} + +/** + * @brief Finds a wpa supplicant interface entry by d-bus path. + * + * @param wpas The wpa supplicant instance. + * @param path The d-bus path of the interface to find. + * @return struct wpa_supplicant_interface_entry* The interface entry, if + * present. Otherwise NULL. + */ +struct wpa_supplicant_interface_entry * +find_interface_entry_by_path(struct ztp_wpa_supplicant *wpas, const char *path) +{ + struct wpa_supplicant_interface_entry *entry; + + list_for_each_entry (entry, &wpas->interfaces, list) { + if (strcmp(entry->path, path) == 0) + break; + } + + return entry; +} + +/** + * @brief Finds a wpa supplicant interface entry by an interface name. + * + * @param wpas The wpa supplicant instance. + * @param name The name of the interface to find. + * @return struct wpa_supplicant_interface_entry* The interface entry, if + * present. Otherwise NULL. + */ +struct wpa_supplicant_interface_entry * +find_interface_entry_by_name(struct ztp_wpa_supplicant *wpas, const char *name) +{ + struct wpa_supplicant_interface_entry *entry; + + list_for_each_entry (entry, &wpas->interfaces, list) { + if (strcmp(entry->name, name) == 0) + break; + } + + return entry; +} + +/** + * @brief Adds a new interface to the list of interfaces being tracked. + * + * @param wpas The wpa_supplicant instance. + * @param path The d-bus object path of the interface. + * @param name The name of the interface. + * @return int The result of the operation. 0 if successful, -1 otherwise. + */ +struct wpa_supplicant_interface_entry * +wpa_supplicant_interface_entry_initialize(const char *path, const char *name) +{ + struct wpa_supplicant_interface_entry *entry = calloc(1, sizeof *entry); + if (!entry) { + zlog_error_if(name, "failed to allocate memory for interface entry"); + return NULL; + } + + INIT_LIST_HEAD(&entry->list); + + entry->path = strdup(path); + if (!entry->path) { + zlog_error_if(name, "failed to allocate memory for interface path string"); + free(entry); + return NULL; + } + + entry->name = strdup(name); + if (!entry->name) { + zlog_error_if(name, "failed to allocate memory for interface name string"); + goto fail; + } + + return entry; + +fail: + wpa_supplicant_interface_entry_uninitialize(entry); + return NULL; +} + +/** + * @brief Add a new interface for monitoring. + * + * @param wpas The wpa supplicant instance. + * @param path The d-bus path of the interface to add. + * @param name The name of the interface. + * @return int 0 if the interface was added for monitoring, non-zero otherwise. + */ +static int +wpas_interface_add(struct ztp_wpa_supplicant *wpas, const char *path, const char *name) +{ + zlog_info_if(name, "arrived on %s", path); + + // Create new interface entry for this path. + struct wpa_supplicant_interface_entry *entry = wpa_supplicant_interface_entry_initialize(path, name); + if (!entry) { + zlog_error_if(name, "failed to allocate memory for new interface entry"); + return -ENOMEM; + } + + // Add the new interface to the tracking list. + list_add(&entry->list, &wpas->interfaces); + invoke_interface_presence_changed_callbacks(wpas, ZTP_INTERFACE_ARRIVED, name, path); + + return 0; +} + +/** + * @brief Generic wpa_supplicant interface added handler. This will be + * invoked whenever wpa_supplicant begins monitoring a new interface. This + * callback will dispatch the relevant information to the registered callback + * in ztpd. + * + * @param msg The sd-bus message describing the InterfaceAdded signal. + * @param userdata The user context that will be passed to the ztpd caback. + * @param ret_error An sd-bus proxy for the return value. Unused. + * @return int The result of the opertation. 0 if successful, otherwise + * non-zero error value. + */ +static int +wpas_interface_added(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) +{ + __unused(ret_error); + + char *name = NULL; + char *path = NULL; + struct ztp_wpa_supplicant *wpas = (struct ztp_wpa_supplicant *)userdata; + + // Read the interface object path. + int ret = sd_bus_message_read(msg, "o", &path); + if (ret < 0) { + zlog_error("failed reading InterfaceAdded 'interface' property (%d)", ret); + return ret; + } + + // Read the interface name. + sd_bus_error error = SD_BUS_ERROR_NULL; + ret = sd_bus_get_property_string(wpas->bus, + WPAS_DBUS_SERVICE, + path, + WPAS_DBUS_INTERFACE, + "Ifname", + &error, + &name); + if (ret < 0) { + zlog_error("failed to retrieve 'Ifname' property (%d)", ret); + return ret; + } + + ret = wpas_interface_add(wpas, path, name); + if (ret < 0) { + zlog_error("[%s] failed to add new interface (%d)", name, ret); + return ret; + } + + return 0; +} + +/** + * @brief Remove an interface from the interface list. Following the call, the + * interface will no longer be monitored. + * + * @param wpas The wpa supplicant instance. + * @param path The d-bus path of the interface to remove. + */ +static void +wpas_interface_remove(struct ztp_wpa_supplicant *wpas, const char *path) +{ + struct wpa_supplicant_interface_entry *entry = find_interface_entry_by_path(wpas, path); + const char *interface = entry ? entry->name : ""; + zlog_info_if(interface, "departed from %s", path); + + invoke_interface_presence_changed_callbacks(wpas, ZTP_INTERFACE_DEPARTED, interface, path); + wpa_supplicant_interface_entry_uninitialize(entry); +} + +/** + * @brief Generic wpa_supplicant interface removed handler. This will be + * invoked whenever wpa_supplicant is no longer monitoring a particular + * interface. This callback will dispatch the relevant information to the + * registered callback in ztpd. + * + * @param msg The sd-bus message describing the InterfaceRemoved signal. + * @param userdata The user context that will be passed to the ztpd caback. + * @param ret_error An sd-bus proxy for the return value. Unused. + * @return int The result of the opertation. 0 if successful, otherwise + * non-zero error value. + */ +static int +wpas_interface_removed(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) +{ + __unused(ret_error); + + struct ztp_wpa_supplicant *wpas = (struct ztp_wpa_supplicant *)userdata; + + char *path = NULL; + int ret = sd_bus_message_read(msg, "o", &path); + if (ret < 0) { + zlog_error("failed reading InterfaceRemoved interface property (%d)", ret); + return ret; + } + + wpas_interface_remove(wpas, path); + return 0; +} + +/** + * @brief Enumerates and processes the current interfaces being controlled by + * wpa_supplicant. Each interface will be added for monitoring for zero touch + * provisioning. + * + * Note that in future, the decision to include an interface for ZTP will be + * controlled by a configuration file option. + * + * @param wpas The wpa_supplicant instance to use. + * @return int The result of the operation. 0 if successful, otherwise a + * non-zero error value. + */ +static int +process_current_interfaces(struct ztp_wpa_supplicant *wpas) +{ + sd_bus_message *msg; + sd_bus_error error = SD_BUS_ERROR_NULL; + int ret = sd_bus_get_property(wpas->bus, + WPAS_DBUS_SERVICE, + WPAS_DBUS_SERVICE_PATH, + WPAS_DBUS_SERVICE, + "Interfaces", + &error, + &msg, + "ao"); + if (ret < 0) { + zlog_error("failed to retrieve 'Interfaces' property value (%d)", ret); + return ret; + } + + ret = sd_bus_message_enter_container(msg, SD_BUS_TYPE_ARRAY, "o"); + if (ret < 0) { + zlog_error("unable to enter 'Interfaces' array' (%d)", ret); + goto out; + } + + { + char *path = NULL; + for (;;) { + ret = sd_bus_message_read(msg, "o", &path); + if (ret == 0) { + break; + } else if (ret < 0) { + zlog_error("failed reading interface element (%d)", ret); + goto out; + } + + char *name = NULL; + ret = sd_bus_get_property_string(wpas->bus, + WPAS_DBUS_SERVICE, + path, + WPAS_DBUS_INTERFACE, + "Ifname", + &error, + &name); + if (ret < 0) { + zlog_error("failed to retrieve 'Ifname' property (%d)", ret); + goto out; + } + + ret = wpas_interface_add(wpas, path, name); + if (ret < 0) { + zlog_error_if(name, "failed to add new interface entry (%d)", ret); + } + + free(name); + } + } + + sd_bus_message_exit_container(msg); + +out: + sd_bus_message_unref(msg); + return ret; +} + +/** + * @brief Resets the known interface list such that none are known. + * + * @param wpas The wpa_supplicant instance. + */ +static void +reset_interface_list(struct ztp_wpa_supplicant *wpas) +{ + // Invoke removed handler for all known interfaces, then delete them. + struct wpa_supplicant_interface_entry *interface; + struct wpa_supplicant_interface_entry *tmp; + list_for_each_entry_safe (interface, tmp, &wpas->interfaces, list) { + invoke_interface_presence_changed_callbacks(wpas, ZTP_INTERFACE_DEPARTED, interface->path, interface->name); + wpa_supplicant_interface_entry_uninitialize(interface); + } +} + +/** + * @brief Handler function for when the wpa_supplicant d-bus service arrives on + * the bus. + * + * @param wpas The wpa_supplicant instance. + */ +static void +on_service_arrived(struct ztp_wpa_supplicant *wpas) +{ + zlog_info(WPAS_DBUS_SERVICE_PATH " service arrived on d-bus"); + process_current_interfaces(wpas); +} + +/** + * @brief Handler function for when the wpa_supplicant d-bus service departs + * from the bus (eg. crash or manual stop). + * + * @param wpas The wpa_supplicant instance. + */ +static void +on_service_departed(struct ztp_wpa_supplicant *wpas) +{ + zlog_info(WPAS_DBUS_SERVICE_PATH " service departed d-bus"); + reset_interface_list(wpas); +} + +/** + * @brief Handler function to respond to the NameOwnerChanged signal for the + * wpa_supplicant service. This callback will be invoked each time the owner of + * the wpa_supplicant service name changes. This occurs when the daemon claims + * the name (when it starts up) and when it releases the name (when it stops + * running or crashes). + * + * See https://dbus.freedesktop.org/doc/dbus-specification.html#bus-messages-name-owner-changed + * for more details. + * + * @param msg The message containing the signal change data. + * @param userdata User context associated with the callback. In this case, + * this will always be the wpa_supplicant instance that was used to register + * this signal handler. + * @param error Pointer to pass back any errors to the signal generator. Unused. + * @return int The result of the operation. 0 if successful, non-zero otherwise. + */ +static int +wpas_service_availability_changed(sd_bus_message *msg, void *userdata, sd_bus_error *error) +{ + __unused(error); + + struct ztp_wpa_supplicant *wpas = (struct ztp_wpa_supplicant *)userdata; + + // Read service name argument. + const char *name; + int ret = sd_bus_message_read_basic(msg, SD_BUS_TYPE_STRING, &name); + if (ret < 0) { + zlog_error("failed to read service name for NameOwnerChanged (%d)", ret); + return ret; + } + + // Read old owner name. + const char *owner_old; + ret = sd_bus_message_read_basic(msg, SD_BUS_TYPE_STRING, &owner_old); + if (ret < 0) { + zlog_error("failed to read 'old_owner' property of NameOwnerChanged (%d)", ret); + return ret; + } + + // Read new owner name. + const char *owner_new; + ret = sd_bus_message_read_basic(msg, SD_BUS_TYPE_STRING, &owner_new); + if (ret < 0) { + zlog_error("failed to read 'new_owner' property of NameOwnerChanged (%d)", ret); + return ret; + } + + zlog_debug("%s NameOwnerChanged '%s' -> '%s'", name, owner_old, owner_new); + + // When there was no previous owner, the service has arrived on the bus. + if (strcmp(owner_old, "") == 0) { + on_service_arrived(wpas); + return 0; + } + + // When the new owner is empty, the service has departed from the bus. + if (strcmp(owner_new, "") == 0) { + on_service_departed(wpas); + return 0; + } + + zlog_warning("%s NameOwnerChanged state unexpected", name); + return 0; +} + +/** + * @brief Fixed match rule that triggers on NameOwnerChanged signals where the + * first argument (service name) is equal to that of the wpa_supplicant + * service. This allows us to detect when the wpa_supplicant service arrives + * and departs the d-bus system bus. + */ +#define WPA_SUPPLICANT_MATCH_NAME_OWNER_CHANGED \ + "type='signal'," \ + "interface='" DBUS_GLOBAL_SERVICE "'," \ + "member='" DBUS_GLOBAL_NAME_OWNER_CHANGED "'," \ + "arg0='" WPAS_DBUS_SERVICE "'" + +/** + * @brief Initializes the wpa_supplicant related facilities. + * + * @param wpas A pointer to the structure to initialize. + * @return int The result of the operation. 0 if successful, otherwise a non-zero error value. + */ +int +ztp_wpa_supplicant_initialize(struct ztp_wpa_supplicant *wpas) +{ + int ret; + + INIT_LIST_HEAD(&wpas->interfaces); + INIT_LIST_HEAD(&wpas->interface_presence_changed); + + // Establish bus connection. + ret = sd_bus_default_system(&wpas->bus); + if (ret < 0) { + zlog_error("failed to open system bus (%d)", -ret); + goto fail; + } + + // Register for signals we're interested in. + ret = sd_bus_match_signal(wpas->bus, &wpas->slot_interface_added, + WPAS_DBUS_SERVICE, // sender + WPAS_DBUS_SERVICE_PATH, // path + WPAS_DBUS_SERVICE, // interface + "InterfaceAdded", // member + wpas_interface_added, // callback + wpas); // userdata + if (ret < 0) { + zlog_error("failed to register handler for InterfaceAdded signal (%d)", ret); + goto fail; + } + + ret = sd_bus_match_signal(wpas->bus, &wpas->slot_interface_removed, + WPAS_DBUS_SERVICE, // sender + WPAS_DBUS_SERVICE_PATH, // path + WPAS_DBUS_SERVICE, // interface + "InterfaceRemoved", // member + wpas_interface_removed, // callback + wpas); // userdata + if (ret < 0) { + zlog_error("failed to register handler for InterfaceAdded signal (%d)", ret); + goto fail; + } + + ret = sd_bus_add_match(wpas->bus, &wpas->slot_service_availability, WPA_SUPPLICANT_MATCH_NAME_OWNER_CHANGED, wpas_service_availability_changed, wpas); + if (ret < 0) { + zlog_error("failed to register handler for wpa_supplicant service name ownership changes (%d)", ret); + goto fail; + } + + // Since we're init'ing, assume the service is up. + on_service_arrived(wpas); + +out: + return ret; +fail: + ztp_wpa_supplicant_uninitialize(wpas); + goto out; +} + +/** + * @brief Uninitializes a ztpd_wpa_supplicant instance. + * + * @param wpas The instance to uninitialize. + */ +void +ztp_wpa_supplicant_uninitialize(struct ztp_wpa_supplicant *wpas) +{ + if (wpas->interfaces.next != NULL) + reset_interface_list(wpas); + + if (wpas->slot_interface_added != NULL) { + sd_bus_slot_unref(wpas->slot_interface_added); + wpas->slot_interface_added = NULL; + } + + if (wpas->slot_interface_removed != NULL) { + sd_bus_slot_unref(wpas->slot_interface_removed); + wpas->slot_interface_removed = NULL; + } + + if (wpas->slot_service_availability != NULL) { + sd_bus_slot_unref(wpas->slot_service_availability); + wpas->slot_service_availability = NULL; + } + + if (wpas->bus) { + sd_bus_unref(wpas->bus); + wpas->bus = NULL; + } +} + +/** + * @brief Retrieves the 'State' property of the + * 'fi.w1.wpa_supplicant1.Interface' d-bus interface of the object referred to + * by 'path'. + * + * @param wpas The wpa_supplicant instance. + * @param path The path of the object which implements the interface. + * @param state Output argument that will receive the interface state. + * @return int 0 if the operation succeeded, non-zero otherwise. + */ +int +ztp_wpa_supplicant_get_interface_state(struct ztp_wpa_supplicant *wpas, const char *path, enum wpas_interface_state *state) +{ + char *statestr; + sd_bus_error error = SD_BUS_ERROR_NULL; + int ret = sd_bus_get_property_string(wpas->bus, + WPAS_DBUS_SERVICE, + path, + WPAS_DBUS_INTERFACE, + "State", + &error, + &statestr); + if (ret < 0) { + zlog_error("failed to retrieve interface 'State' property (%d)", ret); + return ret; + } + + *state = parse_wpas_interface_state(statestr); + free(statestr); + + return 0; +} + +/** + * @brief Returns the number of networks configured for the specified interface. + * + * @param wpas The wpa_supplicant instance. + * @param path The path of the object which implements the wpa_supplicant 'Interface' interface. + * @param count Output argument to hold the number of networks. + * @return int 0 if the operation was successful. *count will contain the + * number of networks. Otherwise a non-zero error value is returned and *count + * is undefined. + */ +int +ztp_get_interface_network_count(struct ztp_wpa_supplicant *wpas, const char *path, uint32_t *count) +{ + *count = 0; + + sd_bus_message *msg; + sd_bus_error error = SD_BUS_ERROR_NULL; + int ret = sd_bus_get_property(wpas->bus, + WPAS_DBUS_SERVICE, + path, + WPAS_DBUS_INTERFACE, + "Networks", + &error, + &msg, + "ao"); + if (ret < 0) { + zlog_error("failed to retrieve 'Networks' property value (%d)", ret); + return ret; + } + + ret = sd_bus_message_enter_container(msg, SD_BUS_TYPE_ARRAY, "o"); + if (ret < 0) { + zlog_error("unable to enter 'Networks' array' (%d)", ret); + goto out; + } + + { + // Just count the number of entries (no need to read them). + while ((ret = sd_bus_message_skip(msg, "o")) > 0) + (*count)++; + } + + sd_bus_message_exit_container(msg); + +out: + sd_bus_message_unref(msg); + return ret; +} + +/** + * @brief Registers a handler for the interface added event. + * + * @param wpas The wpa supplicant instance. + * @param handler The handler function to invoke when interface presence changes. + * @param userdata The context to be passed to the handler function. + * @return int 0 if successful, non-zero otherwise. + */ +int +ztp_wpa_supplicant_register_interface_presence_changed_callback(struct ztp_wpa_supplicant *wpas, interface_presence_changed_fn handler, void *userdata) +{ + struct interface_presence_changed_callback *callback = malloc(sizeof *callback); + if (!callback) { + zlog_error("failed to allocate memory for interface presence changed callback"); + return -ENOMEM; + } + + INIT_LIST_HEAD(&callback->list); + callback->handler = handler; + callback->userdata = userdata; + list_add(&callback->list, &wpas->interface_presence_changed); + + // Manually invoke callback with all known present interfaces. + struct wpa_supplicant_interface_entry *interface; + list_for_each_entry (interface, &wpas->interfaces, list) { + invoke_interface_presence_changed_callback(callback, ZTP_INTERFACE_ARRIVED, interface->name, interface->path); + } + + return 0; +} + +/** + * @brief Unregisters an interface presence changed handler. + * + * @param wpas The wpa supplicant instance. + * @param handler The handler function that was previously registered. + * @param userdata The context previously associated with the handler function. + */ +void +ztp_wpa_supplicant_unregister_interface_presence_changed_callback(struct ztp_wpa_supplicant *wpas, interface_presence_changed_fn handler, void *userdata) +{ + struct interface_presence_changed_callback *callback; + + list_for_each_entry (callback, &wpas->interface_presence_changed, list) { + if (callback->handler == handler && callback->userdata == userdata) { + list_del(&callback->list); + free(callback); + break; + } + } +} + +/** + * @brief Looks up a d-bus interface path, given its name. + * + * @param wpas The wpa supplicant instance. + * @param interface The name of the interface to lookup the d-bus path. + * @param path An output pointer to hold the d-bus path, if it exists. In this + * case, the caller owns the memory associated with the path which can be + * released by passing it to free(). + * @return int 0 if the d-bus path was retrieved and written to *path. -ENOENT + * is returned if wpa asupplicant is not aware of the specified interface. + * Otherwise a non-zero value is returned. + */ +int +ztp_wpa_supplicant_get_interface_path(struct ztp_wpa_supplicant *wpas, const char *interface, char **path) +{ + sd_bus_message *reply; + sd_bus_error error = SD_BUS_ERROR_NULL; + + int ret = sd_bus_call_method(wpas->bus, + WPAS_DBUS_SERVICE, + WPAS_DBUS_SERVICE_PATH, + WPAS_DBUS_SERVICE, + "GetInterface", + &error, + &reply, + "s", + interface); + if (ret == -ENOENT) { + zlog_debug_if(interface, "not monitored by wpa_supplicant, no d-bus path available"); + return ret; + } else if (ret < 0) { + zlog_error("failed to invoke 'GetInterface' wpa_supplicant method (%d)", ret); + return ret; + } + + const char *object_path = NULL; + ret = sd_bus_message_read_basic(reply, SD_BUS_TYPE_OBJECT_PATH, &object_path); + if (ret < 0) { + zlog_error("failed to read 'GetInterface' reply string (%d)", ret); + return ret; + } + + *path = strdup(object_path); + if (!*path) { + zlog_error("failed to allocate memory for d-bus interface path"); + return -ENOMEM; + } + + return 0; +} diff --git a/src/core/ztp_wpa_supplicant.h b/src/core/ztp_wpa_supplicant.h new file mode 100644 index 0000000..3c44dc1 --- /dev/null +++ b/src/core/ztp_wpa_supplicant.h @@ -0,0 +1,133 @@ + +#ifndef __ZTP_WPA_SUPPLICANT_H__ +#define __ZTP_WPA_SUPPLICANT_H__ + +#include + +#include +#include + +#include "dpp.h" +#include "wpa_supplicant.h" +#include "ztp.h" + +struct wpa_ctrl; + +/** + * @brief Helper structure to track known intefaces. + */ +struct wpa_supplicant_interface_entry { + struct list_head list; + char *path; + char *name; +}; + +/** + * @brief Structure collecting wpa_supplicant related details. This + * primarily refers to the global/system wpa_supplicant interface and + * not specific wireless interfaces. + */ +struct ztp_wpa_supplicant { + sd_bus *bus; + sd_bus_slot *slot_service_availability; + sd_bus_slot *slot_interface_added; + sd_bus_slot *slot_interface_removed; + struct list_head interfaces; + struct list_head interface_presence_changed; +}; + +/** + * @brief Initializes the wpa_supplicant related facilities. + * + * @param wpas A pointer to the structure to initialize. + * @return int The result of the operation. 0 if successful, otherwise a non-zero error value. + */ +int +ztp_wpa_supplicant_initialize(struct ztp_wpa_supplicant *wpas); + +/** + * @brief Uninitializes a ztp_wpa_supplicant instance. + * + * @param wpas The instance to uninitialize. + */ +void +ztp_wpa_supplicant_uninitialize(struct ztp_wpa_supplicant *wpas); + +/** + * @brief Retrieves the 'State' property of the + * 'fi.w1.wpa_supplicant1.Interface' d-bus interface of the object referred to + * by 'path'. + * + * @param wpas The wpa_supplicant instance. + * @param path The path of the object which implements the interface. + * @param state Output argument that will hold receive the interface state. + * @return int 0 if the operation succeeded, non-zero otherwise. + */ +int +ztp_wpa_supplicant_get_interface_state(struct ztp_wpa_supplicant *wpas, const char *path, enum wpas_interface_state *state); + +/** + * @brief Returns the number of networks configured for this interface. + * + * @param wpas The wpa_supplicant instance. + * @param path The d-bus object path to retrieve state from. + * @param count Output variable to receive the number of networks. + * @return int The status of the operation. 0 if successful, non-zero otherise. + */ +int +ztp_get_interface_network_count(struct ztp_wpa_supplicant *wpas, const char *path, uint32_t *count); + +/** + * @brief Describes the presence action of an interface. + */ +enum ztp_interface_presence { + ZTP_INTERFACE_ARRIVED, + ZTP_INTERFACE_DEPARTED, +}; + +/** + * @brief Prototype for the interface added event callback. + * + * @param userdata Contextual data that was registered with the handler. + * @param name The interface name. + * @param path The d-bus object path of the interface that was added. + */ +typedef void (*interface_presence_changed_fn)(void *userdata, enum ztp_interface_presence presence, const char *name, const char *path); + +/** + * @brief Registers a handler for the interface added event. + * + * @param wpas The wpa supplicant instance. + * @param handler The handler function to invoke when an interface is added. + * @param userdata The context to be passed to the handler function. + * @return int 0 if successful, non-zero otherwise. + */ +int +ztp_wpa_supplicant_register_interface_presence_changed_callback(struct ztp_wpa_supplicant *wpas, interface_presence_changed_fn handler, void *userdata); + +/** + * @brief Unregisters an interface presence changed handler. + * + * @param wpas The wpa supplicant instance. + * @param handler The handler function that was previously registered. + * @param userdata The context previously associated with the handler function. + */ +void +ztp_wpa_supplicant_unregister_interface_presence_changed_callback(struct ztp_wpa_supplicant *wpas, interface_presence_changed_fn handler, void *userdata); + +/** + * @brief Looks up a d-bus interface path, given its name. + * + * @param wpas The wpa supplicant instance. + * @param interface The name of the interface to lookup the d-bus path. + * @param path An output pointer to hold the d-bus path, if it exists. In this + * case, the caller owns the memory associated with the path which can be + * released by passing it to free(). + * @return int 0 if the d-bus path was retrieved and written to *path. -ENOENT + * is returned if wpa asupplicant is not aware of the specified interface. + * Otherwise a non-zero value is returned. + */ +int +ztp_wpa_supplicant_get_interface_path(struct ztp_wpa_supplicant *wpas, const char *interface, char **path); + +#endif //__ZTP_WPA_SUPPLICANT_H__ diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt new file mode 100644 index 0000000..9af26ab --- /dev/null +++ b/src/dbus/CMakeLists.txt @@ -0,0 +1,32 @@ + +project(ztpdbus) + +add_library(ztpdbus + STATIC + "" +) + +target_sources(ztpdbus + PRIVATE + dbus_message_helpers.c + ztp_dbus_configurator.c + ztp_dbus_network_configuration.c + ztp_dbus_client.c + ztp_dbus_server.c +) + +target_include_directories(ztpdbus + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../core + ${CMAKE_CURRENT_SOURCE_DIR}/../core/biproviders + ${CMAKE_CURRENT_SOURCE_DIR}/../core/biproviders/file + ${CMAKE_CURRENT_SOURCE_DIR}/../core/biproviders/azuredps + ${CMAKE_CURRENT_SOURCE_DIR}/../utils + ${CMAKE_CURRENT_SOURCE_DIR}/../wpas + ${CMAKE_CURRENT_SOURCE_DIR}/../wifi +) + +target_link_libraries(ztpdbus + ztpcore +) diff --git a/src/dbus/dbus_common.h b/src/dbus/dbus_common.h new file mode 100644 index 0000000..7ab1a29 --- /dev/null +++ b/src/dbus/dbus_common.h @@ -0,0 +1,32 @@ + +#ifndef __DBUS_COMMON_H__ +#define __DBUS_COMMON_H__ + +#define DBUS_GLOBAL_SERVICE "org.freedesktop.DBus" +#define DBUS_GLOBAL_NAME_OWNER_CHANGED "NameOwnerChanged" + +#define DBUS_PROPERTIES_INTERFACE "org.freedesktop.DBus.Properties" +#define DBUS_PROPERTIES_METHOD_GETALL "GetAll" + +/** + * @brief Helper structure to manage metadata for reading string properties + * from a d-bus dictionary. + */ +struct dbus_str_prop_desc { + const char *name; + char **dst; +}; + +/** + * @brief Helper structure to manage metadata for reading array properties from + * a d-bus dictionary + */ +struct dbus_array_prop_desc { + const char *name; + char type; + size_t *count; + size_t elem_size; + void **dst; +}; + +#endif //__DBUS_COMMON_H__ diff --git a/src/dbus/dbus_message_helpers.c b/src/dbus/dbus_message_helpers.c new file mode 100644 index 0000000..1e92bc3 --- /dev/null +++ b/src/dbus/dbus_message_helpers.c @@ -0,0 +1,665 @@ + +#include +#include + +#include "dbus_message_helpers.h" +#include "ztp_log.h" + +/** + * @brief + * + * @param message + * @param contents + * @param value + * @param read_value + * @param value_context + * @return int + */ +int +dbus_read_variant(sd_bus_message *message, const char *contents, void *value, dbus_read_obj_fn read_value, void *value_context) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_VARIANT, contents); + if (ret < 0) { + zlog_error("failed to enter variant container (%d)", ret); + return ret; + } + + { + ret = read_value(message, value, value_context); + if (ret < 0) { + zlog_error("failed to read variant value (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error("failed to exit variant container (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param message + * @param contents + * @param value + * @param append_value + * @param value_context + * @return int + */ +int +dbus_append_variant(sd_bus_message *message, const char *contents, const void *value, dbus_append_obj_fn append_value, void *value_context) +{ + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_VARIANT, contents); + if (ret < 0) { + zlog_error("failed to open variant container (%d)", ret); + return ret; + } + + { + ret = append_value(message, value, value_context); + if (ret < 0) { + zlog_error("failed to append variant value (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error("failed to close variant container (%d)", ret); + return ret; + } + + return 0; +} + +struct dbus_append_value_basic_context { + char value_type; +}; + +/** + * @brief + * + * @param message + * @param value + * @param contextp + * @return int + */ +static int +dbus_append_value_basic(sd_bus_message *message, const void *value, void *contextp) +{ + struct dbus_append_value_basic_context *context = (struct dbus_append_value_basic_context *)contextp; + + int ret = sd_bus_message_append_basic(message, context->value_type, value); + if (ret < 0) { + zlog_error("failed to append value of type '%c' to message (%d)", context->value_type, ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param message + * @param value_type + * @param value + * @return int + */ +int +dbus_append_variant_basic(sd_bus_message *message, char value_type, const void *value) +{ + struct dbus_append_value_basic_context context = { + .value_type = value_type, + }; + + char contents[16]; + int ret = snprintf(contents, sizeof contents, "%c", value_type); + if (ret < 0) { + ret = -errno; + zlog_error("failed to format variant value type '%c' (%d)", value_type, ret); + return ret; + } else if ((size_t)ret > sizeof contents) { + zlog_error("invalid variant value type '%c' specified", value_type); + return -EINVAL; + } + + return dbus_append_variant(message, contents, value, dbus_append_value_basic, &context); +} + +struct dbus_append_value_variant_context { + const char *contents; + dbus_append_obj_fn append_value; + void *append_value_context; +}; + +static int +dbus_append_value_variant(sd_bus_message *message, const void *value, void *contextp) +{ + struct dbus_append_value_variant_context *context = (struct dbus_append_value_variant_context *)contextp; + + int ret = dbus_append_variant(message, context->contents, value, context->append_value, context->append_value_context); + if (ret < 0) { + zlog_error("failed to append variant value (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param message + * @param key + * @param append_value + * @param value_context + * @return int + */ +int +dbus_append_dict_entry(sd_bus_message *message, const char *key, const void *value, const char *contents, dbus_append_obj_fn append_value, void *value_context) +{ + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_DICT_ENTRY, "sv"); + if (ret < 0) { + zlog_error("failed to open dictionary entry container with key %s (%d)", key, ret); + return ret; + } + + { + ret = sd_bus_message_append_basic(message, SD_BUS_TYPE_STRING, key); + if (ret < 0) { + zlog_error("failed to append dictionary entry with key %s (%d)", key, ret); + return ret; + } + + ret = dbus_append_variant(message, contents, value, append_value, value_context); + if (ret < 0) { + zlog_error("failed to append variant value to dictionary entry with key %s (%d)", key, ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error("failed to close dictionary entry container with key %s (%d)", key, ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_append_dict_entry_basic(sd_bus_message *message, char value_type, const char *key, const void *value) +{ + struct dbus_append_value_basic_context context = { + .value_type = value_type, + }; + + char contents[16]; + int ret = snprintf(contents, sizeof contents, "%c", value_type); + if (ret < 0) { + ret = -errno; + zlog_error("failed to format dictionary entry value type '%c' (%d)", value_type, ret); + return ret; + } else if ((size_t)ret > sizeof contents) { + zlog_error("invalid dictionary entry value type '%c' specified", value_type); + return -EINVAL; + } + + return dbus_append_dict_entry(message, key, value, contents, dbus_append_value_basic, &context); +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_append_dict_entry_string(sd_bus_message *message, const char *key, const char *value) +{ + return dbus_append_dict_entry_basic(message, SD_BUS_TYPE_STRING, key, value); +} + +struct dbus_read_value_basic_context { + char value_type; +}; + +/** + * @brief + * + * @param message + * @param value + * @param contextp + * @return int + */ +static int +dbus_read_value_basic(sd_bus_message *message, void *value, void *contextp) +{ + struct dbus_read_value_basic_context *context = (struct dbus_read_value_basic_context *)contextp; + + int ret = sd_bus_message_read_basic(message, context->value_type, value); + if (ret < 0) { + zlog_error("failed to read value of type '%c' from message (%d)", context->value_type, ret); + return ret; + } + + return 0; +} + +struct dbus_read_value_variant_context { + const char *contents; + dbus_read_obj_fn read_value; + void *read_value_context; +}; + +/** + * @brief + * + * @param message + * @param value + * @param contextp + * @return int + */ +static int +dbus_read_value_variant(sd_bus_message *message, void *value, void *contextp) +{ + struct dbus_read_value_variant_context *context = (struct dbus_read_value_variant_context *)contextp; + + int ret = dbus_read_variant(message, context->contents, value, context->read_value, context->read_value_context); + if (ret < 0) { + zlog_error("failed to read variant value (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @param contents + * @param read_value + * @param value_context + * @return int + */ +int +dbus_read_dict_entry(sd_bus_message *message, const char *key, void *value, const char *contents, dbus_read_obj_fn read_value, void *value_context) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "sv"); + if (ret < 0) { + zlog_error("failed to enter dictionary entry container with key %s (%d)", key, ret); + return ret; + } + + { + const char *key_read; + ret = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &key_read); + if (ret < 0) { + zlog_error("failed to read dictionary entry key %s (%d)", key, ret); + return ret; + } else if (!key_read || strcmp(key_read, key) != 0) { + zlog_error("dictionary entry key mismatch, got %s, expected %s", key_read, key); + return -EINVAL; + } + + ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_VARIANT, contents); + if (ret < 0) { + zlog_error("failed to enter variant container for dictionary entry with key %s (%d)", key, ret); + return ret; + } + + { + ret = read_value(message, value, value_context); + if (ret < 0) { + zlog_error("failed to read value from dictionary entry with key %s (%d)", key, ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error("failed to exit variant container for dictionary entry with key %s (%d)", key, ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error("failed to exit dictionary entry container with key %s (%d)", key, ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param message + * @param value_type + * @param key + * @param value + * @return int + */ +int +dbus_read_dict_entry_basic(sd_bus_message *message, char value_type, const char *key, void *value) +{ + struct dbus_read_value_basic_context context = { + .value_type = value_type, + }; + + char contents[16]; + int ret = snprintf(contents, sizeof contents, "%c", value_type); + if (ret < 0) { + ret = -errno; + zlog_error("failed to format dictionary entry value type '%c' (%d)", value_type, ret); + return ret; + } else if ((size_t)ret > sizeof contents) { + zlog_error("invalid dictionary entry value type '%c' specified", value_type); + return -EINVAL; + } + + return dbus_read_dict_entry(message, key, value, contents, dbus_read_value_basic, &context); +} + +/** + * @brief + * + * @param message + * @param value_type + * @param key + * @param value + * @return int + */ +int +dbus_read_dict_entry_string(sd_bus_message *message, const char *key, const char **value) +{ + return dbus_read_dict_entry_basic(message, SD_BUS_TYPE_STRING, key, value); +} + +/** + * @brief + * + * @param message + * @param key + * @param contents + * @param append_value + * @param value_context + * @return int + */ +int +dbus_append_kv_pair(sd_bus_message *message, const char *key, const void *value, const char *contents, dbus_append_obj_fn append_value, void *value_context) +{ + char value_contents[64]; + int ret = snprintf(value_contents, sizeof value_contents, "s%s", contents); + if (ret < 0) { + ret = -errno; + zlog_error("failed to format key-value contents (%d)", ret); + return ret; + } else if ((size_t)ret > sizeof value_contents) { + zlog_error("key-value contents too big"); + return -ERANGE; + } + + ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, value_contents); + if (ret < 0) { + zlog_error("failed to open struct container with key %s (%d)", key, ret); + return ret; + } + + { + ret = sd_bus_message_append_basic(message, SD_BUS_TYPE_STRING, key); + if (ret < 0) { + zlog_error("failed to append dictionary entry with key %s (%d)", key, ret); + return ret; + } + + ret = append_value(message, value, value_context); + if (ret < 0) { + zlog_error("failed to append value to struct container with key %s (%d)", key, ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error("failed to close struct container with key %s (%d)", key, ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_append_kv_pair_basic(sd_bus_message *message, char value_type, const char *key, const void *value) +{ + struct dbus_append_value_basic_context context = { + .value_type = value_type, + }; + + char contents[16]; + int ret = snprintf(contents, sizeof contents, "%c", value_type); + if (ret < 0) { + ret = -errno; + zlog_error("failed to format struct entry value type '%c' (%d)", value_type, ret); + return ret; + } else if ((size_t)ret > sizeof contents) { + zlog_error("invalid struct entry value type '%c' specified", value_type); + return -EINVAL; + } + + return dbus_append_kv_pair(message, key, value, contents, dbus_append_value_basic, &context); +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_append_kv_pair_string(sd_bus_message *message, const char *key, const char *value) +{ + return dbus_append_kv_pair_basic(message, SD_BUS_TYPE_STRING, key, value); +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @param contents + * @param append_value + * @param value_context + * @return int + */ +int +dbus_append_kv_pair_variant(sd_bus_message *message, const char *key, const void *value, const char *contents, dbus_append_obj_fn append_value, void *value_context) +{ + struct dbus_append_value_variant_context context = { + .contents = contents, + .append_value = append_value, + .append_value_context = value_context, + }; + + return dbus_append_kv_pair(message, key, value, "v", dbus_append_value_variant, &context); +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @param contents + * @param read_value + * @param value_context + * @return int + */ +int +dbus_read_kv_pair(sd_bus_message *message, const char *key, void *value, const char *contents, dbus_read_obj_fn read_value, void *value_context) +{ + char value_contents[64]; + int ret = snprintf(value_contents, sizeof value_contents, "s%s", contents); + if (ret < 0) { + ret = -errno; + zlog_error("failed to format key-value contents (%d)", ret); + return ret; + } else if ((size_t)ret > sizeof value_contents) { + zlog_error("key-value contents too big"); + return -ERANGE; + } + + ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, value_contents); + if (ret < 0) { + zlog_error("failed to enter struct container with key %s (%d)", key, ret); + return ret; + } + + { + const char *key_read; + ret = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &key_read); + if (ret < 0) { + zlog_error("failed to read dictionary entry with key %s (%d)", key, ret); + return ret; + } else if (!key_read || strcmp(key_read, key) != 0) { + zlog_error("struct key mismatch, got %s, expected %s", key_read, key); + return -EINVAL; + } + + ret = read_value(message, value, value_context); + if (ret < 0) { + zlog_error("failed to read value from struct with key %s (%d)", key, ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error("failed to exit struct with key %s (%d)", key, ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_read_kv_pair_basic(sd_bus_message *message, char value_type, const char *key, void *value) +{ + struct dbus_read_value_basic_context context = { + .value_type = value_type, + }; + + char contents[16]; + int ret = snprintf(contents, sizeof contents, "%c", value_type); + if (ret < 0) { + ret = -errno; + zlog_error("failed to format struct entry value type '%c' (%d)", value_type, ret); + return ret; + } else if ((size_t)ret > sizeof contents) { + zlog_error("invalid struct entry value type '%c' specified", value_type); + return -EINVAL; + } + + return dbus_read_kv_pair(message, key, value, contents, dbus_read_value_basic, &context); +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_read_kv_pair_string(sd_bus_message *message, const char *key, const char **value) +{ + return dbus_read_kv_pair_basic(message, SD_BUS_TYPE_STRING, key, value); +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_read_kv_pair_string_cp(sd_bus_message *message, const char *key, char **value) +{ + const char *valuetmp = NULL; + + int ret = dbus_read_kv_pair_string(message, key, &valuetmp); + if (ret == 0) { + *value = strdup(valuetmp); + if (!*value) { + zlog_error("failed to allocate memory for kv pair with key %s string copy", key); + return -ENOMEM; + } + } + + return ret; +} + +/** + * @brief + * + * @param message + * @param key + * @param value + * @param contents + * @param append_value + * @param value_context + * @return int + */ +int +dbus_read_kv_pair_variant(sd_bus_message *message, const char *key, void *value, const char *contents, dbus_read_obj_fn read_value, void *value_context) +{ + struct dbus_read_value_variant_context context = { + .contents = contents, + .read_value = read_value, + .read_value_context = value_context, + }; + + return dbus_read_kv_pair(message, key, value, "v", dbus_read_value_variant, &context); +} diff --git a/src/dbus/dbus_message_helpers.h b/src/dbus/dbus_message_helpers.h new file mode 100644 index 0000000..4b2d1f1 --- /dev/null +++ b/src/dbus/dbus_message_helpers.h @@ -0,0 +1,238 @@ + +#ifndef __DBUS_MESSAGE_HELPERS_H__ +#define __DBUS_MESSAGE_HELPERS_H__ + +#include + +#include + +/** + * @brief + */ +typedef int (*dbus_append_obj_fn)(sd_bus_message *message, const void *value, void *value_context); + +/** + * @brief + * + * @param message + * @param key + * @param append_value + * @param value_context + * @return int + */ +int +dbus_append_dict_entry(sd_bus_message *message, const char *key, const void *value, const char *contents, dbus_append_obj_fn append_value, void *value_context); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_append_dict_entry_basic(sd_bus_message *message, char value_type, const char *key, const void *value); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_append_dict_entry_string(sd_bus_message *message, const char *key, const char *value); + +/** + * @brief + * + * @param message + * @param contents + * @param value + * @param append_value + * @param value_context + * @return int + */ +int +dbus_append_variant(sd_bus_message *message, const char *contents, const void *value, dbus_append_obj_fn append_value, void *value_context); + +/** + * @brief + * + * @param message + * @param value_type + * @param value + * @return int + */ +int +dbus_append_variant_basic(sd_bus_message *message, char value_type, const void *value); + +/** + * @brief + * + * @param message + * @param key + * @param contents + * @param append_value + * @param value_context + * @return int + */ +int +dbus_append_kv_pair(sd_bus_message *message, const char *key, const void *value, const char *contents, dbus_append_obj_fn append_value, void *value_context); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_append_kv_pair_basic(sd_bus_message *message, char value_type, const char *key, const void *value); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_append_kv_pair_string(sd_bus_message *message, const char *key, const char *value); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @param contents + * @param append_value + * @param value_context + * @return int + */ +int +dbus_append_kv_pair_variant(sd_bus_message *message, const char *key, const void *value, const char *contents, dbus_append_obj_fn append_value, void *value_context); + +/** + * @brief + */ +typedef int (*dbus_read_obj_fn)(sd_bus_message *message, void *value, void *value_context); + +/** + * @brief + * + * @param message + * @param contents + * @param value + * @param read_value + * @param value_context + * @return int + */ +int +dbus_read_variant(sd_bus_message *message, const char *contents, void *value, dbus_read_obj_fn read_value, void *value_context); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @param contents + * @param read_value + * @param value_context + * @return int + */ +int +dbus_read_dict_entry(sd_bus_message *message, const char *key, void *value, const char *contents, dbus_read_obj_fn read_value, void *value_context); + +/** + * @brief + * + * @param message + * @param value_type + * @param key + * @param value + * @return int + */ +int +dbus_read_dict_entry_basic(sd_bus_message *message, char value_type, const char *key, void *value); + +/** + * @brief + * + * @param message + * @param value_type + * @param key + * @param value + * @return int + */ +int +dbus_read_dict_entry_string(sd_bus_message *message, const char *key, const char **value); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @param contents + * @param read_value + * @param value_context + * @return int + */ +int +dbus_read_kv_pair(sd_bus_message *message, const char *key, void *value, const char *contents, dbus_read_obj_fn read_value, void *value_context); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_read_kv_pair_basic(sd_bus_message *message, char value_type, const char *key, void *value); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_read_kv_pair_string(sd_bus_message *message, const char *key, const char **value); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @return int + */ +int +dbus_read_kv_pair_string_cp(sd_bus_message *message, const char *key, char **value); + +/** + * @brief + * + * @param message + * @param key + * @param value + * @param contents + * @param append_value + * @param value_context + * @return int + */ +int +dbus_read_kv_pair_variant(sd_bus_message *message, const char *key, void *value, const char *contents, dbus_read_obj_fn read_value, void *value_context); + +#endif //__DBUS_MESSAGE_HELPERS_H__ diff --git a/src/dbus/ztp_dbus_client.c b/src/dbus/ztp_dbus_client.c new file mode 100644 index 0000000..f55cf64 --- /dev/null +++ b/src/dbus/ztp_dbus_client.c @@ -0,0 +1,266 @@ + +#include +#include +#include + +#include + +#include "event_loop.h" +#include "ztp_dbus_client.h" +#include "ztp_log.h" + +/** + * @brief Private structure tracking context associated with a + * PropertiesChanged handler. + */ +struct ztp_dbus_properties_changed_handle { + struct list_head list; + void *userdata; + ztp_dbus_properties_changed_handler handler; + sd_bus_slot *slot; +}; + +/** + * @brief Generic properties changed handler that does some basic unwrapping of + * the sd-bus message and passes on the changed property dictionary to + * registered callbacks. + * + * @param msg The message containing the PropertiesChanged signal payload. + * @param userdata The context containing the registered callback handler. + * @param ret_error A proxy to return an error value. Unused (as we are the receiver). + * @return int 0 if the properties were handled successfully, non-zero otherwise. + */ +static int +ztpd_dbus_on_properties_changed(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) +{ + __unused(ret_error); + + struct ztp_dbus_properties_changed_handle *context = (struct ztp_dbus_properties_changed_handle *)userdata; + + // All PropertiesChanged signals include the name of the interface as the first argument. Read it. + char *interface; + int ret = sd_bus_message_read_basic(msg, SD_BUS_TYPE_STRING, &interface); + if (ret < 0) { + zlog_error("failed to read 'interface' property of 'PropertiesChanged' signal (%d)", ret); + goto fail; + } + + // Unwrap the dictionary entries which are a d-bus "array". + ret = sd_bus_message_enter_container(msg, SD_BUS_TYPE_ARRAY, "{sv}"); + if (ret < 0) { + zlog_error("failed to enter properties array container (%d)", ret); + goto fail; + } + + // Invoke user-supplied handler, passing supplied data pointer and message. + context->handler(msg, interface, context->userdata); + + ret = 0; +out: + return ret; +fail: + goto out; +} + +/** + * @brief Registers a property changed handler function for a d-bus interface. + * This refers to the standard-defined org.freedesktop.DBus.Properties.PropertiesChanged + * signal. + * + * @param dbus The dbus control object. + * @param interface The dbus interface to monitor for property changes. + * @param userdata User data that will be passed to the property change handler. + * @param handler The function that will be invoked upon property changes. + * @param handle A handle referencing the installed property handler. + * @return int The result of the operation. 0 if successful, in which case + * '*handle' will be populated with a value that refers to the monitor. A + * negative error value is returned otherwise. + */ +int +ztp_dbus_register_properties_changed_handler( + struct ztp_dbus_client *dbus, + const char *interface, + void *userdata, + ztp_dbus_properties_changed_handler handler, + struct ztp_dbus_properties_changed_handle **phandle) +{ + // Disallow unescaped single quote characters (') to prevent injection of + // key-value pairs into the signal matching rule. + if (strchr(interface, '\'') != NULL) + return -EINVAL; + + struct ztp_dbus_properties_changed_handle *handle = calloc(1, sizeof *handle); + if (!handle) + return -ENOMEM; + + handle->userdata = userdata; + handle->handler = handler; + + // Build a d-bus match expression that will link to a PropertiesChanged + // event on a specific interface. This is done using an 'Arg' match, which + // matches a positional argument that is passed to the signal. For the + // PropertiesChanged event, the first argument is the interface for which + // the signal is being raised, and so 'arg0' is used with the supplied + // interface to limit the match to that interface. See 'Match Rules' at + // https://dbus.freedesktop.org/doc/dbus-specification.html#type-system + // for more information. + char match[256]; + snprintf(match, sizeof match, + "type='signal'," + "interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged'," + "arg0='%s'", interface); + + // Register the targeted match rule for this signal. + int ret = sd_bus_add_match(dbus->bus, &handle->slot, match, ztpd_dbus_on_properties_changed, handle); + if (ret < 0) { + zlog_error("failed to add match for PropertiesChanged handler (%d)", ret); + goto fail; + } + + list_add(&handle->list, &dbus->property_changed_handles); + *phandle = handle; + ret = 0; + +out: + return ret; +fail: + ztp_dbus_unregister_properties_changed_handler(handle); + goto out; +} + +/** + * @brief Unregisters a property changed handler from ztpd. + * + * @param handle The handle to unregister. Must have been obtained from + * ztpd_dbus_register_properties_changed_handler(). + */ +void +ztp_dbus_unregister_properties_changed_handler(struct ztp_dbus_properties_changed_handle *handle) +{ + if (!handle) + return; + + if (handle->slot) { + sd_bus_slot_unref(handle->slot); + handle->slot = NULL; + } + + list_del(&handle->list); + free(handle); +} + +/** + * @brief Processes an update for a file descriptor used for monitoring d-bus + * socket changes. + * + * sd-bus may coalesce multiple ready messages into a single file descriptor + * signal, so a loop is needed to ensure all such messages are processed. + * + * @param fd The file descriptor that has an update. + * @param context The global ztpd instance. + */ +static void +process_fd_update_dbus(int fd, void *context) +{ + __unused(fd); + + uint32_t nmsg = 0; + sd_bus_message *msg; + struct ztp_dbus_client *dbus = (struct ztp_dbus_client *)context; + + for (;;) { + // Retrieve the next message that is available for processing. + int ret = sd_bus_process(dbus->bus, &msg); + if (ret < 0) { + zlog_error("failed to retrieve pending sd-bus message (%d)", ret); + break; + // This indicates no more messages are available for processing. + } else if (ret == 0) { + break; + } + + // Release message reference to indicate we're done with it. + sd_bus_message_unref(msg); + nmsg++; + } + + // Re-configure the file descriptor with updated operations. + int events = sd_bus_get_events(dbus->bus); + struct epoll_event event; + explicit_bzero(&event, sizeof event); + event.events = (events > 0) ? (uint32_t)events : EPOLLIN; + event.data.fd = dbus->fd; + + if (epoll_ctl(dbus->loop->epoll_fd, EPOLL_CTL_MOD, event.data.fd, &event) < 0) + zlog_error("failed to update sd-bus fd settings for epoll (%d)", errno); +} + +/** + * @brief Uninitializes a dbus connector instance. + * + * @param dbus The instance to uninitialize. + */ +void +ztp_dbus_uninitialize(struct ztp_dbus_client *dbus) +{ + if (dbus->fd != -1) { + event_loop_unregister_event(dbus->loop, dbus->fd); + dbus->fd = -1; + } + + if (dbus->bus != NULL) { + sd_bus_unref(dbus->bus); + dbus->bus = NULL; + } +} + +/** + * @brief Initializes an instance of a dbus connector. + * + * @param dbus The instance to initialize. + * @param loop The ztp event loop to run the dbus connection on. + * @return int 0 initialized successfuly, non-zero otherwise. + */ +int +ztp_dbus_initialize(struct ztp_dbus_client *dbus, struct event_loop *loop) +{ + INIT_LIST_HEAD(&dbus->property_changed_handles); + dbus->loop = loop; + dbus->fd = -1; + + // Get a handle to the system bus. + int ret = sd_bus_default_system(&dbus->bus); + if (ret < 0) { + zlog_error("failed to open system bus (%d)", -ret); + goto fail; + } + + // Get the file descriptor for the system bus for use in epoll loop. + int fd = sd_bus_get_fd(dbus->bus); + if (fd < 0) { + ret = fd; + zlog_error("failed to retrieve sd bus file desriptor (%d)", -ret); + goto fail; + } + + dbus->fd = fd; + int events = sd_bus_get_events(dbus->bus); + if (events < 0) { + zlog_error("failed to retrieve sd bus i/o event list (%d)", events); + goto fail; + } + + ret = event_loop_register_event(loop, (uint32_t)events, fd, process_fd_update_dbus, dbus); + if (ret < 0) { + zlog_error("failed to register event for monitoring d-bus updates (%d)", ret); + goto fail; + } + + ret = 0; +out: + return ret; +fail: + ztp_dbus_uninitialize(dbus); + goto out; +} diff --git a/src/dbus/ztp_dbus_client.h b/src/dbus/ztp_dbus_client.h new file mode 100644 index 0000000..5ec1dd5 --- /dev/null +++ b/src/dbus/ztp_dbus_client.h @@ -0,0 +1,78 @@ + +#ifndef __ZTP_DBUS_CLIENT_H__ +#define __ZTP_DBUS_CLIENT_H__ + +#include +#include + +#include "ztp_wpa_supplicant.h" + +struct event_loop; + +/** + * @brief Callback prototype for PropertyChanged handlers. + */ +typedef void (*ztp_dbus_properties_changed_handler)(sd_bus_message *, const char *, void *); + +/** + * @brief Opaque handle for tracking PropertyChanged handlers. + * Full definition is in ztp_dbus_client.c. + */ +struct ztp_dbus_properties_changed_handle; + +/** + * @brief Event control interface for d-bus connections. Primarily used to + * interface with wpa_supplicant using sd-bus. + */ +struct ztp_dbus_client { + sd_bus *bus; + int fd; + struct event_loop *loop; + struct list_head property_changed_handles; +}; + +/** + * @brief Registers a property changed handler function for a d-bus interface. + * This refers to the standard-defined org.freedesktop.DBus.Properties.PropertiesChanged + * signal. + * + * @param dbus The dbus control object. + * @param interface The dbus interface to monitor for property changes. + * @param userdata User data that will be passed to the property change handler. + * @param handler The function that will be invoked upon property changes. + * @param handle A handle referencing the installed property handler. + * @return int The result of the operation. 0 if successful, in which case + * '*handle' will be populated with a value that refers to the monitor. A + * negative error value is returned otherwise. + */ +int +ztp_dbus_register_properties_changed_handler(struct ztp_dbus_client *dbus, const char *interface, void *userdata, ztp_dbus_properties_changed_handler handler, struct ztp_dbus_properties_changed_handle **handle); + +/** + * @brief Unregisters a property changed handler from ztpd. + * + * @param handle The handle to unregister. Must have been obtained from + * ztp_dbus_register_properties_changed_handler(). + */ +void +ztp_dbus_unregister_properties_changed_handler(struct ztp_dbus_properties_changed_handle *handle); + +/** + * @brief Initializes an instance of a dbus connector. + * + * @param dbus The instance to initialize. + * @param loop The ztp event loop to run the dbus connection on. + * @return int 0 initialized successfuly, non-zero otherwise. + */ +int +ztp_dbus_initialize(struct ztp_dbus_client *dbus, struct event_loop *loop); + +/** + * @brief Uninitializes a dbus connector instance. + * + * @param dbus The instance to uninitialize. + */ +void +ztp_dbus_uninitialize(struct ztp_dbus_client *dbus); + +#endif //__ZTP_DBUS_CLIENT_H__ diff --git a/src/dbus/ztp_dbus_configurator.c b/src/dbus/ztp_dbus_configurator.c new file mode 100644 index 0000000..a4b2bf5 --- /dev/null +++ b/src/dbus/ztp_dbus_configurator.c @@ -0,0 +1,793 @@ + +#include +#include + +#include +#include + +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps.h" +#include "bootstrap_info_provider_file.h" +#include "bootstrap_info_provider_settings.h" +#include "ztp_configurator.h" +#include "ztp_configurator_config.h" +#include "ztp_dbus_configurator.h" +#include "ztp_dbus_network_configuration.h" +#include "ztp_log.h" + +/** + * @brief d-bus bootstrap info provider object entry. + */ +struct ztp_dbus_bootstrap_info_provider { + struct list_head list; + struct bootstrap_info_provider *bip; + struct ztp_dbus_configurator *configurator; + uint32_t id; + sd_bus *bus; + sd_bus_slot *slot; + sd_bus_slot *slot_type; + char path[]; +}; + +/** + * @brief Property getter function for the 'Path' property of BootstrapInfoProviderFile objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProviderFile". + * @param property The name of the property. Must be "Path". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_profile_file_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_file_get_path(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_file_instance *bip = (struct bootstrap_info_provider_file_instance *)userdata; + + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, bip->settings->path); + if (ret < 0) + zlog_warning("failed to append bip file path %s to reply message (%d)", bip->settings->path, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'JsonPointerArray' property of BootstrapInfoProviderFile objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProviderFile". + * @param property The name of the property. Must be "JsonPointerArray". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_profile_file_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_file_get_jsonptr_array(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_file_instance *bip = (struct bootstrap_info_provider_file_instance *)userdata; + + const char *value = bip->settings->json_pointer_array; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip json pointer array %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'JsonPointerObject' property of BootstrapInfoProviderFile objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProviderFile". + * @param property The name of the property. Must be "JsonPointerObject". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_profile_file_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_file_get_jsonptr_object(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_file_instance *bip = (struct bootstrap_info_provider_file_instance *)userdata; + + const char *value = bip->settings->json_pointer_object_base; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip json pointer object %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'JsonKeyDppUri' property of BootstrapInfoProviderFile objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProviderFile". + * @param property The name of the property. Must be "JsonKeyDppUri". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_profile_file_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_file_get_jsonkey_dppuri(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_file_instance *bip = (struct bootstrap_info_provider_file_instance *)userdata; + + const char *value = bip->settings->json_key_dpp_uri; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip json key dppuri %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'JsonKeyPublickeyHash' property of BootstrapInfoProviderFile objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProviderFile". + * @param property The name of the property. Must be "JsonKeyPublickeyHash". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_profile_file_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_file_get_jsonkey_publickey_hash(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_file_instance *bip = (struct bootstrap_info_provider_file_instance *)userdata; + + const char *value = bip->settings->json_key_publickeyhash; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip json key publickey hash %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Virtual function table describing methods, properties and signals of + * a ztp d-bus bootstrap information provider. All such providers are + * associated with a com.microsoft.ztp1.Configurator d-bus object. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_configurator_bip_file[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("Path", "s", ztp1_configurator_bip_file_get_path, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("JsonPointerArray", "s", ztp1_configurator_bip_file_get_jsonptr_array, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("JsonPointerObject", "s", ztp1_configurator_bip_file_get_jsonptr_object, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("JsonKeyDppUri", "s", ztp1_configurator_bip_file_get_jsonkey_dppuri, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("JsonKeyPublickeyHash", "s", ztp1_configurator_bip_file_get_jsonkey_publickey_hash, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Property getter function for the 'JsonKeyPublickeyHash' property of BootstrapInfoProviderAzureDps objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProvider.AzureDps". + * @param property The name of the property. Must be "JsonKeyPublickeyHash". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_provider_azure_dps_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_azuredps_get_service_endpoint_uri(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_azure_dps_instance *bip = (struct bootstrap_info_provider_azure_dps_instance *)userdata; + + const char *value = bip->settings->service_endpoint_uri; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip service endpoint uri %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'ServiceEndpointUri' property of BootstrapInfoProviderAzureDps objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProvider.AzureDps". + * @param property The name of the property. Must be "ServiceEndpointUri". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_provider_azure_dps_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_azuredps_get_authority_url(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_azure_dps_instance *bip = (struct bootstrap_info_provider_azure_dps_instance *)userdata; + + const char *value = bip->settings->authority_url; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip authority url %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'ClientId' property of BootstrapInfoProviderAzureDps objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProvider.AzureDps". + * @param property The name of the property. Must be "ClientId". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_provider_azure_dps_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_azuredps_get_clientid(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_azure_dps_instance *bip = (struct bootstrap_info_provider_azure_dps_instance *)userdata; + + const char *value = bip->settings->client_id; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip client id %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'ResourceUri' property of BootstrapInfoProviderAzureDps objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProvider.AzureDps". + * @param property The name of the property. Must be "ResourceUri". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_provider_azure_dps_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_azuredps_get_resource_uri(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_azure_dps_instance *bip = (struct bootstrap_info_provider_azure_dps_instance *)userdata; + + const char *value = bip->settings->resource_uri; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip resource uri %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'ConnectionString' property of BootstrapInfoProviderAzureDps objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProvider.AzureDps". + * @param property The name of the property. Must be "ConnectionString". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_provider_azure_dps_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_azuredps_get_connection_string(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct bootstrap_info_provider_azure_dps_instance *bip = (struct bootstrap_info_provider_azure_dps_instance *)userdata; + + const char *value = bip->settings->connection_string; + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, value); + if (ret < 0) + zlog_warning("failed to append bip resource uri %s to reply message (%d)", value, ret); + + return ret; +} + +/** + * @brief Method to authorize an azure dps bootstrap info provider. This will + * perform oauth2 against the configured dps instance. + * + * @param message The request message. + * @param userdata The user context. Must be of type struct ztp_dbus_bootstrap_info_provider. + * @param error The error proxy to use. + * @return int 0 if the method was successfully executed, non-zero otherwise. + */ +static int +ztp1_configurator_bip_azuredps_authorize(sd_bus_message *message, void *userdata, sd_bus_error *error) +{ + __unused(error); + + struct bootstrap_info_provider_azure_dps_instance *bip = (struct bootstrap_info_provider_azure_dps_instance *)userdata; + + int ret = bootstrap_info_provider_azure_dps_authorize(bip); + return sd_bus_reply_method_return(message, "i", ret); +} + +/** + * @brief Virtual function table describing methods, properties and signals of + * a ztp d-bus bootstrap information provider. All such providers are + * associated with a com.microsoft.ztp1.Configurator d-bus object. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_configurator_bip_azuredps[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("ServiceEndpointUri", "s", ztp1_configurator_bip_azuredps_get_service_endpoint_uri, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("AuthorityUrl", "s", ztp1_configurator_bip_azuredps_get_authority_url, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("ClientId", "s", ztp1_configurator_bip_azuredps_get_clientid, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("ResourceUri", "s", ztp1_configurator_bip_azuredps_get_resource_uri, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("ConnectionString", "s", ztp1_configurator_bip_azuredps_get_connection_string, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_METHOD("Authorize", NULL, "i", ztp1_configurator_bip_azuredps_authorize, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Property getter function for the 'Name' property of BootstrapInfoProvider objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProvider". + * @param property The name of the property. Must be "Name". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct ztp_dbus_bootstrap_info_provider. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_get_name(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_bootstrap_info_provider *obj = (struct ztp_dbus_bootstrap_info_provider *)userdata; + + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, obj->bip->name); + if (ret < 0) + zlog_warning("failed to append bip name %s to reply message (%d)", obj->bip->name, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'Type' property of BootstrapInfoProvider objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProvider". + * @param property The name of the property. Must be "Type". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct ztp_dbus_bootstrap_info_provider. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_get_type(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_bootstrap_info_provider *obj = (struct ztp_dbus_bootstrap_info_provider *)userdata; + + const char *type = bootstrap_info_provider_type_str(obj->bip->type); + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, type); + if (ret < 0) + zlog_warning("failed to append bip type %s to reply message (%d)", type, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'ExpirationTime' property of BootstrapInfoProvider objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProvider". + * @param property The name of the property. Must be "ExpirationTime". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct ztp_dbus_bootstrap_info_provider. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_bip_get_expiration_time(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_bootstrap_info_provider *obj = (struct ztp_dbus_bootstrap_info_provider *)userdata; + + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_UINT32, &obj->bip->settings->expiration_time); + if (ret < 0) + zlog_warning("failed to append bip expiration time %u to reply message (%d)", obj->bip->settings->expiration_time, ret); + + return ret; +} + +/** + * @brief Method to synchronize a bootstrap info provider. + * + * @param message The request message. + * @param userdata The user content. Must be of type struct ztp_dbus_bootstrap_info_provider. + * @param error The error proxy to use. + * @return int 0 if the method was successfully executed, non-zero otherwise. + * Note that zero is returned even if the synchronization itself fails; the + * return value only indicates whether the method was executed. + */ +static int +ztp1_configurator_bip_synchronize(sd_bus_message *message, void *userdata, sd_bus_error *error) +{ + __unused(error); + + struct ztp_dbus_bootstrap_info_provider *obj = (struct ztp_dbus_bootstrap_info_provider *)userdata; + + struct bootstrap_info_sync_options options = { 0 }; + int ret = bootstrap_info_provider_synchronize(obj->bip, &options); + + return sd_bus_reply_method_return(message, "i", ret); +} + +/** + * @brief Virtual function table describing methods, properties and signals of + * a ztp d-bus bootstrap information provider. All such providers are + * associated with a com.microsoft.ztp1.Configurator d-bus object. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_configurator_bip[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("Name", "s", ztp1_configurator_bip_get_name, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("Type", "s", ztp1_configurator_bip_get_type, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("ExpirationTime", "u", ztp1_configurator_bip_get_expiration_time, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_METHOD("Synchronize", NULL, "i", ztp1_configurator_bip_synchronize, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Adds a bootstrap information provider d-bus object to a configurator d-bus object. + * + * @param configurator The d-bus configurator object to add the provider to. + * @param bip The bootstrap information provider to add. The caller must ensure + * this pointer is valid for the lifetime of the bip d-bus object, or until + * ztp_dbus_configurator_bip_remove is called. + * + * @return int 0 if the provider was successfully added, non-zero otherwise. + */ +static int +ztp_dbus_configurator_bip_add(struct ztp_dbus_configurator *configurator, struct bootstrap_info_provider *bip) +{ + char path[ZTP_DBUS_MAX_PATH]; + uint32_t id = configurator->bip_id_next++; + int ret = snprintf(path, sizeof path, "%s/" ZTP_DBUS_CONFIGURATOR_BIP_NAME "/%u", configurator->path, id); + if (ret < 0) { + zlog_error("failed to format ztp dbus configurator bip path"); + return -EINVAL; + } + + size_t pathlength = (size_t)ret; + assert(pathlength <= sizeof path); + + struct ztp_dbus_bootstrap_info_provider *entry = calloc(1, (sizeof *entry) + pathlength); + if (!entry) { + zlog_error("failed to allocate memory for ztp dbus configurator bip object entry"); + return -ENOMEM; + } + + INIT_LIST_HEAD(&entry->list); + entry->id = id; + entry->bip = bip; + entry->bus = configurator->bus; + entry->configurator = configurator; + memcpy(entry->path, path, pathlength); + + ret = sd_bus_add_object_vtable(entry->bus, &entry->slot, entry->path, ZTP_DBUS_CONFIGURATOR_BIP_INTERFACE, vtable_com_microsoft_ztp1_configurator_bip, entry); + if (ret < 0) { + zlog_error("failed to attach bootstrap info provider with path %s to d-bus (%d)", entry->path, ret); + free(entry); + return ret; + } + + switch (bip->type) { + case BOOTSTRAP_INFO_PROVIDER_FILE: { + struct bootstrap_info_provider_file_instance *bip_file = (struct bootstrap_info_provider_file_instance *)bip->context; + ret = sd_bus_add_object_vtable(entry->bus, &entry->slot_type, entry->path, ZTP_DBUS_CONFIGURATOR_BIP_FILE_INTERFACE, vtable_com_microsoft_ztp1_configurator_bip_file, bip_file); + if (ret < 0) + zlog_warning("failed to attach bootstrap info provider file interface with path %s to d-bus (%d)", entry->path, ret); + break; + } + case BOOTSTRAP_INFO_PROVIDER_AZUREDPS: { + struct bootstrap_info_provider_azure_dps_instance *bip_dps = (struct bootstrap_info_provider_azure_dps_instance *)bip->context; + ret = sd_bus_add_object_vtable(entry->bus, &entry->slot_type, entry->path, ZTP_DBUS_CONFIGURATOR_BIP_AZUREDPS_INTERFACE, vtable_com_microsoft_ztp1_configurator_bip_azuredps, bip_dps); + if (ret < 0) + zlog_warning("failed to attach bootstrap info provider azuredps interface with path %s to d-bus (%d)", entry->path, ret); + break; + } + default: { + entry->slot_type = NULL; + break; + } + } + + sd_bus_ref(entry->bus); + list_add(&entry->list, &configurator->bips); + + return 0; +} + +/** + * @brief Removes a bootstrap info provider object from d-bus, freeing any + * owned resources. + * + * @param entry The entry to remove. + */ +static void +ztp_dbus_configurator_bip_remove(struct ztp_dbus_bootstrap_info_provider *entry) +{ + if (entry->slot_type) + sd_bus_slot_unref(entry->slot_type); + if (entry->slot) + sd_bus_slot_unref(entry->slot); + if (entry->bus) + sd_bus_unref(entry->bus); + if (entry->list.next != NULL && !list_empty(&entry->list)) + list_del(&entry->list); + + free(entry); +} + +/** + * @brief Property getter function for the 'DefaultNetwork' property of Configurator objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.Configurator.BootstrapInfoProviderFile". + * @param property The name of the property. Must be "DefaultNetworkConfiguration". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct bootstrap_info_profile_file_instance. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_configurator_get_default_network(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_configurator *obj = (struct ztp_dbus_configurator *)userdata; + const char *objpath = obj->server->network_configuration_default + ? obj->server->network_configuration_default->path + : ""; + + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_OBJECT_PATH, objpath); + if (ret < 0) + zlog_warning("failed to append default network path %s to reply message (%d)", objpath, ret); + + return ret; +} + +/** + * @brief Method to synchronize a bootstrap info provider. + * + * @param message The request message. + * @param userdata The user content. Must be of type struct ztp_dbus_configurator. + * @param error The error proxy to use. + * @return int 0 if the method was successfully executed, non-zero otherwise. + * Note that zero is returned even if the synchronization itself fails; the + * return value only indicates whether the method was executed. + */ +static int +ztp1_configurator_synchronize_bootstrappinginfo(sd_bus_message *message, void *userdata, sd_bus_error *error) +{ + struct ztp_dbus_configurator *obj = (struct ztp_dbus_configurator *)userdata; + + struct bootstrap_info_sync_options options = { 0 }; + int ret = ztp_configurator_synchronize_bootstrapping_info(obj->configurator, &options); + if (ret < 0) { + sd_bus_error_set_errno(error, ret); + return ret; + } + + return sd_bus_reply_method_return(message, "u", (uint32_t)ret); +} + +/** + * @brief Virtual table describing methods, properties and signals of a ztp + * d-bus configurator. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_configurator[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("DefaultNetworkConfiguration", "o", ztp1_configurator_get_default_network, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_METHOD("SynchronizeBootstrappingInfo", NULL, "u", ztp1_configurator_synchronize_bootstrappinginfo, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Registers a configurator with d-bus. + * + * @param server The d-bus server to register the configurator with. + * @param configurator The configurator to register. The caller is responsible + * for ensuring that the provided pointer is valid for the lifetime of the + * d-bus object, or, until ztp_dbus_configurator unregister is called with the + * same pointer. + * @return int 0 if registration was successful, non-zero otherwise. + */ +int +ztp_dbus_configurator_register(struct ztp_dbus_server *server, struct ztp_configurator *configurator) +{ + char path[ZTP_DBUS_MAX_PATH]; + uint32_t id = server->configurator_id_next++; + int ret = snprintf(path, sizeof path, ZTP_DBUS_CONFIGURATOR_PATH "/%u", id); + if (ret < 0) { + zlog_error("failed to format ztp dbus configurator object path"); + return -EINVAL; + } + + size_t pathlength = (size_t)ret; + assert(pathlength <= sizeof path); + + struct ztp_dbus_configurator *entry = calloc(1, (sizeof *entry) + pathlength); + if (!entry) { + zlog_error("failed to allocate memory for ztp dbus configurator object entry"); + return -ENOMEM; + } + + INIT_LIST_HEAD(&entry->list); + INIT_LIST_HEAD(&entry->bips); + entry->id = id; + entry->bip_id_next = 0; + entry->bus = server->bus; + entry->server = server; + entry->configurator = configurator; + memcpy(entry->path, path, pathlength); + + ret = sd_bus_add_object_vtable(entry->bus, &entry->slot, entry->path, ZTP_DBUS_CONFIGURATOR_INTERFACE, vtable_com_microsoft_ztp1_configurator, entry); + if (ret < 0) { + zlog_error("failed to attach configurator with path %s to d-bus (%d)", entry->path, ret); + free(entry); + return ret; + } + + sd_bus_ref(entry->bus); + list_add(&entry->list, &server->configurators); + + struct bootstrap_info_provider *bip; + list_for_each_entry (bip, &configurator->bootstrap_info_providers, list) { + ret = ztp_dbus_configurator_bip_add(entry, bip); + if (ret < 0) + zlog_warning("failed to add bootstrap info provider %s to d-bus (%d)", bip->name, ret); + } + + return 0; +} + +/** + * @brief Finds a configurator object entry, given its instance pointer. + * + * @param server The server owning the configurator object. + * @param configurator The configurator instance pointers. + * @return struct ztp_dbus_configurator* A pointer to the d-bus object entry if + * one matching 'configurator' exists. NULL otherwise. + */ +static struct ztp_dbus_configurator * +find_configurator(struct ztp_dbus_server *server, const struct ztp_configurator *configurator) +{ + struct ztp_dbus_configurator *entry; + list_for_each_entry (entry, &server->configurators, list) { + if (entry->configurator == configurator) { + return entry; + } + } + + return NULL; +} + +/** + * @brief Unregisters a configurator from d-bus, removing its d-bus object and all its children from the bus. + * + * @param server The server the configurator was registered with. + * @param configurator The configurator instance associated with the bus object. + * @return int 0 if the configurator was unregisters, -ENOENT if the specified + * configurator was not previously registered with the bus, and non-zero + * otherwise. + */ +int +ztp_dbus_configurator_unregister(struct ztp_dbus_server *server, struct ztp_configurator *configurator) +{ + struct ztp_dbus_configurator *entry = find_configurator(server, configurator); + if (!entry) + return -ENOENT; + + struct ztp_dbus_bootstrap_info_provider *bip; + struct ztp_dbus_bootstrap_info_provider *biptmp; + list_for_each_entry_safe (bip, biptmp, &entry->bips, list) { + ztp_dbus_configurator_bip_remove(bip); + } + + if (entry->slot) + sd_bus_slot_unref(entry->slot); + if (entry->bus) + sd_bus_unref(entry->bus); + if (entry->list.next != NULL && !list_empty(&entry->list)) + list_del(&entry->list); + + free(entry); + + return 0; +} diff --git a/src/dbus/ztp_dbus_configurator.h b/src/dbus/ztp_dbus_configurator.h new file mode 100644 index 0000000..40e6c2f --- /dev/null +++ b/src/dbus/ztp_dbus_configurator.h @@ -0,0 +1,82 @@ + +#ifndef __ZTP_DBUS_CONFIGURATOR_H__ +#define __ZTP_DBUS_CONFIGURATOR_H__ + +#include "ztp_dbus_server.h" + +struct ztp_configurator; + +/** + * @brief Configurator dbus name, path, and interface definitions. + */ +#define ZTP_DBUS_CONFIGURATOR_NAME "Configurator" +#define ZTP_DBUS_CONFIGURATOR_PATH ZTP_DBUS_SERVER_CHILD_PATH(ZTP_DBUS_CONFIGURATOR_NAME) +#define ZTP_DBUS_CONFIGURATOR_INTERFACE ZTP_DBUS_SERVER_CHILD_INTERFACE(ZTP_DBUS_CONFIGURATOR_NAME) + +/** + * @brief Macro to help construct configurator child interfaces. + */ +#define ZTP_DBUS_CONFIGURATOR_CHILD_INTERFACE(i) ZTP_DBUS_CHILD_INTERFACE(ZTP_DBUS_CONFIGURATOR_INTERFACE, i) + +/** + * @brief Bootstrap info provider dbus name and interface definitions. + */ +#define ZTP_DBUS_CONFIGURATOR_BIP_NAME "BootstrapInfoProvider" +#define ZTP_DBUS_CONFIGURATOR_BIP_FILE_NAME "File" +#define ZTP_DBUS_CONFIGURATOR_BIP_AZUREDPS_NAME "AzureDps" +#define ZTP_DBUS_CONFIGURATOR_BIP_INTERFACE ZTP_DBUS_CONFIGURATOR_CHILD_INTERFACE(ZTP_DBUS_CONFIGURATOR_BIP_NAME) + +/** + * @brief Macro to help constructor bootstrap info provider child interface definitions. + */ +#define ZTP_DBUS_CONFIGURATOR_BIP_CHILD_INTERFACE(i) ZTP_DBUS_CHILD_INTERFACE(ZTP_DBUS_CONFIGURATOR_BIP_INTERFACE, i) + +/** + * @brief Bootstrap info provider derivative dbus name and interface definitions. + */ +#define ZTP_DBUS_CONFIGURATOR_BIP_FILE_INTERFACE ZTP_DBUS_CONFIGURATOR_BIP_CHILD_INTERFACE(ZTP_DBUS_CONFIGURATOR_BIP_FILE_NAME) +#define ZTP_DBUS_CONFIGURATOR_BIP_AZUREDPS_INTERFACE ZTP_DBUS_CONFIGURATOR_BIP_CHILD_INTERFACE(ZTP_DBUS_CONFIGURATOR_BIP_AZUREDPS_NAME) + +/** + * @brief d-bus configurator object entry. + */ +struct ztp_dbus_configurator { + struct list_head list; + struct list_head bips; + struct list_head networks; + struct ztp_configurator *configurator; + struct ztp_dbus_server *server; + sd_bus *bus; + sd_bus_slot *slot; + uint32_t id; + uint32_t bip_id_next; + uint32_t network_id_next; + char path[]; +}; + +/** + * @brief Registers a configurator with d-bus. + * + * @param server The d-bus server to register the configurator with. + * @param configurator The configurator to register. The caller is responsible + * for ensuring that the provided pointer is valid for the lifetime of the + * d-bus object, or, until ztp_dbus_configurator unregister is called with the + * same pointer. + * @return int 0 if registration was successful, non-zero otherwise. + */ +int +ztp_dbus_configurator_register(struct ztp_dbus_server *server, struct ztp_configurator *configurator); + +/** + * @brief Unregisters a configurator from d-bus, removing its d-bus object and all its children from the bus. + * + * @param server The server the configurator was registered with. + * @param configurator The configurator instance associated with the bus object. + * @return int 0 if the configurator was unregisters, -ENOENT if the specified + * configurator was not previously registered with the bus, and non-zero + * otherwise. + */ +int +ztp_dbus_configurator_unregister(struct ztp_dbus_server *server, struct ztp_configurator *configurator); + +#endif //__ZTP_DBUS_CONFIGURATOR_H__ diff --git a/src/dbus/ztp_dbus_network_configuration.c b/src/dbus/ztp_dbus_network_configuration.c new file mode 100644 index 0000000..bafefbe --- /dev/null +++ b/src/dbus/ztp_dbus_network_configuration.c @@ -0,0 +1,636 @@ + +#include +#include +#include + +#include + +#include "dbus_message_helpers.h" +#include "dpp.h" +#include "ztp_dbus_configurator.h" +#include "ztp_dbus_network_configuration.h" +#include "ztp_log.h" + +/** + * @brief Property getter function for the 'Passphrase' property of DppCredential.Sae objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.DppCredential.Sae". + * @param property The name of the property. Must be "Properties". + * @param reply The reply message. + * @param userdata The user context. Must be of type 'struct dpp_network_credential_sae'. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_dpp_credential_sae_get_passphrase(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct dpp_network_credential_sae *sae = (struct dpp_network_credential_sae *)userdata; + + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, sae->passphrase); + if (ret < 0) { + zlog_error("failed to append sae credential passphrase (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Virtual function table describing methods, properties and signals of + * a ztp d-bus dpp network credential. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_dpp_credential_sae[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("Passphrase", "s", ztp1_dpp_credential_sae_get_passphrase, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Property getter function for the 'Properties' property of DppCredential.Psk objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.DppCredential.Psk". + * @param property The name of the property. Must be "Properties". + * @param reply The reply message. + * @param userdata The user context. Must be of type 'struct dpp_network_credential_psk'. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_dpp_credential_psk_get_properties(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct dpp_network_credential_psk *psk = (struct dpp_network_credential_psk *)userdata; + + int ret = sd_bus_message_open_container(reply, SD_BUS_TYPE_ARRAY, "{sv}"); + if (ret < 0) { + zlog_error("failed to open psk network credential dictionary array container (%d)", ret); + return ret; + } + + { + switch (psk->type) { + case PSK_CREDENTIAL_TYPE_PASSPHRASE: { + ret = dbus_append_dict_entry_string(reply, "Passphrase", psk->passphrase.ascii); + if (ret < 0) { + zlog_error("failed to append psk credential passphrase (%d)", ret); + return ret; + } + break; + } + case PSK_CREDENTIAL_TYPE_PSK: { + ret = dbus_append_dict_entry_string(reply, "Psk", psk->key.hex); + if (ret < 0) { + zlog_error("failed tp append psk credential key (%d)", ret); + return ret; + } + break; + } + default: + break; + } + } + + ret = sd_bus_message_close_container(reply); + if (ret < 0) { + zlog_error("failed to close psk credential dictionary container (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Virtual function table describing methods, properties and signals of + * a ztp d-bus dpp network credential. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_dpp_credential_psk[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("Properties", "a{sv}", ztp1_dpp_credential_psk_get_properties, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Property getter function for the 'Akm' property of DppCredential objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.DppCredential". + * @param property The name of the property. Must be "Akm". + * @param reply The reply message. + * @param userdata The user context. Must be of type 'struct dpp_network_credential'. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_dpp_credential_get_type(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct dpp_network_credential *credential = (struct dpp_network_credential *)userdata; + + const char *akm = dpp_akm_str(credential->akm); + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, akm); + if (ret < 0) + zlog_warning("failed to add akm %s to reply message (%d)", akm, ret); + + return ret; +} + +/** + * @brief Virtual function table describing methods, properties and signals of + * a ztp d-bus dpp network credential. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_dpp_credential[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("Akm", "s", ztp1_dpp_credential_get_type, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Removes a dpp network credential from a network object. + * + * @param credential The credential object to remove. + */ +static void +ztp_dbus_network_configuration_credential_remove(struct ztp_dbus_dpp_credential *obj) +{ + if (obj->slot) + sd_bus_slot_unref(obj->slot); + if (obj->slot_child) + sd_bus_slot_unref(obj->slot_child); + if (obj->bus) + sd_bus_unref(obj->bus); + if (obj->list.next != NULL && !list_empty(&obj->list)) + list_del(&obj->list); + + free(obj); +} + +/** + * @brief Adds a network credential to a network configuration d-bus object. + * + * @param entry The entry to add the credential to. + * @param credential The credential to add. + * @return int 0 if the credential was successully added, non-zero otherwise. + */ +static int +ztp_dbus_network_configuration_credential_add(struct ztp_dbus_network_configuration *network, struct dpp_network_credential *credential) +{ + char path[ZTP_DBUS_MAX_PATH]; + uint32_t id = network->credential_id_next++; + int ret = snprintf(path, sizeof path, "%s/" ZTP_DBUS_NETWORK_DPP_CREDENTIAL_NAME "/%u", network->path, id); + if (ret < 0) { + zlog_error("failed to format dbus dpp network credential path"); + return -EINVAL; + } + + size_t pathlength = (size_t)ret; + assert(pathlength <= sizeof path); + + struct ztp_dbus_dpp_credential *entry = calloc(1, (sizeof *entry) + pathlength); + if (!entry) { + zlog_error("failed to allocate memory for dbus dpp network credential"); + return -ENOMEM; + } + + INIT_LIST_HEAD(&entry->list); + entry->id = id; + entry->bus = network->bus; + entry->credential = credential; + memcpy(entry->path, path, pathlength); + sd_bus_ref(entry->bus); + + ret = sd_bus_add_object_vtable(entry->bus, &entry->slot, entry->path, ZTP_DBUS_NETWORK_DPP_CREDENTIAL_INTERFACE, vtable_com_microsoft_ztp1_dpp_credential, credential); + if (ret < 0) { + zlog_error("failed to attach dpp network credential to d-bus (%d)", ret); + ztp_dbus_network_configuration_credential_remove(entry); + return ret; + } + + switch (credential->akm) { + case DPP_AKM_PSK: { + ret = sd_bus_add_object_vtable(entry->bus, &entry->slot_child, entry->path, ZTP_DBUS_NETWORK_DPP_CREDENTIAL_PSK_INTERFACE, vtable_com_microsoft_ztp1_dpp_credential_psk, &credential->psk); + if (ret < 0) { + zlog_error("failed to attach dpp psk credential to d-bus (%d)", ret); + ztp_dbus_network_configuration_credential_remove(entry); + return ret; + } + break; + } + case DPP_AKM_SAE: { + ret = sd_bus_add_object_vtable(entry->bus, &entry->slot_child, entry->path, ZTP_DBUS_NETWORK_DPP_CREDENTIAL_SAE_INTERFACE, vtable_com_microsoft_ztp1_dpp_credential_sae, &credential->sae); + if (ret < 0) { + zlog_error("failed to attach dpp sae credential to d-bus (%d)", ret); + ztp_dbus_network_configuration_credential_remove(entry); + return ret; + } + break; + } + default: + break; + } + + list_add(&entry->list, &network->credentials); + + return 0; +} + +/** + * @brief Property getter function for the 'WifiTechnology' property of NetworkConfiguration objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.NetworkConfiguration". + * @param property The name of the property. Must be "WifiTechnology". + * @param reply The reply message. + * @param userdata The user context. Must be of type 'struct dpp_network'. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_network_configuration_get_wifitech(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(userdata); + __unused(ret_error); + + static const char infrastructure[] = "Infrastructure"; + + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, infrastructure); + if (ret < 0) + zlog_warning("failed to add infrastructure %s to reply message (%d)", infrastructure, ret); + + return ret; +} + +/** + * @brief Property getter function for the 'Ssid' property of NetworkConfiguration objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.NetworkConfiguration". + * @param property The name of the property. Must be "Ssid". + * @param reply The reply message. + * @param userdata The user context. Must be of type 'struct dpp_network'. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_network_configuration_get_ssid(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_network_configuration *entry = (struct ztp_dbus_network_configuration *)userdata; + + int ret = sd_bus_message_append_basic(reply, SD_BUS_TYPE_STRING, entry->network->discovery.ssid); + if (ret < 0) + zlog_warning("failed to add ssid to reply message (%d)", ret); + + return ret; +} + +/** + * @brief Retrieves an array network credentials. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.NetworkConfiguration". + * @param property The name of the property. Must be "Credentials". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct dpp_network. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_network_configuration_get_credentials(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_network_configuration *network = (struct ztp_dbus_network_configuration *)userdata; + + int ret = sd_bus_message_open_container(reply, SD_BUS_TYPE_ARRAY, "o"); + if (ret < 0) { + zlog_error("failed to open array container in reply message for 'Credentials' property (%d)", ret); + return ret; + } + + { + struct ztp_dbus_dpp_credential *credential; + list_for_each_entry (credential, &network->credentials, list) { + ret = sd_bus_message_append(reply, "o", credential->path); + if (ret < 0) { + zlog_error("failed to append network credential to reply message (%d)", ret); + continue; + } + } + } + + ret = sd_bus_message_close_container(reply); + if (ret < 0) { + zlog_warning("failed to close array container in reply message for 'Configurators' property (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Virtual function table describing methods, properties and signals of + * a ztp d-bus bootstrap information provider. All such providers are + * associated with a com.microsoft.ztp1.Configurator d-bus object. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_network_configuration[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("WifiTechnology", "s", ztp1_network_configuration_get_wifitech, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("Ssid", "s", ztp1_network_configuration_get_ssid, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("Credentials", "ao", ztp1_network_configuration_get_credentials, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Registers a network with the specified configurator. + * + * @param manager The d-bus manager object associated with the network. + * @param network The network to add. The caller is responsible for ensuring + * this object remains valid for the lifetime of the parent object, or until + * ztp_dbus_network_configuration_manager_unregister is called. + * @param entryp Optional output argument to hold the allocated d-bus entry object. + * + * @return int 0 if the network was successfully added, non-zero otherwise. + */ +int +ztp_dbus_network_configuration_manager_register(struct ztp_dbus_network_configuration_manager *manager, struct dpp_network *network, struct ztp_dbus_network_configuration **entryp) +{ + char path[ZTP_DBUS_MAX_PATH]; + uint32_t id = manager->network_id_next++; + int ret = snprintf(path, sizeof path, "%s/Networks/%u", manager->path, id); + if (ret < 0) { + zlog_error("failed to format dbus dpp network configuration path"); + return -EINVAL; + } + + size_t pathlength = (size_t)ret; + assert(pathlength <= sizeof path); + + struct ztp_dbus_network_configuration *entry = calloc(1, (sizeof *entry) + pathlength); + if (!entry) { + zlog_error("failed to allocate memory for dbus dpp network configuration"); + return -ENOMEM; + } + + INIT_LIST_HEAD(&entry->list); + INIT_LIST_HEAD(&entry->credentials); + entry->id = id; + entry->bus = manager->bus; + entry->network = network; + memcpy(entry->path, path, pathlength); + + ret = sd_bus_add_object_vtable(entry->bus, &entry->slot, entry->path, ZTP_DBUS_NETWORK_INTERFACE, vtable_com_microsoft_ztp1_network_configuration, entry); + if (ret < 0) { + zlog_error("failed to attach dpp network credential to d-bus (%d)", ret); + free(entry); + return ret; + } + + struct dpp_network_credential *credential; + list_for_each_entry (credential, &network->credentials, list) { + ret = ztp_dbus_network_configuration_credential_add(entry, credential); + if (ret < 0) { + zlog_warning("failed to add network credential to d-bus network object with id=%u (%d)", id, ret); + continue; + } + } + + sd_bus_ref(entry->bus); + list_add(&entry->list, &manager->networks); + + if (entryp) + *entryp = entry; + + return 0; +} + +/** + * @brief Unregisters a network from its parent d-bus configurator object. + * + * @param network The network to remove. + */ +void +ztp_dbus_network_configuration_manager_unregister(struct ztp_dbus_network_configuration **pnetwork) +{ + if (!pnetwork || !*pnetwork) + return; + + struct ztp_dbus_network_configuration *network = *pnetwork; + struct ztp_dbus_dpp_credential *credential; + struct ztp_dbus_dpp_credential *credentialtmp; + + list_for_each_entry_safe (credential, credentialtmp, &network->credentials, list) { + ztp_dbus_network_configuration_credential_remove(credential); + } + + if (network->slot) + sd_bus_slot_unref(network->slot); + if (network->bus) + sd_bus_unref(network->bus); + + list_del(&network->list); + free(network); + + *pnetwork = NULL; +} + +/* + * @brief Retrieves an array of network configuration objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1.NetworkConfigurationManager". + * @param property The name of the property. Must be "Networks". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct ztp_dbus_network_configuration_manager. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise + */ +static int +ztp1_network_manager_get_networks(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_network_configuration_manager *manager = (struct ztp_dbus_network_configuration_manager *)userdata; + + int ret = sd_bus_message_open_container(reply, SD_BUS_TYPE_ARRAY, "o"); + if (ret < 0) { + zlog_error("failed to open reply message for 'Networks' property (%d)", ret); + return ret; + } + + { + struct ztp_dbus_network_configuration *entry; + list_for_each_entry (entry, &manager->networks, list) { + ret = sd_bus_message_append(reply, "o", entry->path); + if (ret < 0) { + zlog_error("failed to append network configuration path %s to reply message (%d)", entry->path, ret); + continue; + } + } + } + + ret = sd_bus_message_close_container(reply); + if (ret < 0) { + zlog_warning("failed to close reply message container for 'Networks' property (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Virtual table describing methods, properties, and signals of the ztp + * d-bus network configuration manager interface. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1_network_configuration_manager[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("Networks", "ao", ztp1_network_manager_get_networks, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Initializes a network manager instance. + * + * @param manager The manager instance to initialize. + * @return int 0 if the + */ +static int +ztp_dbus_network_configuration_manager_initialize(struct ztp_dbus_network_configuration_manager *manager) +{ + sd_bus *bus; + int ret = sd_bus_default_system(&bus); + if (ret < 0) { + zlog_error("failed to open system bus (%d)", ret); + return ret; + } + + sd_bus_slot *slot_vtable = NULL; + ret = sd_bus_add_object_vtable(bus, + &slot_vtable, + manager->path, + ZTP_DBUS_SERVER_NETWORK_CONFIGURATION_MANAGER_INTERFACE, + vtable_com_microsoft_ztp1_network_configuration_manager, + manager); + if (ret < 0) { + zlog_error("failed to install '%s' object vtable (%d)", ZTP_DBUS_SERVER_NETWORK_CONFIGURATION_MANAGER_INTERFACE, ret); + return ret; + } + + INIT_LIST_HEAD(&manager->networks); + manager->network_id_next = 0; + manager->bus = bus; + manager->slot_vtable = slot_vtable; + + return 0; +} + +/** + * @brief Creates and initializes a d-bus network configuration manager. + * + * @param manager An output pointer to receive the newly created manager. + * @param path The base d-bus path the manager will own. + * @return int 0 if the manager was successfully initialized, non-zero otherwise. + */ +int +ztp_dbus_network_configuration_manager_create(struct ztp_dbus_network_configuration_manager **pmanager, const char *path) +{ + size_t pathlength = strlen(path) + sizeof ZTP_DBUS_SERVER_NETWORK_CONFIGURATION_MANAGER_NAME + 1 /* / */; + struct ztp_dbus_network_configuration_manager *manager = calloc(1, (sizeof *manager) + pathlength); + if (!manager) { + zlog_error("failed to allocate memory for dbus network manager"); + return -ENOMEM; + } + + snprintf(manager->path, pathlength, "%s/" ZTP_DBUS_SERVER_NETWORK_CONFIGURATION_MANAGER_NAME, path); + + int ret = ztp_dbus_network_configuration_manager_initialize(manager); + if (ret < 0) { + zlog_error("failed to initialize dbus network manager (%d)", ret); + free(manager); + return ret; + } + + *pmanager = manager; + + return 0; +} + +/** + * @brief Uninitializes a dbus network configuration manager. + * + * @param manager + */ +static void +ztp_dbus_network_configuration_manager_uninitialize(struct ztp_dbus_network_configuration_manager *manager) +{ + struct ztp_dbus_network_configuration *network; + struct ztp_dbus_network_configuration *networktmp; + list_for_each_entry_safe (network, networktmp, &manager->networks, list) { + ztp_dbus_network_configuration_manager_unregister(&network); + } + + if (manager->bus) { + sd_bus_unref(manager->bus); + manager->bus = NULL; + } +} + +/** + * @brief Uninitializes and destroys a d-bus network configuration manager. + * This will de-register all known d-bus network configuration objects from the + * bus. + * + * @param manager The manager to uninitialize and destroy. + */ +void +ztp_dbus_network_configuration_manager_destroy(struct ztp_dbus_network_configuration_manager **manager) +{ + if (!manager || !*manager) + return; + + ztp_dbus_network_configuration_manager_uninitialize(*manager); + free(*manager); + + *manager = NULL; +} diff --git a/src/dbus/ztp_dbus_network_configuration.h b/src/dbus/ztp_dbus_network_configuration.h new file mode 100644 index 0000000..c817532 --- /dev/null +++ b/src/dbus/ztp_dbus_network_configuration.h @@ -0,0 +1,117 @@ + +#ifndef __ZTP_DBUS_NETWORK_CONFIGURATION_H__ +#define __ZTP_DBUS_NETWORK_CONFIGURATION_H__ + +#include "ztp_dbus_server.h" + +/** + * @brief Dpp network dbus name, path, and interface definitions. + */ +#define ZTP_DBUS_NETWORK_CONFIGURATION_NAME "DppNetworkConfiguration" +#define ZTP_DBUS_NETWORK_INTERFACE ZTP_DBUS_SERVER_CHILD_INTERFACE(ZTP_DBUS_NETWORK_CONFIGURATION_NAME) +#define ZTP_DBUS_NETWORK_CHILD_INTERFACE(i) ZTP_DBUS_CHILD_INTERFACE(ZTP_DBUS_NETWORK_INTERFACE, i) + +/** + * @brief d-bus network configuration object entry. + */ +struct ztp_dbus_network_configuration { + struct list_head list; + struct list_head credentials; + struct dpp_network *network; + sd_bus *bus; + sd_bus_slot *slot; + uint32_t id; + uint32_t credential_id_next; + char path[]; +}; + +/** + * @brief Dpp network credentials dbus name, path, and interface definitions. + */ +#define ZTP_DBUS_NETWORK_DPP_CREDENTIAL_NAME "DppCredential" +#define ZTP_DBUS_NETWORK_DPP_CREDENTIAL_INTERFACE ZTP_DBUS_NETWORK_CHILD_INTERFACE(ZTP_DBUS_NETWORK_DPP_CREDENTIAL_NAME) +#define ZTP_DBUS_NETWORK_DPP_CREDENTIAL_CHILD_INTERFACE(i) ZTP_DBUS_CHILD_INTERFACE(ZTP_DBUS_NETWORK_DPP_CREDENTIAL_INTERFACE, i) + +/** + * @brief d-bus dpp network credential object entry. + */ +struct ztp_dbus_dpp_credential { + struct list_head list; + struct dpp_network_credential *credential; + sd_bus *bus; + sd_bus_slot *slot; + sd_bus_slot *slot_child; + uint32_t id; + char path[]; +}; + +/** + * @brief Dpp network credential interface types. + */ +#define ZTP_DBUS_NETWORK_DPP_CREDENTIAL_PSK_NAME "Psk" +#define ZTP_DBUS_NETWORK_DPP_CREDENTIAL_PSK_INTERFACE ZTP_DBUS_NETWORK_DPP_CREDENTIAL_CHILD_INTERFACE(ZTP_DBUS_NETWORK_DPP_CREDENTIAL_PSK_NAME) +#define ZTP_DBUS_NETWORK_DPP_CREDENTIAL_SAE_NAME "Sae" +#define ZTP_DBUS_NETWORK_DPP_CREDENTIAL_SAE_INTERFACE ZTP_DBUS_NETWORK_DPP_CREDENTIAL_CHILD_INTERFACE(ZTP_DBUS_NETWORK_DPP_CREDENTIAL_SAE_NAME) + +/** + * @brief d-bus interface name for the network profile manager. + */ +#define ZTP_DBUS_SERVER_NETWORK_CONFIGURATION_MANAGER_NAME "NetworkConfigurationManager" +#define ZTP_DBUS_SERVER_NETWORK_CONFIGURATION_MANAGER_INTERFACE ZTP_DBUS_SERVER_CHILD_INTERFACE(ZTP_DBUS_SERVER_NETWORK_CONFIGURATION_MANAGER_NAME) + +/** + * @brief d-bus network configuration manager. Manages all network + * configuration d-bus objects. + */ +struct ztp_dbus_network_configuration_manager { + struct list_head networks; + uint32_t network_id_next; + sd_bus *bus; + sd_bus_slot *slot_vtable; + char path[]; +}; + +/** + * @brief Registers a network with the specified configurator. + * + * @param manager The d-bus manager object associated with the network. + * @param network The network to add. The caller is responsible for ensuring + * this object remains valid for the lifetime of the parent object, or until + * ztp_dbus_network_configuration_manager_unregister is called. + * @param entryp Optional output argument to hold the allocated d-bus entry object. + * + * @return int 0 if the network was successfully added, non-zero otherwise. + */ +int +ztp_dbus_network_configuration_manager_register(struct ztp_dbus_network_configuration_manager *manager, struct dpp_network *network, struct ztp_dbus_network_configuration **entryp); + +/** + * @brief Unregisters a network from its parent d-bus configurator object. + * + * @param pnetwork Pointer to the network to remove. Referenced memory will be + * set to NULL, so this pointer must not be used following this call. + */ +void +ztp_dbus_network_configuration_manager_unregister(struct ztp_dbus_network_configuration **pnetwork); + +/** + * @brief Creates and initializes a d-bus network configuration manager. + * + * @param manager An output pointer to receive the newly created manager. + * @param path The base d-bus path the manager will own. + * @return int 0 if the manager was successfully initialized, non-zero otherwise. + */ +int +ztp_dbus_network_configuration_manager_create(struct ztp_dbus_network_configuration_manager **manager, const char *path); + +/** + * @brief Uninitializes and destroys a d-bus network configuration manager. + * This will de-register all known d-bus network configuration objects from the + * bus. + * + * @param manager The manager to uninitialize and destroy. + */ +void +ztp_dbus_network_configuration_manager_destroy(struct ztp_dbus_network_configuration_manager **manager); + +#endif //__ZTP_DBUS_NETWORK_CONFIGURATION_H__ diff --git a/src/dbus/ztp_dbus_server.c b/src/dbus/ztp_dbus_server.c new file mode 100644 index 0000000..80b96d1 --- /dev/null +++ b/src/dbus/ztp_dbus_server.c @@ -0,0 +1,1563 @@ + +#include +#include +#include + +#include +#include + +#include "bootstrap_info_provider.h" +#include "bootstrap_info_provider_azure_dps.h" +#include "bootstrap_info_provider_file.h" +#include "bootstrap_info_provider_settings.h" +#include "dbus_message_helpers.h" +#include "dpp.h" +#include "ztp_configurator_config.h" +#include "ztp_dbus_configurator.h" +#include "ztp_dbus_network_configuration.h" +#include "ztp_dbus_server.h" +#include "ztp_log.h" +#include "ztp_settings.h" + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" +#endif //__clang__ + +/** + * @brief Helper macros for logging dbus server specific messages. + */ +#define zlog_dbus(_prio, _fmt, ...) zlog_##_prio("dbus: " _fmt, ##__VA_ARGS__) +#define zlog_panic_dbus(_fmt, ...) zlog_dbus(panic, _fmt, ##__VA_ARGS__) +#define zlog_alert_dbus(_fmt, ...) zlog_dbus(alert, _fmt, ##__VA_ARGS__) +#define zlog_critical_dbus(_fmt, ...) zlog_dbus(critical, _fmt, ##__VA_ARGS__) +#define zlog_error_dbus(_fmt, ...) zlog_dbus(error, _fmt, ##__VA_ARGS__) +#define zlog_warning_dbus(_fmt, ...) zlog_dbus(warning, _fmt, ##__VA_ARGS__) +#define zlog_notice_dbus(_fmt, ...) zlog_dbus(notice, _fmt, ##__VA_ARGS__) +#define zlog_info_dbus(_fmt, ...) zlog_dbus(info, _fmt, ##__VA_ARGS__) +#define zlog_debug_dbus(_fmt, ...) zlog_dbus(debug, _fmt, ##__VA_ARGS__) + +/** + * @brief Retrieves an array of active configurator objects. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1". + * @param property The name of the property. Must be "Configurators". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct ztp_dbus_server. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_get_configurators(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_server *server = (struct ztp_dbus_server *)userdata; + + int ret = sd_bus_message_open_container(reply, SD_BUS_TYPE_ARRAY, "o"); + if (ret < 0) { + zlog_error_dbus("failed to open reply message for 'Configurators' property (%d)", ret); + return ret; + } + + { + struct ztp_dbus_configurator *entry; + list_for_each_entry (entry, &server->configurators, list) { + ret = sd_bus_message_append(reply, "o", entry->path); + if (ret < 0) { + zlog_error_dbus("failed to append configurator path %s to reply message (%d)", entry->path, ret); + continue; + } + } + } + + ret = sd_bus_message_close_container(reply); + if (ret < 0) { + zlog_warning_dbus("failed to close reply message container for 'Configurators' property (%d)", ret); + return ret; + } + + return 0; +} + +/* + * @brief Retrieves an array of active dpp roles. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1". + * @param property The name of the property. Must be "Roles". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct ztp_dbus_server. + * @param ret_error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_get_dpp_roles(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + __unused(ret_error); + + struct ztp_dbus_server *server = (struct ztp_dbus_server *)userdata; + + int ret = sd_bus_message_open_container(reply, SD_BUS_TYPE_ARRAY, "s"); + if (ret < 0) { + zlog_error_dbus("failed to open reply message for 'Roles' property (%d)", ret); + return ret; + } + + { + for (size_t i = 0; i < ARRAY_SIZE(server->settings->dpp_roles_activated); i++) { + if (server->settings->dpp_roles_activated[i]) { + const char *role = dpp_device_role_str((enum dpp_device_role)i); + ret = sd_bus_message_append(reply, "s", role); + if (ret < 0) { + zlog_error_dbus("failed to append role %s to 'Roles' reply message (%d)", role, ret); + continue; + } + } + } + } + + ret = sd_bus_message_close_container(reply); + if (ret < 0) { + zlog_warning_dbus("failed to close reply message container for 'Roles' property (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_BI_PROVIDER_FILE_CHILDREN "(ss)(ss)(ss)(ss)(ss)" +#define DBUS_DATA_BI_PROVIDER_FILE_SIGNATURE "(" DBUS_DATA_BI_PROVIDER_FILE_CHILDREN ")" + +static int +ztp1_populate_bootstrap_info_provider_settings_file(sd_bus_message *message, const struct bootstrap_info_provider_file_settings *settings) +{ + const struct { + const char *key; + const char *value; + } entries[] = { + { "Path", settings->path }, + { "JsonKeyDppUri", settings->json_key_dpp_uri }, + { "JsonKeyPublickeyHash", settings->json_key_publickeyhash }, + { "JsonPointerObject", settings->json_pointer_object_base }, + { "JsonPointerArray", settings->json_pointer_array }, + }; + + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_BI_PROVIDER_FILE_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to open file bootstrap info provider container (%d)", ret); + return ret; + } + + { + for (size_t i = 0; i < ARRAY_SIZE(entries); i++) { + ret = dbus_append_kv_pair_string(message, entries[i].key, entries[i].value); + if (ret < 0) { + zlog_error_dbus("failed to append '%s' setting for file bootstrap info provider (%d)", entries[i].key, ret); + return ret; + } + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close file bootstrap info provider container (%d)", ret); + return ret; + } + + return 0; +} + +static int +ztp1_decode_bootstrap_info_provider_settings_file(sd_bus_message *message, struct bootstrap_info_provider_file_settings *settings) +{ + const struct { + const char *key; + char **value; + } entries[] = { + { "Path", &settings->path }, + { "JsonKeyDppUri", &settings->json_key_dpp_uri }, + { "JsonKeyPublickeyHash", &settings->json_key_publickeyhash }, + { "JsonPointerObject", &settings->json_pointer_object_base }, + { "JsonPointerArray", &settings->json_pointer_array }, + }; + + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_BI_PROVIDER_FILE_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to enter file bootstrap info provider container (%d)", ret); + return ret; + } + + { + for (size_t i = 0; i < ARRAY_SIZE(entries); i++) { + ret = dbus_read_kv_pair_string_cp(message, entries[i].key, entries[i].value); + if (ret < 0) { + zlog_error_dbus("failed to append '%s' setting for file bootstrap info provider (%d)", entries[i].key, ret); + return ret; + } + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit file bootstrap info provider container (%d)", ret); + return ret; + } + + return 0; +} + +static int +ztp1_append_bootstrap_info_provider_setttings_file(sd_bus_message *message, const void *value, void *context) +{ + __unused(context); + + struct bootstrap_info_provider_file_settings *settings = (struct bootstrap_info_provider_file_settings *)value; + return ztp1_populate_bootstrap_info_provider_settings_file(message, settings); +} + +static int +ztp1_read_bootstrap_info_provider_setttings_file(sd_bus_message *message, void *value, void *context) +{ + __unused(context); + + struct bootstrap_info_provider_file_settings *settings = (struct bootstrap_info_provider_file_settings *)value; + return ztp1_decode_bootstrap_info_provider_settings_file(message, settings); +} + +#define DBUS_DATA_BI_PROVIDER_AZUREPDS_CHILDREN "(ss)(ss)(ss)(ss)(ss)(ss)" +#define DBUS_DATA_BI_PROVIDER_AZUREDPS_SIGNATURE "(" DBUS_DATA_BI_PROVIDER_AZUREPDS_CHILDREN ")" + +static int +ztp1_populate_bootstrap_info_provider_settings_azuredps(sd_bus_message *message, const struct bootstrap_info_provider_azure_dps_settings *settings) +{ + const struct { + const char *key; + const char *value; + } entries[] = { + { "ServiceEndpointUri", settings->service_endpoint_uri }, + { "AuthorityUrl", settings->authority_url }, + { "ClientId", settings->client_id }, + { "ClientSecret", settings->client_secret }, + { "ResourceUri", settings->resource_uri }, + { "ConnectionString", settings->connection_string }, + }; + + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_BI_PROVIDER_AZUREPDS_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to open azure dps bootstrap info provider container (%d)", ret); + return ret; + } + + { + for (size_t i = 0; i < ARRAY_SIZE(entries); i++) { + ret = dbus_append_kv_pair_string(message, entries[i].key, entries[i].value); + if (ret < 0) { + zlog_error_dbus("failed to append '%s' setting for azure dps bootstrap info provider (%d)", entries[i].key, ret); + return ret; + } + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close azure dps bootstrap info provider container (%d)", ret); + return ret; + } + + return 0; +} + +static int +ztp1_decode_bootstrap_info_provider_settings_azuredps(sd_bus_message *message, struct bootstrap_info_provider_azure_dps_settings *settings) +{ + const struct { + const char *key; + char **value; + } entries[] = { + { "ServiceEndpointUri", &settings->service_endpoint_uri }, + { "AuthorityUrl", &settings->authority_url }, + { "ClientId", &settings->client_id }, + { "ClientSecret", &settings->client_secret }, + { "ResourceUri", &settings->resource_uri }, + { "ConnectionString", &settings->connection_string }, + }; + + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_BI_PROVIDER_AZUREPDS_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to enter azure dps bootstrap info provider container (%d)", ret); + return ret; + } + + { + for (size_t i = 0; i < ARRAY_SIZE(entries); i++) { + ret = dbus_read_kv_pair_string_cp(message, entries[i].key, entries[i].value); + if (ret < 0) { + zlog_error_dbus("failed to read '%s' setting for azure dps bootstrap info provider (%d)", entries[i].key, ret); + return ret; + } + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit azure dps bootstrap info provider container (%d)", ret); + return ret; + } + + return 0; +} + +static int +ztp1_append_bootstrap_info_provider_setttings_azuredps(sd_bus_message *message, const void *value, void *context) +{ + __unused(context); + + struct bootstrap_info_provider_azure_dps_settings *settings = (struct bootstrap_info_provider_azure_dps_settings *)value; + return ztp1_populate_bootstrap_info_provider_settings_azuredps(message, settings); +} + +static int +ztp1_read_bootstrap_info_provider_setttings_azuredps(sd_bus_message *message, void *value, void *context) +{ + __unused(context); + + struct bootstrap_info_provider_azure_dps_settings *settings = (struct bootstrap_info_provider_azure_dps_settings *)value; + return ztp1_decode_bootstrap_info_provider_settings_azuredps(message, settings); +} + +#define DBUS_DATA_BI_PROVIDER_CHILDREN "(ss)(ss)(su)(sv)" +#define DBUS_DATA_BI_PROVIDER_SIGNATURE "(" DBUS_DATA_BI_PROVIDER_CHILDREN ")" + +static int +ztp1_populate_bootstrap_info_provider_settings(sd_bus_message *message, const struct bootstrap_info_provider_settings *settings) +{ + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_BI_PROVIDER_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to open bootstrap info provider settings struct container (%d)", ret); + return ret; + } + + { + ret = dbus_append_kv_pair_string(message, "Type", bootstrap_info_provider_type_str(settings->type)); + if (ret < 0) { + zlog_error_dbus("failed to append bootstrap info provider type (%d)", ret); + return ret; + } + + ret = dbus_append_kv_pair_string(message, "Name", settings->name); + if (ret < 0) { + zlog_error_dbus("failed to append bootstrap info provider name (%d)", ret); + return ret; + } + + ret = dbus_append_kv_pair_basic(message, SD_BUS_TYPE_UINT32, "ExpirationTime", &settings->expiration_time); + if (ret < 0) { + zlog_error_dbus("failed to append expiration time value to bootstrap info provider (%d)", ret); + return ret; + } + + switch (settings->type) { + case BOOTSTRAP_INFO_PROVIDER_FILE: { + ret = dbus_append_kv_pair_variant(message, "File", settings->file, DBUS_DATA_BI_PROVIDER_FILE_SIGNATURE, ztp1_append_bootstrap_info_provider_setttings_file, NULL); + if (ret < 0) { + zlog_error_dbus("failed to append file bootstrap info provider settings (%d)", ret); + return ret; + } + break; + } + case BOOTSTRAP_INFO_PROVIDER_AZUREDPS: { + ret = dbus_append_kv_pair_variant(message, "AzureDps", settings->dps, DBUS_DATA_BI_PROVIDER_AZUREDPS_SIGNATURE, ztp1_append_bootstrap_info_provider_setttings_azuredps, NULL); + if (ret < 0) { + zlog_error_dbus("failed to append azure dps bootstrap info provider settings (%d)", ret); + return ret; + } + break; + } + default: + zlog_error_dbus("unsupported bootstrap info provider type (%d)", ret); + return -EINVAL; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close bootstrap info provider settings struct container (%d)", ret); + return ret; + } + + return 0; +} + +static int +ztp1_decode_bootstrap_info_provider_settings_instance(sd_bus_message *message, struct bootstrap_info_provider_settings *settings) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_BI_PROVIDER_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to enter bootstrap info provider settings struct container (%d)", ret); + return ret; + } + + { + const char *type; + ret = dbus_read_kv_pair_string(message, "Type", &type); + if (ret < 0) { + zlog_error_dbus("failed to read 'Type' property from bootstrap info provider (%d)", ret); + return ret; + } + + settings->type = parse_bootstrap_info_provider_type(type); + if (settings->type == BOOTSTRAP_INFO_PROVIDER_INVALID) { + zlog_error_dbus("invalid bootstrap info provider type specified"); + return -EINVAL; + } + + ret = dbus_read_kv_pair_string_cp(message, "Name", &settings->name); + if (ret < 0) { + zlog_error_dbus("failed to read 'Name' property from bootstrap info provider (%d)", ret); + return ret; + } + + ret = dbus_read_kv_pair_basic(message, SD_BUS_TYPE_UINT32, "ExpirationTime", &settings->expiration_time); + if (ret < 0) { + zlog_error_dbus("failed to read expiration time value from bootstrap info provider (%d)", ret); + return ret; + } + + switch (settings->type) { + case BOOTSTRAP_INFO_PROVIDER_FILE: { + settings->file = calloc(1, sizeof *(settings->file)); + if (!settings->file) { + zlog_error_dbus("failed to allocate memory for file bootstrap provider settings"); + return -ENOMEM; + } + + ret = dbus_read_kv_pair_variant(message, "File", settings->file, DBUS_DATA_BI_PROVIDER_FILE_SIGNATURE, ztp1_read_bootstrap_info_provider_setttings_file, NULL); + if (ret < 0) { + zlog_error_dbus("failed to append file bootstrap info provider settings (%d)", ret); + return ret; + } + break; + } + case BOOTSTRAP_INFO_PROVIDER_AZUREDPS: { + settings->dps = calloc(1, sizeof *(settings->dps)); + if (!settings->dps) { + zlog_error_dbus("failed to allocate memory for azure dps bootstrap provider settings"); + return -ENOMEM; + } + + ret = dbus_read_kv_pair_variant(message, "AzureDps", settings->dps, DBUS_DATA_BI_PROVIDER_AZUREDPS_SIGNATURE, ztp1_read_bootstrap_info_provider_setttings_azuredps, NULL); + if (ret < 0) { + zlog_error_dbus("failed to append azure dps bootstrap info provider settings (%d)", ret); + return ret; + } + break; + } + default: + zlog_error_dbus("unsupported bootstrap info provider type (%d)", ret); + return -EINVAL; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit bootstrap info provider settings struct container (%d)", ret); + return ret; + } + + return 0; +} + +static int +ztp1_decode_bootstrap_info_provider_settings(sd_bus_message *message, struct ztp_configurator_settings *settings) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, DBUS_DATA_BI_PROVIDER_SIGNATURE); + if (ret < 0) { + zlog_error_dbus("failed to enter bootstrap info provider array container (%d)", ret); + return ret; + } + + { + for (;;) { + ret = sd_bus_message_at_end(message, 0); + if (ret < 0) { + zlog_error_dbus("failed to determine end of message reached (%d)", ret); + return ret; + } else if (ret) { + break; + } + + struct bootstrap_info_provider_settings *provider = bootstrap_info_provider_settings_alloc(); + if (!provider) { + zlog_error_dbus("failed to allocate memory for bootstrap info provider settings"); + return -ENOMEM; + } + + ret = ztp1_decode_bootstrap_info_provider_settings_instance(message, provider); + if (ret < 0) { + zlog_error_dbus("failed to decode bootstrap info provider settings (%d)", ret); + bootstrap_info_provider_settings_uninitialize(provider); + return ret; + } + + ztp_configurator_settings_add_bi_provider_settings(settings, provider); + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit network credential array container (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_ROLES_CHILDREN "sas" +#define DBUS_DATA_ROLES_SIGNATURE "(" DBUS_DATA_ROLES_CHILDREN ")" + +/** + * @brief Populates a message with the roles setting of the ztp service. + * + * The roles setting is encoded as a structure with the following d-bus signature: (sas) + * + * The first entry, s, will always be the value "Roles". + * The second entry, as, will contain the active roles of the service. The range of possible values is: + * + * "enrolleee" + * "configurator" + * + * @param server The server control structure. + * @param message The message to populate. + * @return int 0 if the message was populated, non-zero otherwise. + */ +static int +ztp1_settings_property_populate_roles(struct ztp_dbus_server *server, sd_bus_message *message) +{ + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_ROLES_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to open roles containter (%d)", ret); + return ret; + } + + { + ret = sd_bus_message_append_basic(message, SD_BUS_TYPE_STRING, "Roles"); + if (ret < 0) { + zlog_error_dbus("failed to append 'Roles' key to roles container (%d)", ret); + return ret; + } + + ret = sd_bus_message_open_container(message, SD_BUS_TYPE_ARRAY, "s"); + if (ret < 0) { + zlog_error_dbus("failed to open roles array container (%d)", ret); + return ret; + } + + { + for (size_t i = 0; i < ARRAY_SIZE(server->settings->dpp_roles_activated); i++) { + if (server->settings->dpp_roles_activated[i]) { + const char *role = dpp_device_role_str((enum dpp_device_role)i); + ret = sd_bus_message_append(message, "s", role); + if (ret < 0) { + zlog_error_dbus("failed to append role %s to roles container (%d)", role, ret); + continue; + } + } + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close roles array container (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_warning_dbus("failed to close roles container (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_NETWORK_CREDENTIAL_PSK_CHILDREN "(ss)a{sv}" +#define DBUS_DATA_NETWORK_CREDENTIAL_PSK_SIGNATURE "(" DBUS_DATA_NETWORK_CREDENTIAL_PSK_CHILDREN ")" + +static int +ztp1_decode_network_credential_psk(sd_bus_message *message, struct dpp_network_credential_psk *psk) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_NETWORK_CREDENTIAL_PSK_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to enter psk credential container (%d)", ret); + } + + { + const char *type; + ret = dbus_read_kv_pair_string(message, "Type", &type); + if (ret < 0) { + zlog_error_dbus("failed to read psk credentiak type (%d)", ret); + return ret; + } + + psk->type = parse_dpp_psk_credential_type(type); + if (psk->type == PSK_CREDENTIAL_TYPE_INVALID) { + zlog_error_dbus("invalid psk credential type specified"); + return -EINVAL; + } + + ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{sv}"); + if (ret < 0) { + zlog_error_dbus("failed to enter psk network credential dictionary array container (%d)", ret); + return ret; + } + + { + switch (psk->type) { + case PSK_CREDENTIAL_TYPE_PASSPHRASE: { + const char *passphrase; + ret = dbus_read_dict_entry_string(message, "Passphrase", &passphrase); + if (ret < 0) { + zlog_error_dbus("failed to read psk credential passphrase (%d)", ret); + return ret; + } + + ret = dpp_credential_psk_set_passphrase(psk, passphrase); + if (ret < 0) { + zlog_error_dbus("failed to set psk credential passphrase (%d)", ret); + return ret; + } + break; + } + case PSK_CREDENTIAL_TYPE_PSK: { + const char *key_hex; + ret = dbus_read_dict_entry_string(message, "Psk", &key_hex); + if (ret < 0) { + zlog_error_dbus("failed to read psk credential key (%d)", ret); + return ret; + } + + ret = dpp_credential_psk_set_key(psk, key_hex); + if (ret < 0) { + zlog_error_dbus("failed to set psk credential key (%d)", ret); + return ret; + } + break; + } + default: + break; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit psk credential dictionary container (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit psk credential psk container (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_NETWORK_CREDENTIAL_SAE_CHILDREN "(ss)" +#define DBUS_DATA_NETWORK_CREDENTIAL_SAE_SIGNATURE "(" DBUS_DATA_NETWORK_CREDENTIAL_SAE_CHILDREN ")" + +static int +ztp1_decode_network_credential_sae(sd_bus_message *message, struct dpp_network_credential_sae *sae) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_NETWORK_CREDENTIAL_SAE_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to enter sae credential container (%d)", ret); + } + + { + const char *passphrase; + ret = dbus_read_kv_pair_string(message, "Passphrase", &passphrase); + if (ret < 0) { + zlog_error_dbus("failed to read sae credential passphrase (%d)", ret); + return ret; + } + + ret = dpp_credential_sae_set_passphrase(sae, passphrase); + if (ret < 0) { + zlog_error_dbus("failed to set sae credential passphrase (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit sae credential container (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_NETWORK_CREDENTIAL_CHILDREN "(ss)(sv)" +#define DBUS_DATA_NETWORK_CREDENTIAL_SIGNATURE "(" DBUS_DATA_NETWORK_CREDENTIAL_CHILDREN ")" + +static int +dbus_read_network_credential_psk(sd_bus_message *message, void *value, void *context) +{ + __unused(context); + + struct dpp_network_credential_psk *psk = (struct dpp_network_credential_psk *)value; + return ztp1_decode_network_credential_psk(message, psk); +} + +static int +dbus_read_network_credential_sae(sd_bus_message *message, void *value, void *context) +{ + __unused(context); + + struct dpp_network_credential_sae *sae = (struct dpp_network_credential_sae *)value; + return ztp1_decode_network_credential_sae(message, sae); +} + +static int +ztp1_decode_network_credential(sd_bus_message *message, struct dpp_network_credential *credential) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_NETWORK_CREDENTIAL_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to enter network credential container (%d)", ret); + return ret; + } + + { + const char *akmstr; + ret = dbus_read_kv_pair_string(message, "Akm", &akmstr); + if (ret < 0) { + zlog_error_dbus("failed to reak akm from network credential (%d)", ret); + return ret; + } + + credential->akm = parse_dpp_akm(akmstr); + + switch (credential->akm) { + case DPP_AKM_PSK: { + ret = dbus_read_kv_pair_variant(message, "Psk", &credential->psk, DBUS_DATA_NETWORK_CREDENTIAL_PSK_SIGNATURE, dbus_read_network_credential_psk, NULL); + if (ret < 0) { + zlog_error_dbus("failed to decode psk network credential (%d)", ret); + return ret; + } + break; + } + case DPP_AKM_SAE: { + ret = dbus_read_kv_pair_variant(message, "Sae", &credential->sae, DBUS_DATA_NETWORK_CREDENTIAL_SAE_SIGNATURE, dbus_read_network_credential_sae, NULL); + if (ret < 0) { + zlog_error_dbus("failed to decode sae network credential (%d)", ret); + return ret; + } + break; + } + default: { + zlog_error_dbus("invalid dpp credential akm specified"); + return -EINVAL; + } + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close network credential container (%d)", ret); + return ret; + } + + return 0; +} + +static int +ztp1_decode_network_credentials(sd_bus_message *message, struct dpp_network *network) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, DBUS_DATA_NETWORK_CREDENTIAL_SIGNATURE); + if (ret < 0) { + zlog_error_dbus("failed to enter network credential array container (%d)", ret); + return ret; + } + + { + for (;;) { + ret = sd_bus_message_at_end(message, 0); + if (ret < 0) { + zlog_error_dbus("failed to determine end of message reached (%d)", ret); + return ret; + } else if (ret) { + break; + } + + struct dpp_network_credential *credential = dpp_network_credential_alloc(); + if (!credential) { + zlog_error_dbus("failed to allocate memory for dpp network credential"); + return -ENOMEM; + } + + ret = ztp1_decode_network_credential(message, credential); + if (ret < 0) { + zlog_error_dbus("failed to decode network credential (%d)", ret); + dpp_network_credential_uninitialize(credential); + return ret; + } + + dpp_network_add_credential(network, credential); + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit network credential array container (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_NETWORK_CHILDREN "(ss)(ss)(sa" DBUS_DATA_NETWORK_CREDENTIAL_SIGNATURE ")" +#define DBUS_DATA_NETWORK_SIGNATURE "(" DBUS_DATA_NETWORK_CHILDREN ")" + +static int +ztp1_decode_network(sd_bus_message *message, struct dpp_network *network) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_NETWORK_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to enter container for network (%d)", ret); + return ret; + } + + { + const char *value; + ret = dbus_read_kv_pair_string(message, "WifiTechnology", &value); + if (ret < 0) { + zlog_error_dbus("failed to read 'WifiTechnology' kv-pair from network (%d)", ret); + return ret; + } + + const char *ssid; + ret = dbus_read_kv_pair_string(message, "SSID", &ssid); + if (ret < 0) { + zlog_error_dbus("failed to append 'SSID' kv-pair to network (%d)", ret); + return ret; + } + + size_t ssid_length = strlen(ssid) + 1; + if (ssid_length > sizeof network->discovery.ssid) { + zlog_error_dbus("ssid too long; must be <= %lu characters", (sizeof network->discovery.ssid) - 1); + return -EINVAL; + } + + memcpy(network->discovery.ssid, ssid, ssid_length); + network->discovery.ssid_length = ssid_length; + + ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, "sa" DBUS_DATA_NETWORK_CREDENTIAL_SIGNATURE); + if (ret < 0) { + zlog_error_dbus("failed to open struct container for network credentials (%d)", ret); + return ret; + } + + { + ret = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &value); + if (ret < 0) { + zlog_error_dbus("failed to append network credentials array key 'Credentials' (%d)", ret); + return ret; + } else if (!value || strcmp(value, "Credentials") != 0) { + zlog_error_dbus("failed to find 'Credentials' property, found '%s'", value ? value : ""); + return -EINVAL; + } + + ret = ztp1_decode_network_credentials(message, network); + if (ret < 0) { + zlog_error_dbus("failed to decode network credentials (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit struct container for network credentials (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit struct container for network (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_BOOTSTRAP_INFO_CHILDREN "(su)(sa" DBUS_DATA_BI_PROVIDER_SIGNATURE ")" +#define DBUS_DATA_BOOTSTRAP_INFO_SIGNATURE "(" DBUS_DATA_BOOTSTRAP_INFO_CHILDREN ")" + +static int +ztp1_populate_bootstrap_info(sd_bus_message *message, const struct ztp_configurator_settings *settings) +{ + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_BOOTSTRAP_INFO_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to open container for bootstrap info (%d)", ret); + return ret; + } + + { + uint32_t expiration_time = settings->expiration_time; + if (expiration_time == BOOTSTRAP_INFO_PROVIDER_EXPIRATION_UNSET) { + expiration_time = 0; + } + + ret = dbus_append_kv_pair_basic(message, SD_BUS_TYPE_UINT32, "ExpirationTime", &expiration_time); + if (ret < 0) { + zlog_error_dbus("failed to append 'ExpirationTime' to configurator settings container (%d)", ret); + return ret; + } + + ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, "sa" DBUS_DATA_BI_PROVIDER_SIGNATURE); + if (ret < 0) { + zlog_error_dbus("failed to open bootstrap info provider array struct (%d)", ret); + return ret; + } + + { + ret = sd_bus_message_append_basic(message, SD_BUS_TYPE_STRING, "Providers"); + if (ret < 0) { + zlog_error_dbus("failed to append bootstrap info provider array (%d)", ret); + return ret; + } + + ret = sd_bus_message_open_container(message, SD_BUS_TYPE_ARRAY, DBUS_DATA_BI_PROVIDER_SIGNATURE); + if (ret < 0) { + zlog_error_dbus("failed to open bootstrap info provider array container for settings (%d)", ret); + return ret; + } + + { + struct bootstrap_info_provider_settings *provider_settings; + list_for_each_entry (provider_settings, &settings->provider_settings, list) { + ret = ztp1_populate_bootstrap_info_provider_settings(message, provider_settings); + if (ret < 0) { + zlog_error_dbus("failed to populate bootstrap info provider settings with name %s (%d)", provider_settings->name, ret); + return ret; + } + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close bootstrap info array container for settings (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close bootstrap info array struct (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close struct container for bootstrap info (%d)", ret); + return ret; + } + + return 0; +} + +static int +dbus_append_bootstrap_info(sd_bus_message *message, const void *value, void *value_context) +{ + __unused(value_context); + + const struct ztp_configurator_settings *settings = (const struct ztp_configurator_settings *)value; + return ztp1_populate_bootstrap_info(message, settings); +} + +static int +ztp1_decode_bootstrap_info(sd_bus_message *message, struct ztp_configurator_settings *settings) +{ + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_BOOTSTRAP_INFO_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to enter bootstrap info container (%d)", ret); + return ret; + } + + { + ret = dbus_read_kv_pair_basic(message, SD_BUS_TYPE_UINT32, "ExpirationTime", &settings->expiration_time); + if (ret < 0) { + zlog_error_dbus("failed to read 'ExpirationTime' property (%d)", ret); + return ret; + } + + ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, "sa" DBUS_DATA_BI_PROVIDER_SIGNATURE); + if (ret < 0) { + zlog_error_dbus("failed to enter bootstrap info array struct container (%d)", ret); + return ret; + } + + { + const char *name; + ret = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &name); + if (ret < 0) { + zlog_error_dbus("failed to read 'Providers' property (%d)", ret); + return ret; + } else if (!name || strcmp(name, "Providers") != 0) { + zlog_error_dbus("failed to find 'Providers' property, found '%s'", name ? name : ""); + return -EINVAL; + } + + ret = ztp1_decode_bootstrap_info_provider_settings(message, settings); + if (ret < 0) { + zlog_error_dbus("failed to decode bootstrap info provider settings (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit bootstrap info array struct container (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to exit bootstrap info container (%d)", ret); + return ret; + } + + return 0; +} + +static int +dbus_read_bootstrap_info(sd_bus_message *message, void *value, void *context) +{ + __unused(context); + + struct ztp_configurator_settings *settings = (struct ztp_configurator_settings *)value; + return ztp1_decode_bootstrap_info(message, settings); +} + +#define DBUS_DATA_CONFIGURATOR_CHILDREN "(s" DBUS_DATA_NETWORK_SIGNATURE ")(s" DBUS_DATA_BOOTSTRAP_INFO_SIGNATURE ")" +#define DBUS_DATA_CONFIGURATOR_CHILDREN_R "(ss)(s" DBUS_DATA_BOOTSTRAP_INFO_SIGNATURE ")" +#define DBUS_DATA_CONFIGURATOR_SIGNATURE "(" DBUS_DATA_CONFIGURATOR_CHILDREN ")" +#define DBUS_DATA_CONFIGURATOR_SIGNATURE_R "(" DBUS_DATA_CONFIGURATOR_CHILDREN_R ")" + +/** + * @brief + * + * @param message + * @param server + * @return int + */ +static int +ztp1_rolesettings_property_populate_configurator_settings(sd_bus_message *message, struct ztp_dbus_server *server) +{ + struct ztp_configurator_settings *settings = &server->settings_configurator->configurator; + + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_CONFIGURATOR_CHILDREN_R); + if (ret < 0) { + zlog_error_dbus("failed to open configurator settings container (%d)", ret); + return ret; + } + + { + ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, "ss"); + if (ret < 0) { + zlog_error_dbus("failed to open default network container for settings (%d)", ret); + return ret; + } + + { + const char *network_path = server->network_configuration_default + ? server->network_configuration_default->path + : ""; + + ret = sd_bus_message_append_basic(message, SD_BUS_TYPE_STRING, "DefaultNetwork"); + if (ret < 0) { + zlog_error_dbus("failed to append 'DefaultNetwork' key to default network container (%d)", ret); + return ret; + } + + ret = sd_bus_message_append_basic(message, SD_BUS_TYPE_STRING, network_path); + if (ret < 0) { + zlog_error_dbus("failed to append default network settings to container (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close default network container for settings (%d)", ret); + return ret; + } + + ret = dbus_append_kv_pair(message, "BootstrapInfoProviderSettings", settings, DBUS_DATA_BOOTSTRAP_INFO_SIGNATURE, dbus_append_bootstrap_info, NULL); + if (ret < 0) { + zlog_error_dbus("failed to append bootstrap info provider settings (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_warning_dbus("failed to close configurator settings container (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief + * + * @param server + * @param message + * @return int + */ +static int +ztp1_decode_configurator_settings(sd_bus_message *message, struct ztp_configurator_settings *settings) +{ + const char *name; + + int ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_CONFIGURATOR_CHILDREN); + if (ret < 0) { + zlog_error_dbus("failed to open configurator settings container (%d)", ret); + return ret; + } + + { + ret = sd_bus_message_enter_container(message, SD_BUS_TYPE_STRUCT, "s" DBUS_DATA_NETWORK_SIGNATURE); + if (ret < 0) { + zlog_error_dbus("failed to open default network container for settings (%d)", ret); + return ret; + } + + { + ret = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &name); + if (ret < 0) { + zlog_error_dbus("failed to read 'DefaultNetwork' property (%d)", ret); + return ret; + } else if (!name || strcmp(name, "DefaultNetwork") != 0) { + zlog_error_dbus("failed to find 'DefaultNetwork' property, found '%s'", name ? name : ""); + return -EINVAL; + } + + settings->network_config_default = dpp_network_alloc(); + if (!settings->network_config_default) { + zlog_error_dbus("failed to allocate dpp network for decode (%d)", ret); + return -ENOMEM; + } + + ret = ztp1_decode_network(message, settings->network_config_default); + if (ret < 0) { + zlog_error_dbus("failed to decode default network in configurator settings (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_error_dbus("failed to close default network container for settings (%d)", ret); + return ret; + } + + ret = dbus_read_kv_pair(message, "BootstrapInfoProviderSettings", settings, DBUS_DATA_BOOTSTRAP_INFO_SIGNATURE, dbus_read_bootstrap_info, NULL); + if (ret < 0) { + zlog_error_dbus("failed to decode bootstrap info provider settings (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_exit_container(message); + if (ret < 0) { + zlog_warning_dbus("failed to close configurator settings container (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_ROLESETTINGS_CHILDREN "s(s" DBUS_DATA_CONFIGURATOR_SIGNATURE ")" +#define DBUS_DATA_ROLESETTINGS_CHILDREN_R "s(s" DBUS_DATA_CONFIGURATOR_SIGNATURE_R ")" +#define DBUS_DATA_ROLESETTINGS_SIGNATURE "(" DBUS_DATA_ROLESETTINGS_CHILDREN ")" +#define DBUS_DATA_ROLESETTINGS_SIGNATURE_R "(" DBUS_DATA_ROLESETTINGS_CHILDREN_R ")" + +/** + * @brief Populates a message with the role settings of the ztp service. + * + * The roles settings setting is encoded as a structure with the following d-bus signature: (sa{sv}) + * + * The first entry, s, will always be the value "RoleSettings". + * The second entry, a{sv}, is an array of dictionary entries. + * + * @param server The server control structure. + * @param message The message to populate. + * @return int 0 if the message was populated, non-zero otherwise + */ +static int +ztp1_settings_property_populate_rolesettings(struct ztp_dbus_server *server, sd_bus_message *message) +{ + int ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, DBUS_DATA_ROLESETTINGS_CHILDREN_R); + if (ret < 0) { + zlog_error_dbus("failed to open role-setting container (%d)", ret); + return ret; + } + + { + ret = sd_bus_message_append_basic(message, SD_BUS_TYPE_STRING, "RoleSettings"); + if (ret < 0) { + zlog_error_dbus("failed to append 'RoleSettings' key to role-setting container (%d)", ret); + return ret; + } + + ret = sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, "s" DBUS_DATA_CONFIGURATOR_SIGNATURE_R); + if (ret < 0) { + zlog_error_dbus("failed to open role-setting container (%d)", ret); + return ret; + } + + { + ret = sd_bus_message_append_basic(message, SD_BUS_TYPE_STRING, "Configurator"); + if (ret < 0) { + zlog_error_dbus("failed to append 'RoleSettings' key to role-setting container (%d)", ret); + return ret; + } + + ret = ztp1_rolesettings_property_populate_configurator_settings(message, server); + if (ret < 0) { + zlog_error_dbus("failed to populate configurator settings to role-settings (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_warning_dbus("failed to close role-setting container (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(message); + if (ret < 0) { + zlog_warning_dbus("failed to close role-setting container (%d)", ret); + return ret; + } + + return 0; +} + +#define DBUS_DATA_SETTINGS_CHILDREN DBUS_DATA_ROLES_SIGNATURE DBUS_DATA_ROLESETTINGS_SIGNATURE +#define DBUS_DATA_SETTINGS_CHILDREN_R DBUS_DATA_ROLES_SIGNATURE DBUS_DATA_ROLESETTINGS_SIGNATURE_R +#define DBUS_DATA_SETTINGS_SIGNATURE "(" DBUS_DATA_SETTINGS_CHILDREN ")" +#define DBUS_DATA_SETTINGS_SIGNATURE_R "(" DBUS_DATA_SETTINGS_CHILDREN_R ")" + +/* + * @brief Retrieves the service settings. + * + * @param bus The bus owning this object. + * @param path The d-bus path the property is being retrieved from. + * @param interface The d-bus interface the property is being retrieved from. Must be "com.microsoft.ztp1". + * @param property The name of the property. Must be "Settings". + * @param reply The reply message. + * @param userdata The user context. Must be of type struct ztp_dbus_server. + * @param error The return value error. + * @return int The return value. 0 if succeeded, non-zero otherwise. + */ +static int +ztp1_get_settings(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *error) +{ + __unused(bus); + __unused(path); + __unused(interface); + __unused(property); + + struct ztp_dbus_server *server = (struct ztp_dbus_server *)userdata; + + int ret = sd_bus_message_open_container(reply, SD_BUS_TYPE_STRUCT, DBUS_DATA_SETTINGS_CHILDREN_R); + if (ret < 0) { + zlog_error_dbus("failed to open top-level containter in 'Settings' property (%d)", ret); + return ret; + } + + { + ret = ztp1_settings_property_populate_roles(server, reply); + if (ret < 0) { + zlog_error_dbus("failed to populate 'Settings' property with device roles (%d)", ret); + sd_bus_error_set_errno(error, ret); + return ret; + } + + ret = ztp1_settings_property_populate_rolesettings(server, reply); + if (ret < 0) { + zlog_error_dbus("failed to populate 'Settings' property with role settings (%d)", ret); + return ret; + } + } + + ret = sd_bus_message_close_container(reply); + if (ret < 0) { + zlog_warning_dbus("failed to close top-level container for 'Settings' property (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief Method to set a device role disposition. + * + * @param message The request message. + * @param userdata The user content. Must be of type struct ztp_dbus_configurator. + * @param error The error proxy to use. + * @return int 0 if the method was successfully executed, non-zero otherwise. + */ +static int +ztp1_set_role_disposition(sd_bus_message *message, void *userdata, sd_bus_error *error) +{ + struct ztp_dbus_server *server = (struct ztp_dbus_server *)userdata; + + const char *rolestr; + int ret = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &rolestr); + if (ret < 0) { + zlog_error_dbus("failed to read 'role' argument from SetRoleDisposition method (%d)", ret); + ret = -EINVAL; + sd_bus_error_set_errno(error, ret); + return ret; + } + + const char *action; + ret = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &action); + if (ret < 0) { + zlog_error_dbus("failed to read 'action' argument from SetRoleDisposition method (%d)", ret); + ret = -EINVAL; + sd_bus_error_set_errno(error, ret); + return ret; + } + + bool activate = strcmp(action, "activate") == 0; + enum dpp_device_role role = dpp_device_role_parse(rolestr); + if (role == DPP_DEVICE_ROLE_UNKNOWN) { + zlog_error_dbus("invalid device role %s specified", rolestr); + ret = -EINVAL; + sd_bus_error_set_errno(error, ret); + return ret; + } + + ret = ztp_settings_set_device_role_disposition(server->settings, role, activate); + if (ret < 0) { + zlog_error_dbus("failed to set role %s disposition to '%s' (%d)", rolestr, action, ret); + } else if (ret > 0) { + sd_bus_emit_properties_changed(server->bus, ZTP_DBUS_SERVER_PATH, ZTP_DBUS_SERVER_INTERFACE, "Roles", NULL); + } + + return sd_bus_reply_method_return(message, "b", ret >= 0); +} + +/** + * @brief Method to set a configurator settings. + * + * @param message The request message. + * @param userdata The user content. Must be of type struct ztp_dbus_server. + * @param error The error proxy to use. + * @return int 0 if the method was successfully executed, non-zero otherwise. + */ +static int +ztp1_set_configurator_settings(sd_bus_message *message, void *userdata, sd_bus_error *error) +{ + __unused(error); + + bool succeeded = false; + struct ztp_dbus_server *server = (struct ztp_dbus_server *)userdata; + + struct ztp_configurator_settings settings; + ztp_configurator_settings_initialize(&settings); + + int ret = ztp1_decode_configurator_settings(message, &settings); + if (ret < 0) { + zlog_error_dbus("failed to decode configurator settings (%d)", ret); + goto out; + } + + ret = ztp_configurator_settings_persist(server->settings_configurator->path, &settings); + if (ret < 0) { + zlog_error_dbus("failed to persist updated configurator settings (%d)", ret); + goto out; + } + + ztp_settings_signal_changed(server->settings, ZTP_SETTING_CHANGED_ITEM_CONFIGURATOR_SETTINGS); + sd_bus_emit_properties_changed(server->bus, ZTP_DBUS_SERVER_PATH, ZTP_DBUS_SERVER_INTERFACE, "Settings", "Configurators", NULL); + succeeded = true; +out: + ztp_configurator_settings_uninitialize(&settings); + return sd_bus_reply_method_return(message, "b", succeeded); +} + +/** + * @brief Virtual table describing methods, properties, and signals of the ZTP + * d-bus service. + */ +static const sd_bus_vtable vtable_com_microsoft_ztp1[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("Configurators", "ao", ztp1_get_configurators, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("Roles", "as", ztp1_get_dpp_roles, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("Settings", DBUS_DATA_SETTINGS_SIGNATURE_R, ztp1_get_settings, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_METHOD("SetRoleDisposition", "ss", "b", ztp1_set_role_disposition, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("SetConfiguratorSettings", DBUS_DATA_CONFIGURATOR_SIGNATURE, "b", ztp1_set_configurator_settings, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END, +}; + +/** + * @brief Updates the current settings instance. + * + * @param server The server control structure to update. + * @param settings The new settings instance to use. + */ +void +ztp_dbus_server_update_settings(struct ztp_dbus_server *server, struct ztp_settings *settings) +{ + if (server->network_configuration_default) { + ztp_dbus_network_configuration_manager_unregister(&server->network_configuration_default); + } + + server->settings = settings; + + struct ztp_device_role_settings_entry *entry; + list_for_each_entry (entry, &settings->role_settings, list) { + if (entry->settings.role == DPP_DEVICE_ROLE_CONFIGURATOR) { + server->settings_configurator = &entry->settings; + break; + } + } + + if (server->settings_configurator) { + struct dpp_network *network = server->settings_configurator->configurator.network_config_default; + if (network) { + int ret = ztp_dbus_network_configuration_manager_register(server->network_configuration_manager, network, &server->network_configuration_default); + if (ret < 0) { + zlog_warning("failed to register default network with dbus network configuration manager (%d)", ret); + } + } + } +} + +/** + * @brief Initializes a d-bus server on the specified bus. + * + * This will initialize all ztpd d-bus related server objects and services and + * register them on the specified bus. The dbus-daemon proxy is used as the + * messgage broker, so must be present on the system. + + * This does not handle processing of messages destined for these objects and + * services. Message processing must be done externally in a message loop or + * through the use of higher-level d-bus message processing. Messages for these + * objctes and services can be processed by calling sd_bus_process(). It is + * expected that the ztpd main event loop does this. + * + * Appropriate access control and authorization is not enforced. This is + * expected to be performned by the dbus-daemon, which picks up settings + * from distribution specific configuration files (eg. /etc/dbus-1/system.d). + + * @param server The server structure to initialize. + * @param settings The settings associated with the service. + * @param network_configuration_manager A network confiuration manager + * instance. This instance must be valid for the lifetime of the d-bus server. + * @param path The dbus path to register the service. + * @return int 0 if the d-bus server was successfully initialized, non-zero + * otherwise. + */ +int +ztp_dbus_server_initialize(struct ztp_dbus_server *server, struct ztp_settings *settings, struct ztp_dbus_network_configuration_manager *network_configuration_manager, const char *path) +{ + sd_bus *bus; + sd_bus_slot *slot_vtable = NULL; + + int ret = sd_bus_default_system(&bus); + if (ret < 0) { + zlog_error_dbus("failed to open system bus (%d)", ret); + return ret; + } + + ret = sd_bus_add_object_vtable(bus, + &slot_vtable, + path, + ZTP_DBUS_SERVER_INTERFACE, + vtable_com_microsoft_ztp1, + server); + if (ret < 0) { + zlog_error_dbus("failed to install '%s' object vtable (%d)", ZTP_DBUS_SERVER_INTERFACE, ret); + return ret; + } + + ret = sd_bus_request_name(bus, ZTP_DBUS_SERVER_INTERFACE, 0); + if (ret < 0) { + zlog_error_dbus("failed to acquire well-known service name '%s' (%d)", ZTP_DBUS_SERVER_INTERFACE, ret); + return ret; + } + + explicit_bzero(server, sizeof *server); + server->bus = bus; + server->slot_vtable = slot_vtable; + server->network_configuration_manager = network_configuration_manager; + INIT_LIST_HEAD(&server->configurators); + + ztp_dbus_server_update_settings(server, settings); + + return ret; +} + +/** + * @brief Uninitializes a previously initialized d-bus server. + * + * @param server The server control structure that was previously filled in by + * ztp_dbus_server_initialize. + */ +void +ztp_dbus_server_uninitialize(struct ztp_dbus_server *server) +{ + if (!server) + return; + + if (server->configurators.next != NULL) { + struct ztp_dbus_configurator *entry; + struct ztp_dbus_configurator *tmp; + list_for_each_entry_safe (entry, tmp, &server->configurators, list) { + list_del(&entry->list); + free(entry); + } + } + + ztp_dbus_network_configuration_manager_destroy(&server->network_configuration_manager); + + if (server->slot_vtable) { + sd_bus_slot_unref(server->slot_vtable); + server->slot_vtable = NULL; + } + + if (server->bus) { + sd_bus_unref(server->bus); + server->bus = NULL; + } +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif //__clang_ diff --git a/src/dbus/ztp_dbus_server.h b/src/dbus/ztp_dbus_server.h new file mode 100644 index 0000000..f0b5d50 --- /dev/null +++ b/src/dbus/ztp_dbus_server.h @@ -0,0 +1,97 @@ + +#ifndef __ZTP_DBUS_SERVER_H__ +#define __ZTP_DBUS_SERVER_H__ + +#include + +#include +#include + +struct ztp_settings; +struct ztp_dbus_network_manager; + +/** + * @brief Maximum length of a complete d-bus path, as used by ztpd. + */ +#define ZTP_DBUS_MAX_PATH 512 + +/** + * @brief Macros to help construct child paths and interfaces. + */ +#define ZTP_DBUS_CHILD_PATH(b, p) b "/" p +#define ZTP_DBUS_CHILD_INTERFACE(b, p) b "." p + +/** + * @brief Well-known ztpd d-bus server path and interfaces. The + * server interface name is also used as the well known service name. + */ +#define ZTP_DBUS_SERVER_PATH "/com/microsoft/ztp1" +#define ZTP_DBUS_SERVER_INTERFACE "com.microsoft.ztp1" + +/** + * @brief Macros to help construct ztp service child paths and interfaces. + */ +#define ZTP_DBUS_SERVER_CHILD_PATH(p) ZTP_DBUS_CHILD_PATH(ZTP_DBUS_SERVER_PATH, p) +#define ZTP_DBUS_SERVER_CHILD_INTERFACE(i) ZTP_DBUS_CHILD_INTERFACE(ZTP_DBUS_SERVER_INTERFACE, i) + +/** + * @brief d-bus server control structure. + */ +struct ztp_dbus_server { + sd_bus *bus; + sd_bus_slot *slot_vtable; + struct list_head configurators; + struct ztp_settings *settings; + struct ztp_device_role_settings *settings_configurator; + struct ztp_dbus_network_configuration_manager *network_configuration_manager; + struct ztp_dbus_network_configuration *network_configuration_default; + uint32_t configurator_id_next; +}; + +/** + * @brief Initializes a d-bus server on the specified bus. + * + * This will initialize all ztpd d-bus related server objects and services and + * register them on the specified bus. The dbus-daemon proxy is used as the + * messgage broker, so must be present on the system. + + * This does not handle processing of messages destined for these objects and + * services. Message processing must be done externally in a message loop or + * through the use of higher-level d-bus message processing. Messages for these + * objctes and services can be processed by calling sd_bus_process(). It is + * expected that the ztpd main event loop does this. + * + * Appropriate access control and authorization is not enforced. This is + * expected to be performned by the dbus-daemon, which picks up settings + * from distribution specific configuration files (eg. /etc/dbus-1/system.d). + + * @param server The server structure to initialize. + * @param settings The settings associated with the service. + * @param network_configuration_manager A network confiuration manager + * instance. This instance must be valid for the lifetime of the d-bus server. + * @param path The dbus path to register the service. + * @return int 0 if the d-bus server was successfully initialized, non-zero + * otherwise. + */ +int +ztp_dbus_server_initialize(struct ztp_dbus_server *server, struct ztp_settings *settings, struct ztp_dbus_network_configuration_manager *network_configuration_manager, const char *path); + +/** + * @brief Uninitializes a previously initialized d-bus server. + * + * @param server The server control structure that was previously filled in by + * ztp_dbus_server_initialize. + */ +void +ztp_dbus_server_uninitialize(struct ztp_dbus_server *server); + +/** + * @brief Updates the current settings instance. + * + * @param server The server control structure to update. + * @param settings The new settings instance to use. + */ +void +ztp_dbus_server_update_settings(struct ztp_dbus_server *server, struct ztp_settings *settings); + +#endif //__ZTP_DBUS_SERVER_H__ diff --git a/src/systemd/CMakeLists.txt b/src/systemd/CMakeLists.txt new file mode 100644 index 0000000..1ec897c --- /dev/null +++ b/src/systemd/CMakeLists.txt @@ -0,0 +1,22 @@ + +project(ztpsystemd) + +add_library(ztpsystemd + STATIC + "" +) + +target_sources(ztpsystemd + PRIVATE + ztp_systemd.c +) + +target_include_directories(ztpsystemd + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../utils +) + +target_link_libraries(ztpsystemd + ${LIBSYSTEMD_TARGET} +) diff --git a/src/systemd/ztp_systemd.c b/src/systemd/ztp_systemd.c new file mode 100644 index 0000000..fffd475 --- /dev/null +++ b/src/systemd/ztp_systemd.c @@ -0,0 +1,130 @@ + +#include + +#include + +#include "ztp_log.h" +#include "ztp_systemd.h" + +/** + * @brief Wrapper function for the 'StartUnit' command on the Manager interface + * for systemd. + * + * This function uses the "replace" mode which will replace any existing calls + * to start the unit with a new one. Note this only invokes the call and + * doesn't enforce that the service stays up. For example, if the service + * immediately crashes or otherwise exits and is not configured to restart, + * this function will not make any attempts to start it again. The service + * should be configured appropriately to stay up and have fault-tolerant logic + * or systemd options built in. + + * @param bus Pointer to a d-bus bus connection to send the call on. + * @param unit The name of the unit to start. + * @return int 0 if the unit was started, 0 otherwise. + */ +int +ztp_systemd_unit_start(sd_bus *bus, const char *unit) +{ + sd_bus_message *msg; + sd_bus_error error = SD_BUS_ERROR_NULL; + int ret = sd_bus_call_method(bus, + SYSTEMD_SERVICE, + SYSTEMD_SERVICE_PATH, + SYSTEMD_INTERFACE_MANAGER, + "StartUnit", + &error, + &msg, + "ss", + unit, + "replace"); + if (ret < 0) { + zlog_error("failed to call StartUnit for unit %s(%d)", unit, ret); + return ret; + } + + sd_bus_message_unref(msg); + return 0; +} + +/** + * @brief Wrapper function for the 'StopUnit' command on the Manager interface + * for systemd. this function uses the "replace" mode which will replace any + * existing calls to stop the unit with a new one. + + * @param bus Pointer to a d-bus bus connection to send the call on. + * @param unit The name of the unit to stop. + * @return int 0 if the unit was stopped, 0 otherwise. + */ +int +ztp_systemd_unit_stop(sd_bus *bus, const char *unit) +{ + sd_bus_message *msg; + sd_bus_error error = SD_BUS_ERROR_NULL; + int ret = sd_bus_call_method(bus, + SYSTEMD_SERVICE, + SYSTEMD_SERVICE_PATH, + SYSTEMD_INTERFACE_MANAGER, + "StopUnit", + &error, + &msg, + "ss", + unit, + "replace"); + if (ret < 0) { + zlog_error("failed to call StopUnit for unit %s (%d)", unit, ret); + return ret; + } + + sd_bus_message_unref(msg); + return 0; +} + +/** + * @brief Retrieves the active state of the specified activation unit. + * + * @param bus Pointer to a d-bus bus connection to send the call on. + * @param unit The name of the unit to query. + * @param active_state Output pointer to hold the active state string. Must be free()'ed. + * @return int 0 if the active state was determined, non-zero otherwise. + */ +int +ztpd_systemd_unit_get_activestate(sd_bus *bus, const char *unit, char **active_state) +{ + sd_bus_message *msg; + sd_bus_error error = SD_BUS_ERROR_NULL; + int ret = sd_bus_call_method(bus, + SYSTEMD_SERVICE, + SYSTEMD_SERVICE_PATH, + SYSTEMD_INTERFACE_MANAGER, + "LoadUnit", + &error, + &msg, + "s", + unit); + if (ret < 0) { + zlog_error("failed to determine d-bus path for unit %s (%d)", unit, ret); + return ret; + } + + const char *unit_path = NULL; + ret = sd_bus_message_read_basic(msg, SD_BUS_TYPE_OBJECT_PATH, &unit_path); + sd_bus_message_unref(msg); + if (ret < 0) { + zlog_error("failed to read unit path from message response (%d)", ret); + return ret; + } + + ret = sd_bus_get_property_string(bus, + SYSTEMD_SERVICE, + unit_path, + SYSTEMD_INTERFACE_UNIT, + "ActiveState", + &error, + active_state); + if (ret < 0) { + zlog_error("failed to retrieve ActiveState property from unit %s (%d)", unit, ret); + return ret; + } + + return 0; +} diff --git a/src/systemd/ztp_systemd.h b/src/systemd/ztp_systemd.h new file mode 100644 index 0000000..1dea6ab --- /dev/null +++ b/src/systemd/ztp_systemd.h @@ -0,0 +1,57 @@ + +#ifndef __ZTP_SYSTEMD_H__ +#define __ZTP_SYSTEMD_H__ + +#include + +#include + +#define SYSTEMD_SERVICE_PATH "/org/freedesktop/systemd1" +#define SYSTEMD_SERVICE "org.freedesktop.systemd1" + +#define SYSTEMD_INTERFACE_MANAGER SYSTEMD_SERVICE ".Manager" +#define SYSTEMD_INTERFACE_UNIT SYSTEMD_SERVICE ".Unit" + +/** + * @brief Wrapper function for the 'StartUnit' command on the Manager interface + * for systemd. + * + * This function uses the "replace" mode which will replace any existing calls + * to start the unit with a new one. Note this only invokes the call and + * doesn't enforce that the service stays up. For example, if the service + * immediately crashes or otherwise exits and is not configured to restart, + * this function will not make any attempts to start it again. The service + * should be configured appropriately to stay up and have fault-tolerant logic + * or systemd options built in. + + * @param bus Pointer to a d-bus bus connection to send the call on. + * @param unit The name of the unit to start. + * @return int 0 if the unit was started, 0 otherwise. + */ +int +ztp_systemd_unit_start(sd_bus *bus, const char *unitname); + +/** + * @brief Wrapper function for the 'StopUnit' command on the Manager interface + * for systemd. this function uses the "replace" mode which will replace any + * existing calls to stop the unit with a new one. + + * @param bus Pointer to a d-bus bus connection to send the call on. + * @param unit The name of the unit to stop. + * @return int 0 if the unit was stopped, 0 otherwise. + */ +int +ztp_systemd_unit_stop(sd_bus *bus, const char *unitname); + +/** + * @brief Retrieves the active state of the specified activation unit. + * + * @param bus Pointer to a d-bus bus connection to send the call on. + * @param unit The name of the unit to query. + * @param active_state Output pointer to hold the active state string. Must be free()'ed. + * @return int 0 if the active state was determined, non-zero otherwise. + */ +int +ztpd_systemd_unit_get_activestate(sd_bus *bus, const char *unit, char **active_state); + +#endif //__ZTP_SYSTEMD_H__ diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt new file mode 100644 index 0000000..dbb9e99 --- /dev/null +++ b/src/utils/CMakeLists.txt @@ -0,0 +1,22 @@ + +project(ztputils) + +add_library(ztputils + STATIC + "" +) + +target_sources(ztputils + PRIVATE + file_utils.c + json_parse.c + led_ctrl.c + string_utils.c + time_utils.c + event_loop.c +) + +target_include_directories(ztputils + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/src/utils/event_loop.c b/src/utils/event_loop.c new file mode 100644 index 0000000..31f9ed0 --- /dev/null +++ b/src/utils/event_loop.c @@ -0,0 +1,671 @@ + +#include +#include +#include +#include +#include + +#include "event_loop.h" +#include "time_utils.h" +#include "ztp_log.h" + +/** + * @brief Converts a ztpd scheduled task type to a string. + * + * @param type The type of convert. + * @return const char* A string representation of the task type. + */ +static const char * +scheduled_task_type_str(enum scheduled_task_type type) +{ + switch (type) { + case TASK_ONESHOT: + return "oneshot"; + case TASK_PERIODIC: + return "periodic"; + default: + return "??"; + } +} + +/** + * @brief Determines if the specified task timeout has expired, compared to a reference time. + * + * @param task The task to check. + * @param reference The time to check against. + * @return true If the task has expired: expiry time is earlier than reference time. + * @return false If the task has not expired: expiry time is later than reference time. + */ +static bool +scheduled_task_is_expired(struct scheduled_task *task, struct timespec *reference) +{ + return timespec_time_is_earlier(&task->expiry, reference); +} + +/** + * @brief Allocates and initializes a new scheduled task. + * + * @return struct scheduled_task* + */ +static struct scheduled_task * +scheduled_task_alloc() +{ + struct scheduled_task *task; + task = malloc(sizeof *task); + if (!task) + return NULL; + + INIT_LIST_HEAD(&task->list); + + return task; +} + +/** + * @brief Removes and deletes a task from the event loop. + * + * @param task The task to delete/remove. + */ +static void +scheduled_task_delete(struct scheduled_task *task) +{ + if (!task) + return; + + list_del(&task->list); + free(task); +} + +/** + * @brief Calculates the timeout (remaining time) in milliseconds compared to a reference time. + * + * This calculates the number of milliseconds between the reference time and + * the expiry time. In case the reference time is earlier than the expiry time + * (eg. scheduled task is expired), 0 is returned as the timeout value, + * indicating the scheduled task should be processed immediately. + + * @param task The task to calculate the timeout for. + * @param reference The reference time to compare against. + * @return int + */ +static int +scheduled_task_get_timeout(struct scheduled_task *task, struct timespec *reference) +{ + if (scheduled_task_is_expired(task, reference)) + return 0; + + // Task is not expired, so task->expiry > reference; calculate remaining timeout. + struct timespec remaining = task->expiry; + remaining.tv_sec -= reference->tv_sec; + remaining.tv_nsec -= reference->tv_nsec; + + // Convert relative time to milliseconds. + time_t timeout = 0; + timeout += remaining.tv_sec * MSEC_PER_SEC; + timeout += remaining.tv_nsec / NSEC_PER_MSEC; + + return (int)timeout; +} + +/** + * @brief Returns the next (in time) scheduled task. + * + * @param loop The event loop control structure. + * @return struct scheduled_task The task structure of the next expiring + * scheduled task. + */ +static struct scheduled_task * +scheduled_task_next(struct event_loop *loop) +{ + return list_first_entry_or_null(&loop->scheduled_tasks, struct scheduled_task, list); +} + +/** + * @brief Initializes an event dispatcher for use, allocating its initial descriptor table. + * + * @param num_events The number of events the dispatcher should support. + * @return int 0 if the dispatcher was successfully initialized, non-zero otherwise. + */ +static int +event_dispatch_initialize(struct event_dispatch *dispatch, size_t num_events) +{ + struct event *events = calloc(num_events, sizeof *events); + if (!events) + return -ENOMEM; + + for (size_t i = 0; i < num_events; i++) + events[i].fd = -1; + + dispatch->events = events; + dispatch->num_events = num_events; + + return 0; +} + +/** + * @brief Uninitializes a event dispatcher for use. + * + * @param dispatch The event dispatcher to uninitialize. + */ +static void +event_dispatch_uninitialize(struct event_dispatch *dispatch) +{ + if (dispatch->events) { + free(dispatch->events); + dispatch->events = NULL; + } + + dispatch->num_events = 0; +} + +/** + * @brief Find an unused slot in the dispatch table. + * + * Note that this only returns the index of the free slot. It does not mark the + * slot as being used; this is the responsibility of the caller (if desired). + * + * @param dispatch The dispatch table to find the slot from. + * @return ssize_t The index of the slot, if one is available. Otherwise -1 is + * returned indicating that no free slots are available. + */ +ssize_t +event_dispatch_find_slot(struct event_dispatch *dispatch) +{ + for (size_t i = 0; i < dispatch->num_events; i++) { + if (dispatch->events[i].fd == -1) + return (ssize_t)i; + } + + return -1; +} + +/** + * @brief Maximum number of read events currently supported. + */ +#define EPOLL_MAX_READ_EVENTS 8 + +/** + * @brief Initialize parts of the event loop that are related to epoll. + * + * @param loop The event loop instance. + * @return int 0 if the epoll event loop was successfull configured, non-zero otherwise. + */ +static int +event_loop_epoll_initialize(struct event_loop *loop) +{ + int ret; + int epoll_fd = epoll_create1(0); + if (epoll_fd < 0) { + ret = errno; + zlog_error("failed to create epoll instance (%d)", ret); + return ret; + } + + size_t events_max = EPOLL_MAX_READ_EVENTS; + struct epoll_event *events = malloc(sizeof *events * events_max); + if (!events) { + zlog_error("failed to allocate epoll events array"); + ret = -ENOMEM; + goto fail; + } + + ret = event_dispatch_initialize(&loop->dispatch, events_max); + if (ret < 0) { + zlog_error("failed to initialize event dispatcher (%d)", ret); + goto fail; + } + + loop->events = events; + loop->events_max = events_max; + loop->epoll_fd = epoll_fd; + loop->terminate_pending = false; + + ret = 0; +out: + return ret; +fail: + if (epoll_fd != -1) + close(epoll_fd); + if (events) + free(events); + goto out; +} + +/** + * @brief Uninitializes epoll related parts of the event loop. The event loop + * must not be in the running state. + * + * @param loop The event loop instancece + */ +static void +event_loop_epoll_uninitialize(struct event_loop *loop) +{ + if (loop->epoll_fd != -1) { + close(loop->epoll_fd); + loop->epoll_fd = -1; + } + + if (loop->events) { + free(loop->events); + loop->events = NULL; + } + + event_dispatch_uninitialize(&loop->dispatch); +} + +/** + * @brief Initialize the event loop. + * + * @param loop The event loop control structure to initialize. + * @return int 0 if initialization was successful, non-zero otherwise. + */ +int +event_loop_initialize(struct event_loop *loop) +{ + INIT_LIST_HEAD(&loop->scheduled_tasks); + loop->clock = CLOCK_BOOTTIME; + loop->epoll_fd = -1; + + int ret = event_loop_epoll_initialize(loop); + if (ret < 0) + return ret; + + return 0; +} + +/** + * @brief Uninitializes the event loop. + * + * This will cancel all existing timers. The event loop must not be active. + * + * @param loop The event loop control structure to uninitialize. + */ +void +event_loop_uninitialize(struct event_loop *loop) +{ + struct scheduled_task *task; + struct scheduled_task *tasktmp; + + list_for_each_entry_safe (task, tasktmp, &loop->scheduled_tasks, list) { + scheduled_task_delete(task); + } + + event_loop_epoll_uninitialize(loop); +} + +/** + * @brief Schedules a task for deferred execution. + * + * @param loop The event loop control structure. + * @param task The task to schedule. + * @param reference The reference time to schedule against + */ +static void +event_loop_schedule_task(struct event_loop *loop, struct scheduled_task *task, struct timespec *reference) +{ + // If the task is already in the scheduled task list, remove it. + if (!list_empty(&task->list)) + list_del_init(&task->list); + + // Calculate the (relative) expiry time from the reference time. + task->expiry = *reference; + task->expiry.tv_sec += task->timeout.seconds; + task->expiry.tv_nsec += task->timeout.useconds * NSEC_PER_USEC; + + // Sanitize the expiry time to ensure time-context limits aren't exceeded. + while (task->expiry.tv_nsec >= NSEC_PER_SEC) { + task->expiry.tv_sec++; + task->expiry.tv_nsec -= NSEC_PER_SEC; + } + + // Insert the scheduled task into the list of all tasks according to its expiry time. + struct scheduled_task *task_scheduled; + list_for_each_entry (task_scheduled, &loop->scheduled_tasks, list) { + if (timespec_time_is_earlier(&task->expiry, &task_scheduled->expiry)) { + list_add_tail(&task->list, &task_scheduled->list); + break; + } + } + + // If entry was not added, it is later than all existing timers, so add to + // the end of the list. + if (list_empty(&task->list)) + list_add(&task->list, &loop->scheduled_tasks); + + // Assign the loop context. + task->loop = loop; +} + +/** + * @brief Schedules a task for execution at a later time. + * + * The expiry time is specified as the total number of seconds plus the + * total number of microseconds from the current time. + * + * The event loop does not need to be rescheduled following the addition of new + * or removal of existing timers. Each time the event loop blocks to wait for + * events, it schedules its wait timeout to be the expiry time of the next + * scheduled task. This ensures the event loop will be unblocked precisely when + * the next (in time) scheduled task need to run. + * + * Since the event loop and all code interacting with it is single-threaded, a + * new task cannot be scheduled until the event loop is unblocked. This + * guarantees that newly added and removed tasks will be accounted for. + * + * @param loop The event loop control structure. + * @param seconds The number of seconds from now the task should execute. + * @param useconds The number of microseconds, relative to the number of seconds, the task should execute. + * @param type The type of scheduled task; oneshot or periodic. + * @param handler The handler function to invoke when the task expiry time occurs. + * @param task_context The contextual data that will be passed to the handler function. + * @return int + */ +int +event_loop_task_schedule(struct event_loop *loop, uint32_t seconds, uint32_t useconds, enum scheduled_task_type type, scheduled_task_handler handler, void *task_context) +{ + int ret; + struct timespec now; + struct scheduled_task *task; + + task = scheduled_task_alloc(); + if (!task) + return -ENOMEM; + + ret = clock_gettime(loop->clock, &now); + if (ret < 0) { + ret = errno; + zlog_error("failed to retrieve current time (%d)", ret); + goto fail; + } + + // Fill in structure with contextual data. + task->type = type; + task->handler = handler; + task->context = task_context; + task->timeout.seconds = seconds; + task->timeout.useconds = useconds; + + event_loop_schedule_task(loop, task, &now); + +out: + return 0; +fail: + free(task); + goto out; +} + +/** + * @brief Helper function to schedule a one-shot scheduled task to run immediately. + * + * @param loop The event loop control structure. + * @param handler The handler function to invoke when the task expiry time occurs. + * @param task_context The contextual data that will be passed to the handler function. + * @return int + */ +int +event_loop_task_schedule_now(struct event_loop *loop, scheduled_task_handler handler, void *task_context) +{ + return event_loop_task_schedule(loop, 0, 0, TASK_ONESHOT, handler, task_context); +} + +/** + * @brief Cancels a scheduled task. + * + * @param loop The event loop control structure. + * @param handler The task event handler. + * @param context The context for the event handler. + * @return uint32_t The number of tasks that were canceled. + */ +uint32_t +event_loop_task_cancel(struct event_loop *loop, scheduled_task_handler handler, void *context) +{ + uint32_t num_canceled = 0; + struct scheduled_task *task; + struct scheduled_task *tasktmp; + + list_for_each_entry_safe (task, tasktmp, &loop->scheduled_tasks, list) { + if (task->handler == handler && task->context == context) { + scheduled_task_delete(task); + num_canceled++; + } + } + + return num_canceled; +} + +/** + * @brief Macro representing an infinite timeout for epoll_wait(). + */ +#define EPOLL_TIMEOUT_INFINITE (-1) + +/** + * @brief Calculates the timeout of the next scheduled task, specified in milliseconds. + * + * If there are no configured scheduled tasks, the returned value represents an + * infinite timeout for epoll_wait(). + * + * @param loop The event loop control structure. + * @return int The timeout value to be passed to epoll_wait(). + */ +static int +event_loop_scheduled_task_next_timeout(struct event_loop *loop, struct timespec *reference) +{ + int timeout; + struct scheduled_task *task = scheduled_task_next(loop); + + if (!task) { + timeout = EPOLL_TIMEOUT_INFINITE; + } else { + timeout = scheduled_task_get_timeout(task, reference); + } + + return timeout; +} + +/** + * @brief Calculates the event loop timeout. + * + * The event loop timeout is the relative time when the event loops needs to + * unblock to check for events. Currently the only events that need to be + * checked after such a timeout is the expiry of a scheduled task. + * + * If there are no configured scheduled tasks, the returned value represents an + * infinite timeout for epoll_wait(). + * + * @param loop The event loop control structure. + * @return int The timeout to be supplied to epoll_wait(). + */ +int +event_loop_get_timeout(struct event_loop *loop) +{ + struct timespec now; + int ret = clock_gettime(loop->clock, &now); + if (ret < 0) { + ret = errno; + zlog_error("failed to retrieve current time (%d)", ret); + return ret; + } + + return event_loop_scheduled_task_next_timeout(loop, &now); +} + +/** + * @brief Process the scheduled task queue. + * + * @param loop The event loop control structure. + */ +void +event_loop_process_scheduled_tasks(struct event_loop *loop) +{ + struct scheduled_task *task = scheduled_task_next(loop); + if (!task) + return; + + struct timespec now; + int ret = clock_gettime(loop->clock, &now); + if (ret < 0) { + ret = errno; + zlog_error("failed to retrieve current time (%d)", ret); + return; + } + + if (!scheduled_task_is_expired(task, &now)) + return; + + // Copy the handler and context argument to allow removal if it's a oneshot timer. + scheduled_task_handler handler = task->handler; + enum scheduled_task_type type = task->type; + void *context = task->context; + + // Set future task action (remove or reschedule). + switch (type) { + case TASK_ONESHOT: + scheduled_task_delete(task); + break; + case TASK_PERIODIC: + event_loop_schedule_task(loop, task, &now); + break; + default: + break; + } + + zlog_debug("executing scheduled task handler=0x%" PRIx64 "(arg=%p, type=%s)", + (uint64_t)handler, + context, + scheduled_task_type_str(type)); + + handler(context); +} + +/** + * @brief Registers a handler for events that signal data is available from a file descriptor. + * + * @param loop The event loop instance. + * @param events The event types to monitor. Must be one of the EPOLL* macros. + * @param fd The file descriptor to monitor for changes. + * @param handler The handler function to invoke when data is available on 'fd'. + * @param handler_arg The argument that should be passed to the handler function. + * @return int 0 if the handler was successfully registered, non-zero otherwise. + */ +int +event_loop_register_event(struct event_loop *loop, uint32_t events, int fd, event_handler_fn handler, void *handler_arg) +{ + struct event_dispatch *dispatch = &loop->dispatch; + + ssize_t event_slot = event_dispatch_find_slot(dispatch); + if (event_slot == -1) { + zlog_error("read event slots (%lu) exhausted", dispatch->num_events); + return -ENOSPC; + } + + struct epoll_event event; + explicit_bzero(&event, sizeof event); + event.events = events; + event.data.fd = fd; + + int ret = epoll_ctl(loop->epoll_fd, EPOLL_CTL_ADD, fd, &event); + if (ret < 0) { + ret = errno; + zlog_error("failed to register fd=%d for event monitoring (%d)", fd, ret); + return ret; + } + + struct event *zevent = &dispatch->events[event_slot]; + zevent->fd = fd; + zevent->handler = handler; + zevent->handler_arg = handler_arg; + + return 0; +} + +/** + * @brief Unregisters an read event handler. + * + * @param loop The event loop instance. + * @param fd The file descriptor associated with the read event to unregister. + */ +void +event_loop_unregister_event(struct event_loop *loop, int fd) +{ + struct event *event = NULL; + struct event_dispatch *dispatch = &loop->dispatch; + + for (size_t i = 0; i < dispatch->num_events; i++) { + if (dispatch->events[i].fd == fd) { + event = &dispatch->events[i]; + break; + } + } + + if (!event) + return; + + int ret = epoll_ctl(loop->epoll_fd, EPOLL_CTL_DEL, fd, NULL); + if (ret < 0) { + ret = errno; + zlog_error("failed to unregister fd=%d from event monitoring (%d)", fd, ret); + return; + } + + event->fd = -1; + event->handler = NULL; + event->handler_arg = NULL; +} + +/** + * @brief Requests that the event loop stop running. The event loop will not + * terminate immediately, however, it will terminate at the end of the current + * loop iteration. + * + * @param loop The event loop instance. + */ +void +event_loop_stop(struct event_loop *loop) +{ + loop->terminate_pending = true; +} + +/** + * @brief Determines which event fired and dispatches the associated handler for the event. + * + * @param loop The event loop instance. + * @param event The epoll event that was signaled. + */ +static void +event_loop_process_update(struct event_loop *loop, struct epoll_event *event) +{ + struct event_dispatch *dispatch = &loop->dispatch; + + for (size_t i = 0; i < dispatch->num_events; i++) { + if (dispatch->events[i].fd == event->data.fd) { + dispatch->events[i].handler(event->data.fd, dispatch->events[i].handler_arg); + break; + } + } +} + +/** + * @brief Runs the event loop. This uses the calling thread to wait for changes + * to the event loop's configured file descriptors. The event loop will run + * until the event_loop_stop() function is called. + * + * @param loop The event loop instance. + */ +void +event_loop_run(struct event_loop *loop) +{ + for (;;) { + int timeout = event_loop_get_timeout(loop); + + int num_events = epoll_wait(loop->epoll_fd, loop->events, (int)loop->events_max, timeout); + for (int i = 0; i < num_events; i++) { + event_loop_process_update(loop, &loop->events[i]); + } + + event_loop_process_scheduled_tasks(loop); + + if (loop->terminate_pending) { + loop->terminate_pending = false; + break; + } + } +} diff --git a/src/utils/event_loop.h b/src/utils/event_loop.h new file mode 100644 index 0000000..1e25cb1 --- /dev/null +++ b/src/utils/event_loop.h @@ -0,0 +1,214 @@ + +#ifndef __EVENT_LOOP_H__ +#define __EVENT_LOOP_H__ + +#include +#include +#include + +#include + +struct scheduled_task; + +/** + * @brief Event loop read-event handler prototype. + */ +typedef void (*event_handler_fn)(int fd, void *context); + +/** + * @brief Generic file-descriptor based event. + */ +struct event { + int fd; + event_handler_fn handler; + void *handler_arg; +}; + +/** + * @brief Event dispatcher, holding a table of events. + */ +struct event_dispatch { + size_t num_events; + struct event *events; +}; + +/** + * @brief Event loop control structure. + */ +struct event_loop { + struct list_head scheduled_tasks; + clockid_t clock; + int epoll_fd; + bool terminate_pending; + + size_t events_max; + struct epoll_event *events; + struct event_dispatch dispatch; +}; + +/** + * @brief Registers a handler for events that signal data is available to be read from a file descriptor. + * + * @param loop The event loop instance. + * @param events The event types to monitor. Must be one of the EPOLL* macros. + * @param fd The file descriptor to monitor for changes to read from. + * @param handler The handler function to invoke when data is available to be read from 'fd'. + * @param handler_arg The argument that should be passed to the handler function. + * @return int 0 if the handler was successfully registered, non-zero otherwise. + */ +int +event_loop_register_event(struct event_loop *loop, uint32_t events, int fd, event_handler_fn handler, void *handler_arg); + +/** + * @brief Unregisters an read event handler. + * + * @param loop The event loop instance. + * @param fd The file descriptor associated with the read event to unregister. + */ +void +event_loop_unregister_event(struct event_loop *loop, int fd); + +/** + * @brief Type of scheduled task. + */ +enum scheduled_task_type { + TASK_ONESHOT, + TASK_PERIODIC +}; + +/** + * @brief Handler function to be invoked for a scheduled task. + */ +typedef void (*scheduled_task_handler)(void *task_context); + +struct scheduled_task_timeout { + uint32_t seconds; + uint32_t useconds; +}; + +/** + * @brief Context for scheduled task. + */ +struct scheduled_task { + struct list_head list; + struct timespec expiry; + struct event_loop *loop; + struct scheduled_task_timeout timeout; + enum scheduled_task_type type; + scheduled_task_handler handler; + void *context; +}; + +/** + * @brief Schedules a task for execution at a later time. + * + * The expiry time is specified as the total number of seconds plus the + * total number of microseconds from the current time. + * + * The event loop does not need to be rescheduled following the addition of new + * or removal of existing timers. Each time the event loop blocks to wait for + * events, it schedules its wait timeout to be the expiry time of the next + * scheduled task. This ensures the event loop will be unblocked precisely when + * the next (in time) scheduled task need to run. + * + * Since the event loop and all code interacting with it is single-threaded, a + * new task cannot be scheduled until the event loop is unblocked. This + * guarantees that newly added and removed tasks will be accounted for. + * + * @param loop The event loop control structure. + * @param seconds The number of seconds from now the task should execute. + * @param useconds The number of microseconds, relative to the number of seconds, the task should execute. + * @param type The type of scheduled task; oneshot or periodic. + * @param handler The handler function to invoke when the task expiry time occurs. + * @param task_context The contextual data that will be passed to the handler function. + * @return int + */ +int +event_loop_task_schedule(struct event_loop *loop, uint32_t seconds, uint32_t useconds, enum scheduled_task_type type, scheduled_task_handler handler, void *task_context); + +/** + * @brief Helper function to schedule a one-shot scheduled task to run immediately. + * + * @param loop The event loop control structure. + * @param handler The handler function to invoke when the task expiry time occurs. + * @param task_context The contextual data that will be passed to the handler function. + * @return int + */ +int +event_loop_task_schedule_now(struct event_loop *loop, scheduled_task_handler handler, void *task_context); + +/** + * @brief Cancels a scheduled task. + * + * @param loop The event loop control structure. + * @param handler The task event handler. + * @param context The context for the event handler. + * @return uint32_t The number of tasks that were canceled. + */ +uint32_t +event_loop_task_cancel(struct event_loop *loop, scheduled_task_handler handler, void *task_context); + +/** + * @brief Initialize the event loop. + * + * @param loop The event loop control structure to initialize. + * @return int 0 if initialization was successful, non-zero otherwise. + */ +int +event_loop_initialize(struct event_loop *loop); + +/** + * @brief Process the scheduled task queue. + * + * @param loop The event loop control structure. + */ +void +event_loop_process_scheduled_tasks(struct event_loop *loop); + +/** + * @brief Calculates the event loop timeout. + * + * The event loop timeout is the relative time when the event loops needs to + * unblock to check for events. Currently the only events that need to be + * checked after such a timeout is the expiry of a scheduled task. + * + * If there are no configured scheduled tasks, the returned value represents an + * infinite timeout for epoll_wait(). + * + * @param loop The event loop control structure. + * @return int The timeout to be supplied to epoll_wait(). + */ +int +event_loop_get_timeout(struct event_loop *loop); + +/** + * @brief Uninitializes the event loop. + * + * This will cancel all existing timers. The event loop must not be active. + * + * @param loop The event loop control structure to uninitialize. + */ +void +event_loop_uninitialize(struct event_loop *loop); + +/** + * @brief Requests that the event loop stop running. The event loop will not + * terminate immediately, however, it will terminate at the end of the current + * loop iteration. + * + * @param loop The event loop instance. + */ +void +event_loop_stop(struct event_loop *loop); + +/** + * @brief Runs the event loop. This uses the calling thread to wait for changes + * to the event loop's configured file descriptors. The event loop will run + * until the event_loop_stop() function is called. + * + * @param loop The event loop instance. + */ +void +event_loop_run(struct event_loop *loop); + +#endif //__EVENT_LOOP_H__ diff --git a/src/utils/file_utils.c b/src/utils/file_utils.c new file mode 100644 index 0000000..1f6b116 --- /dev/null +++ b/src/utils/file_utils.c @@ -0,0 +1,72 @@ + +#include +#include +#include +#include +#include + +#include "file_utils.h" +#include "ztp_log.h" + +/** + * @brief Get the link target of a file. + * + * @param filename The filename to get the link target for. + * @param target An output argument holding the link target, if it exists. + * Otherwise this will be set to NULL. + * @return int 0 if it was determined whether a link target exists and was + * written to *target. If the file does not have a link target (eg. non + * symbolic link), then *target will be set to NULL. + */ +int +get_link_target(const char *filename, char **target) +{ + ssize_t len; + char *path = NULL; + struct stat statbuf; + + int ret = lstat(filename, &statbuf); + if (ret < 0) { + ret = -errno; + zlog_error("failed to lstat file '%s' (%d)", filename, ret); + goto fail; + } else if (!S_ISLNK(statbuf.st_mode)) { + goto out; + } else if (statbuf.st_size < 0) { + zlog_error("link size (st_size) for file '%s' is invalid (< 0)", filename); + ret = -EINVAL; + goto out; + } + + size_t path_length = (size_t)statbuf.st_size; + + path = (char *)malloc(path_length + 1); + if (!path) { + zlog_error("failed to allocate memory for file '%s' link target", filename); + ret = -ENOMEM; + goto fail; + } + + len = readlink(filename, path, path_length + 1); + if (len < 0) { + ret = -errno; + zlog_error("failed to obtain file '%s' link target (%d)", filename, ret); + goto fail; + } else if ((size_t)len > path_length) { + zlog_error("file '%s' link target changed", filename); + ret = -EBUSY; + goto fail; + } + + path[len] = '\0'; + ret = 0; +out: + *target = path; + return ret; +fail: + if (path) { + free(path); + path = NULL; + } + goto out; +} diff --git a/src/utils/file_utils.h b/src/utils/file_utils.h new file mode 100644 index 0000000..3700427 --- /dev/null +++ b/src/utils/file_utils.h @@ -0,0 +1,18 @@ + +#ifndef __FILE_UTILS_H__ +#define __FILE_UTILS_H__ + +/** + * @brief Get the link target of a file. + * + * @param filename The filename to get the link target for. + * @param target An output argument holding the link target, if it exists. + * Otherwise this will be set to NULL. + * @return int 0 if it was determined whether a link target exists and was + * written to *target. If the file does not have a link target (eg. non + * symbolic link), then *target will be set to NULL. + */ +int +get_link_target(const char *filename, char **target); + +#endif //__FILE_UTILS_H__ diff --git a/src/utils/json_parse.c b/src/utils/json_parse.c new file mode 100644 index 0000000..155efb4 --- /dev/null +++ b/src/utils/json_parse.c @@ -0,0 +1,418 @@ + +#include +#include + +#include +#include +#include +#include +#include + +#include "json_parse.h" +#include "ztp_log.h" + +/** + * @brief Helper function to confirm two json types match, logging an error if not. + * + * @param name The name of the json object for which the type describes. + * @param actual The actual type of the object. + * @param expected The expected type of the object. + * @return true If the types match. + * @return false If the types differ. + */ +bool +json_validate_type(const char *name, json_type actual, json_type expected) +{ + if (actual == expected) + return true; + + zlog_debug("%s expected %s, got %s", name, json_type_to_name(expected), json_type_to_name(actual)); + return false; +} + +/** + * @brief Helper function to confirm json type of object. + * + * @param name The name of the json object for which the json object describes. + * @param jobj The json object to validate the type for. + * @param expected The expected json type for the object. + * @return true If the json object 'jobj' has actual type 'expected'. + * @return false if the json object 'jobj' has actual type different than 'expected'. + */ +bool +json_validate_object_type(const char *name, struct json_object *jobj, json_type expected) +{ + return json_validate_type(name, json_object_get_type(jobj), expected); +} + +/** + * @brief Helper function to confirm two json types of an array element match, logging an error if not. + * + * @param name The name of the json object for which the type describes. + * @param index The index of the array element within its parent. + * @param actual The actual type of the object. + * @param expected The expected type of the object. + * @return true If the types match. + * @return false If the types differ. + */ +bool +json_validate_type_array_entry(const char *name, uint32_t index, json_type actual, json_type expected) +{ + if (actual == expected) + return true; + + zlog_debug("%s[%u] expected %s, got %s", name, index, json_type_to_name(expected), json_type_to_name(actual)); + return false; +} + +/** + * @brief Context used for automatically continuing object iteration. + */ +struct json_object_visit_continue_context { + void *context; + json_object_visit_fn visit; +}; + +/** + * @brief Convenience visitor function for json_for_each_object that continues + * following each iteration. This is suitable when all properties need to be + * enumerated with no stopping condition. + * + * @param parent The parent object. + * @param name The name of the property associated with the object. + * @param jobj The json object to iterate over. + * @param context Context pointer. This must be of type struct + * json_object_visit_continue_context, which contains the outer/child context + * that will be supplied to the visit function. + * @return int + */ +static int +json_object_visit_continue(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + struct json_object_visit_continue_context *child = (struct json_object_visit_continue_context *)context; + child->visit(parent, name, jobj, child->context); + return JSON_ITERATE_CONTINUE; +} + +/** + * @brief Helper function for processing all entries in a json object. Mostly + * syntactic sugar around the clunky iterator interface. + + * @param jobj The json object to process. + * @param visit The function to process each json object entry. + * @param context The user supplied context data. + */ +void +json_for_each_object_ss(struct json_object *jobj, json_object_visit_ss_fn visit, void *context) +{ + struct json_object_iterator it = json_object_iter_begin(jobj); + struct json_object_iterator end = json_object_iter_end(jobj); + + while (!json_object_iter_equal(&it, &end)) { + int ret = visit(jobj, json_object_iter_peek_name(&it), json_object_iter_peek_value(&it), context); + if (ret != JSON_ITERATE_CONTINUE) + break; + json_object_iter_next(&it); + } +} + +/** + * @brief Helper function for processing all entries in a json object. Mostly + * syntactic sugar around the clunky iterator interface. + + * @param jobj The json object to process. + * @param visit The function to process each json object entry. + * @param context The user supplied context data. + */ +void +json_for_each_object(struct json_object *jobj, json_object_visit_fn visit, void *context) +{ + struct json_object_visit_continue_context parent_context = { + .visit = visit, + .context = context, + }; + + json_for_each_object_ss(jobj, json_object_visit_continue, &parent_context); +} + +/** + * @brief Context used for automatically continuing array iteration. + */ +struct json_array_visit_continue_context { + void *context; + json_array_visit_fn visit; +}; + +/** + * @brief Convenience visitor function for json_for_each_array that continues + * following each iteration. This is suitable when all array entires need to be + * iterated over with no stopping function. + * + * @param parent The parent object. + * @param array The containing json array. + * @param name The name of the parent object. + * @param value The current array object value. + * @param index The current array index. + * @param type The type of the object value. + * @param context Context pointer. This must be of type struct + * json_array_visit_continue_context, which contains the outer/child context + * that will be supplied to the visit function + */ +int +json_array_visit_continue(struct json_object *parent, struct json_object *array, const char *name, struct json_object *value, uint32_t index, json_type type, void *context) +{ + struct json_array_visit_continue_context *child = (struct json_array_visit_continue_context *)context; + child->visit(parent, array, name, value, index, type, child->context); + return JSON_ITERATE_CONTINUE; +} + +/** + * @brief Helper function for processing all entries in a json array. + * + * @param parent The parent object. + * @param array The json array object to process. + * @param name The name of the key/object representing the array. + * @param visit The function to process each json array entry. + * @param context The user supplied context data. + */ +void +json_for_each_array_entry_ss(struct json_object *parent, struct json_object *array, const char *name, json_array_visit_ss_fn visit, void *context) +{ + size_t num_entries = json_object_array_length(array); + if (num_entries == 0) + return; + + for (uint32_t i = 0; i < num_entries; i++) { + struct json_object *element = json_object_array_get_idx(array, i); + json_type type = json_object_get_type(element); + int ret = visit(parent, array, name, element, i, type, context); + if (ret != JSON_ITERATE_CONTINUE) + break; + } +} + +/** + * @brief Helper function for processing all entries in a json array. + * + * @param parent The parent object. + * @param array The json object to process. + * @param name The name of the key/object representing the array. + * @param visit The function to process each json array entry. + * @param context The user supplied context data. + */ +void +json_for_each_array_entry(struct json_object *parent, struct json_object *array, const char *name, json_array_visit_fn visit, void *context) +{ + struct json_array_visit_continue_context parent_context = { + .visit = visit, + .context = context, + }; + + json_for_each_array_entry_ss(parent, array, name, json_array_visit_continue, &parent_context); +} + +/** + * @brief Context used for processing each array entry with + * json_for_each_array_type_visitor. + */ +struct json_for_each_array_type_context { + json_type type_expected; + json_array_visit_fn visit; + void *visit_context; +}; + +/** + * @brief Visitor function used with json_for_each_array_type. + * + * @param parent The parent object. + * @param array The containing array object. + * @param name The name of the parent object. + * @param value The value of the array entry with index 'index' of the parent (ie. parent[index]). + * @param index The index of the value within the parent. + * @param type The type of the array element value. + * @param context The user-supplied context information. + */ +void +json_for_each_array_type_visitor(struct json_object *parent, struct json_object *array, const char *name, struct json_object *value, uint32_t index, json_type type, void *context) +{ + struct json_for_each_array_type_context *array_context = (struct json_for_each_array_type_context *)context; + + if (!json_validate_type_array_entry(name, index, type, array_context->type_expected)) + return; + + array_context->visit(parent, array, name, value, index, type, array_context->visit_context); +} + +/** + * @brief Helper function for processing all entries in a json array. This also + * provides strict type checking on each array element. + + * @param parent The parent object. + * @param array The json object to process. + * @param name The name of the parent json object. + * @param visit The function to process each json array entry. + * @param type_expected The expected type of each array element. + * @param context The user supplied context data. + */ +void +json_for_each_array_entry_type(struct json_object *parent, struct json_object *array, const char *name, json_type type_expected, json_array_visit_fn visit, void *context) +{ + struct json_for_each_array_type_context array_context = { + .type_expected = type_expected, + .visit = visit, + .visit_context = context, + }; + + json_for_each_array_entry(parent, array, name, json_for_each_array_type_visitor, &array_context); +} + +/** + * @brief Generic string parsing function. + * + * This function parses a json string and writes it to a destination pointer. + * The destination must be provided as the context (type char**). If The + * context holds an existing string, it will be deleted using free(). + * + * @param parent The parent object. + * @param name The name of the json property. + * @param jobj The json object value. + * @param context The destination where a copy of the string should be written. + */ +void +json_parse_string_generic(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + __unused(parent); + + char *value = strdup(json_object_get_string(jobj)); + if (!value) { + zlog_warning("allocation failure parsing string property '%s'", name); + return; + } + + char **destination = (char **)context; + if (*destination) + free(*destination); + + *destination = value; +} + +/** + * @brief Helper structure used for parsing arbitrary json values. This allows + * passing parsing-specific context, along with user-supplied context to + * parsers. + */ +struct json_parse_property_opts { + struct json_property_parser *parsers; + size_t count; + void *context; +}; + +/** + * @brief Returns the child property context, given the parent property context. + * + * @param parser The parsing configuration for the property. + * @param parent_context The parent object context. + * @return void* The child's context object. + */ +static void * +json_get_child_context(struct json_property_parser *parser, void *parent_context) +{ + return parser->get_context + ? parser->get_context(parent_context, parser->name) + : parent_context; +} + +/** + * @brief Handler function to parse a json property of an object. + * + * @param parent The parent object. + * @param name The name of the json property being parsed. + * @param jobj The value of the json property being parsed. + * @param context The context for the property being parsed. This must be an + * instance of type 'struct json_parse_property_opts *' which describes all of + * the supported properties and their parsers. + */ +static void +json_parse_property(struct json_object *parent, const char *name, struct json_object *jobj, void *context) +{ + struct json_parse_property_opts *properties = (struct json_parse_property_opts *)context; + + for (size_t i = 0; i < properties->count; i++) { + struct json_property_parser *property = &properties->parsers[i]; + if (strcmp(name, property->name) != 0) + continue; + + if (!json_validate_object_type(name, jobj, property->type)) + continue; + + void *context_child = json_get_child_context(property, properties->context); + + json_type type = json_object_get_type(jobj); + switch (type) { + case json_type_array: + if (property->array.parse) + json_for_each_array_entry_type(parent, jobj, name, property->array.type, property->array.parse, context_child); + // fall-through to value parser since array entries can have both. + // fallthrough + default: + if (property->value.parse) + property->value.parse(parent, name, jobj, context_child); + break; + } + } +} + +/** + * @brief Parses a json object and all of its properties. + * + * @param jobj The json object to parse. + * @param parsers An array of parsers describing the supported properties. + * @param parsers_num The number of parsers in the 'parsers' array. + * @param context The user-supplied context to be passed to each parsing + * function. + */ +void +json_parse_object(struct json_object *jobj, struct json_property_parser *parsers, size_t parsers_num, void *context) +{ + struct json_parse_property_opts opts = { + .parsers = parsers, + .count = parsers_num, + .context = context, + }; + + json_for_each_object(jobj, json_parse_property, &opts); +} + +/** + * @brief Parses a json-formatted file. + * + * @param file The path of the file to parse. + * @param parsers An array of parses describing the supported properties. + * @param parsers_num The number of parses in the 'parsers' array. + * @param context The user-supplied context to be passed to each parsing + * function. + * @param json Output argument to hold the parsed json object. May be NULL. + * @return int Returns 0 if parsing was successful, non-zero otherwise. + */ +int +json_parse_file(const char *file, struct json_property_parser *parsers, size_t parsers_num, void *context, struct json_object **json) +{ + zlog_debug("parsing %s", file); + + struct json_object *jobj = json_object_from_file(file); + if (!jobj) { + const char *err = json_util_get_last_err(); + zlog_error("parsing %s failed (%s)", file, err); + return -1; + } + + json_parse_object(jobj, parsers, parsers_num, context); + + if (json) { + *json = jobj; + } else { + json_object_put(jobj); + } + + return 0; +} diff --git a/src/utils/json_parse.h b/src/utils/json_parse.h new file mode 100644 index 0000000..e168648 --- /dev/null +++ b/src/utils/json_parse.h @@ -0,0 +1,217 @@ + +#ifndef __JSON_PARSE_H__ +#define __JSON_PARSE_H__ + +#include +#include +#include + +#include +#include + +/** + * @brief Helper function to confirm two json types match, logging an error if not. + * + * @param name The name of the json object for which the type describes. + * @param actual The actual type of the object. + * @param expected The expected type of the object. + * @return true If the types match. + * @return false If the types differ. + */ +bool +json_validate_type(const char *name, json_type actual, json_type expected); + +/** + * @brief Helper function to confirm json type of object. + * + * @param name The name of the json object for which the json object describes. + * @param jobj The json object to validate the type for. + * @param expected The expected json type for the object. + * @return true If the json object 'jobj' has actual type 'expected'. + * @return false if the json object 'jobj' has actual type different than 'expected'. + */ +bool +json_validate_object_type(const char *name, struct json_object *jobj, json_type expected); + +/** + * @brief Helper function to confirm two json types of an array element match, logging an error if not. + * + * @param name The name of the json object for which the type describes. + * @param index The index of the array element within its parent. + * @param actual The actual type of the object. + * @param expected The expected type of the object. + * @return true If the types match. + * @return false If the types differ. + */ +bool +json_validate_type_array_entry(const char *name, uint32_t index, json_type actual, json_type expected); + +/** + * @brief Macros to be returned by the visit functions to indicate whether iteration should stop or continue. + */ +#define JSON_ITERATE_CONTINUE 0 +#define JSON_ITERATE_STOP 1 + +/** + * @brief Prototypes for json entry handler function used in + * json_for_each_object helper. The 'ss' version allows short-circuiting + * iteration based on the return value of the visit function. + */ +typedef void (*json_object_visit_fn)(struct json_object *parent, const char *name, struct json_object *value, void *context); +typedef int (*json_object_visit_ss_fn)(struct json_object *parent, const char *name, struct json_object *value, void *context); + +/** + * @brief Helper function for processing all entries in a json object. Mostly + * syntactic sugar around the clunky iterator interface. + + * @param jobj The json object to process. + * @param visit The function to process each json object entry. + * @param context The user supplied context data. + */ +void +json_for_each_object(struct json_object *jobj, json_object_visit_fn visit, void *context); + +/** + * @brief Helper function for processing all entries in a json object. Mostly + * syntactic sugar around the clunky iterator interface. + + * @param jobj The json object to process. + * @param visit The function to process each json object entry. + * @param context The user supplied context data. + */ +void +json_for_each_object_ss(struct json_object *jobj, json_object_visit_ss_fn visit, void *context); + +/** + * @brief Prototype for json entry handler function used in + * json_for_each_array_entry helper. + */ +typedef void (*json_array_visit_fn)(struct json_object *parent, struct json_object *array, const char *name, struct json_object *value, uint32_t index, json_type type, void *context); +typedef int (*json_array_visit_ss_fn)(struct json_object *parent, struct json_object *array, const char *name, struct json_object *value, uint32_t index, json_type type, void *context); + +/** + * @brief Helper function for processing all entries in a json array. + * + * @param parent The parent object. + * @param array The json array object to process. + * @param name The name of the key/object representing the array. + * @param visit The function to process each json array entry. + * @param context The user supplied context data. + */ +void +json_for_each_array_entry(struct json_object *parent, struct json_object *array, const char *name, json_array_visit_fn visit, void *context); + +/** + * @brief Helper function for processing all entries in a json array. + * + * @param parent The parent object. + * @param array The json array object to process. + * @param name The name of the key/object representing the array. + * @param visit The function to process each json array entry. + * @param context The user supplied context data. + */ +void +json_for_each_array_entry_ss(struct json_object *parent, struct json_object *array, const char *name, json_array_visit_ss_fn visit, void *context); + +/** + * @brief Visitor function used with json_for_each_array_type. + * + * @param parent The parent object. + * @param array The json array object to process. + * @param name The name of the parent object. + * @param value The value of the array entry with index 'index' of the parent (ie. parent[index]). + * @param index The index of the value within the parent. + * @param type The type of the array element value. + * @param context The user-supplied context information. + */ +void +json_for_each_array_type_visitor(struct json_object *parent, struct json_object *array, const char *name, struct json_object *value, uint32_t index, json_type type, void *context); + +/** + * @brief Helper function for processing all entries in a json array. This also + * provides strict type checking on each array element. + + * @param parent The parent object. + * @param array The json object to process. + * @param name The name of the parent json object. + * @param visit The function to process each json array entry. + * @param type_expected The expected type of each array element. + * @param context The user supplied context data. + */ +void +json_for_each_array_entry_type(struct json_object *parent, struct json_object *array, const char *name, json_type type_expected, json_array_visit_fn visit, void *context); + +/** + * @brief Function prototype for returning the property's context, given its parent context. + */ +typedef void *(*json_property_get_context_fn)(void *parent_context, const char *name); + +/** + * @brief JSON property parser. + */ +struct json_property_parser { + const char *name; + json_type type; + struct { + json_object_visit_fn parse; + } value; + struct { + json_array_visit_fn parse; + json_type type; + } array; + json_property_get_context_fn get_context; +}; + +/** + * @brief Generic string parsing function. + * + * This function parses a json string and writes it to a destination pointer. + * The destination must be provided as the context (type char**). If The + * context holds an existing string, it will be deleted using free(). + * + * @param name The name of the json property. + * @param jobj The json object value. + * @param context The destination where a copy of the string should be written. + */ +void +json_parse_string_generic(struct json_object *parent, const char *name, struct json_object *jobj, void *context); + +/** + * @brief Parses a json object and all of its properties. + * + * @param jobj The json object to parse. + * @param parsers An array of parsers describing the supported properties. + * @param parsers_num The number of parsers in the 'parsers' array. + * @param context The user-supplied context to be passed to each parsing + * function. + */ +void +json_parse_object(struct json_object *jobj, struct json_property_parser *parsers, size_t parsers_num, void *context); + +/** + * @brief Helper macro accepting a statically-sized parsers array. + */ +#define json_parse_object_s(_jobj, _parsers, _context) \ + json_parse_object((_jobj), (_parsers), ARRAY_SIZE((_parsers)), _context) + +/** + * @brief Parses a json-formatted file. + * + * @param file The path of the file to parse. + * @param parsers An array of parses describing the supported properties. + * @param parsers_num The number of parses in the 'parsers' array. + * @param context The user-supplied context to be passed to each parsing + * function. + * @param json Output argument to hold the parsed json object. May be NULL. + * @return int Returns 0 if parsing was successful, non-zero otherwise. + */ +int +json_parse_file(const char *file, struct json_property_parser *parsers, size_t parsers_num, void *context, struct json_object **json); + +/** + * @brief Helper macro accepting a statically-sized parsers array. + */ +#define json_parse_file_s(_file, _parsers, _context, _json) \ + json_parse_file((_file), (_parsers), ARRAY_SIZE((_parsers)), _context, _json) + +#endif //__JSON_PARSE_H__ diff --git a/src/utils/led_ctrl.c b/src/utils/led_ctrl.c new file mode 100644 index 0000000..0ff5f0b --- /dev/null +++ b/src/utils/led_ctrl.c @@ -0,0 +1,340 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "led_ctrl.h" +#include "ztp_log.h" + +/** + * @brief Destroys an led_ctrl object, freeing all owned resources. + * + * @param led The led control object to destroy. + */ +void +led_ctrl_destroy(struct led_ctrl *led) +{ + led_ctrl_set_off(led); + + if (led->fd_pattern != -1) { + close(led->fd_pattern); + led->fd_pattern = -1; + } + + if (led->fd_repeat != -1) { + close(led->fd_repeat); + led->fd_repeat = -1; + } + + free(led); +} + +/** + * @brief Path prefix for all sysfs-based led nodes. + */ +#define LED_SYSFS_PATH_PREFIX "/sys/class/leds/" + +/** + * @brief Value for the pattern trigger. + */ +#define LED_TRIGGER_PATTERN "pattern" + +/** + * @brief Creates a new led control object. This can be used to perform various + * common tasks controlling an LED. + * + * @param node The sysfs node name of the led. Eg. /sys/class/leds/ + * @param led_ctrl Output argument to hold to allocated control object. + * @return int 0 if the control object was successfully created, non-zero otherwise. + */ +int +led_ctrl_create(const char *node, struct led_ctrl **led_ctrl) +{ + size_t path_length = strlen(node) + (sizeof LED_SYSFS_PATH_PREFIX) + 1; + + struct led_ctrl *led = calloc(1, (sizeof *led) + path_length); + if (!led) { + zlog_error("failed to allocate memory for led control structure"); + return -ENOMEM; + } + + int ret = snprintf(led->path, path_length, LED_SYSFS_PATH_PREFIX "%s", node); + if (ret < 0) { + ret = -errno; + zlog_error("failed to construct led node path (%d)", ret); + goto fail; + } else if ((size_t)ret > path_length) { + zlog_error("failed to construct led node path (too long)"); + ret = -ENAMETOOLONG; + goto fail; + } + + char path[PATH_MAX]; + ret = snprintf(path, sizeof path, "%s/trigger", led->path); + if (ret < 0) { + ret = -errno; + zlog_error("failed to construct led trigger path (%d)", ret); + goto fail; + } else if ((size_t)ret > sizeof path) { + zlog_error("failed to construct led trigger path (too long)"); + ret = -ENAMETOOLONG; + goto fail; + } + + int fd_trigger = open(path, O_WRONLY); + if (fd_trigger < 0) { + ret = -errno; + zlog_error("failed to open led trigger path (%d)", ret); + goto fail; + } + + ssize_t written = write(fd_trigger, LED_TRIGGER_PATTERN, sizeof LED_TRIGGER_PATTERN); + if (written < 0 || (size_t)written != sizeof LED_TRIGGER_PATTERN) { + ret = (written < 0) ? -errno : -EAGAIN; + zlog_error("failed to set led trigger to '" LED_TRIGGER_PATTERN "' (%d)", ret); + close(fd_trigger); + goto fail; + } + + close(fd_trigger); + + ret = snprintf(path, sizeof path, "%s/pattern", led->path); + if (ret < 0) { + ret = -errno; + zlog_error("failed to construct led pattern path (%d)", ret); + goto fail; + } else if ((size_t)ret > sizeof path) { + ret = -ENAMETOOLONG; + zlog_error("failed to construct led pattern path (too long)"); + goto fail; + } + + led->fd_pattern = open(path, O_RDWR); + if (led->fd_pattern < 0) { + ret = -errno; + zlog_error("failed to open led pattern path '%s' (%d)", path, ret); + goto fail; + } + + ret = snprintf(path, sizeof path, "%s/repeat", led->path); + if (ret < 0) { + ret = -errno; + zlog_error("failed to construct led repeat path (%d)", ret); + goto fail; + } else if ((size_t)ret > sizeof path) { + ret = -ENAMETOOLONG; + zlog_error("failed to construct led repeat path (too long)"); + goto fail; + } + + led->fd_repeat = open(path, O_RDWR); + if (led->fd_repeat < 0) { + ret = -errno; + zlog_error("failed to open led repeat path '%s' (%d)", path, ret); + goto fail; + } + + *led_ctrl = led; + ret = 0; +out: + return ret; +fail: + led_ctrl_destroy(led); + goto out; +} + +/** + * @brief Macro stringification helpers. + */ +#define XSTR(s) STR(s) +#define STR(s) #s + +/** + * @brief Helper macros for working with the led pattern driver. + */ +#define LED_BRIGHTNESS_MIN 0 +#define LED_BRIGHTNESS_MAX 255 +#define LED_BRIGHTNESS_MIN_STR XSTR(LED_BRIGHTNESS_MIN) +#define LED_BRIGHTNESS_MAX_STR XSTR(LED_BRIGHTNESS_MAX) + +/** + * @brief LED repeat value to repeat the current pattern indefinitely. + */ +#define LED_REPEAT_INDEFINITELY "-1" + +/** + * @brief Pattern which defines a repeating series of on/offs. Specifically, + * this describes an led pattern where the LED will fully illuminate then fully + * clear for the same period of time. + */ +#define LED_FMT_PATTERN_REPEATING \ + LED_BRIGHTNESS_MIN_STR " %" PRIu32 " " LED_BRIGHTNESS_MIN_STR " 0 " \ + LED_BRIGHTNESS_MAX_STR " %" PRIu32 " " LED_BRIGHTNESS_MAX_STR " 0" + +/** + * @brief Pattern which defines either turning the LED on or off indefinitely. + */ +#define LED_FMT_PATTERN_ONOFF \ + "%" PRIu32 " 0 %" PRIu32 " 0" + +/** + * @brief Sets a pattern on the led node. The pattern must be a series of + * tuples of brightness and duration. + * + * The pattern will repeat indefinitely. + * + * @param led The led control object for the led to set the pattern for. + * @param pattern The pattern to set. Must be a serious of tuples. See above + * for more details. + * @return int 0 if the pattern was set, non-zero otherwise. + */ +int +led_ctrl_set_pattern(struct led_ctrl *led, const char *pattern) +{ + if (led->fd_repeat == -1 || led->fd_pattern == -1) { + zlog_error("failed to set pattern for led '%s' (uninitialized)", led->path); + return -EBADF; + } + + int ret; + size_t length = sizeof LED_REPEAT_INDEFINITELY; + ssize_t written = write(led->fd_repeat, LED_REPEAT_INDEFINITELY, length); + if (written < 0 || (size_t)written < length) { + ret = (written < 0) ? -errno : -EAGAIN; + zlog_error("failed to enable indefinite repearing for led '%s' (%d)", led->path, ret); + return ret; + } + + length = strlen(pattern); + written = write(led->fd_pattern, pattern, length); + if (written < 0 || (size_t)written < length) { + ret = (written < 0) ? -errno : -EAGAIN; + zlog_error("failed to set pattern for led '%s' (%d)", led->path, ret); + return ret; + } + + zlog_debug("set pattern='%s' for led '%s'", pattern, led->path); + + return 0; +} + +/** + * @brief Sets a pattern interval, or blink pattern, for an led. The blink + * pattern toggles between maximum brightness and no brightness, making it an + * on/off pattern. + * + * The amount of time the led stays on and off is specified by the 'ms_on' and + * 'ms_off' arguments respectively. Both intervals are specified in milliseconds. + * + * The pattern will repeat indefinitely. + * + * @param led The led control object to set the pattern for. + * @param ms_off The number of milliseconds the led should stay off. + * @param ms_on The number of milliseconds the led should stay illuminated. + * @return int 0 if the pattern was set, non-zero otherwise. + */ +int +led_ctrl_set_repeating_pattern_interval(struct led_ctrl *led, uint32_t ms_off, uint32_t ms_on) +{ + char pattern[64]; + + int ret = snprintf(pattern, sizeof pattern, LED_FMT_PATTERN_REPEATING, ms_off, ms_on); + if (ret < 0) { + ret = -errno; + zlog_error("failed to construct pattern string for led '%s' (%d)", led->path, ret); + return ret; + } else if ((size_t)ret > sizeof pattern) { + zlog_error("failed to construct pattern string for led '%s' (pattern too long)", led->path); + return -ENAMETOOLONG; + } + + return led_ctrl_set_pattern(led, pattern); +} + +/** + * @brief Sets a pattern interval, or blink pattern. The blink pattern toggles + * between maximum brightness and no brightness, for equal amounts of time. + * + * Eg. period_ms = 500 means the led will stay on for 500 ms (0.5s) and then + * stay off for 500 ms. + * + * The pattern will repeat indefinitely. + * + * @param led The led control object to set the pattern for. + * @param period_ms The number of milliseconds the led should stay on and off. + * @return int 0 if the pattern was set, non-zero otherwise. + */ +int +led_ctrl_set_repeating_pattern(struct led_ctrl *led, uint32_t period_ms) +{ + return led_ctrl_set_repeating_pattern_interval(led, period_ms, period_ms); +} + +/** + * @brief Sets the coarse on/off state of the led, either on (max brightness) + * or off (min brightness). + * + * @param led The led control object to set the pattern for. + * @param state The desired state of the led, either LED_ON or LED_OFF. + * @return int 0 if the pattern was set, non-zero otherwise. + */ +int +led_ctrl_set_state(struct led_ctrl *led, enum led_state state) +{ + uint32_t brightness; + switch (state) { + case LED_ON: + brightness = LED_BRIGHTNESS_MAX; + break; + case LED_OFF: + brightness = LED_BRIGHTNESS_MIN; + break; + default: + zlog_error("invalid led state specified"); + return -EINVAL; + } + + char pattern[32]; + int ret = snprintf(pattern, sizeof pattern, LED_FMT_PATTERN_ONOFF, brightness, brightness); + if (ret < 0) { + ret = -errno; + zlog_error("failed to construct on/off pattern string for led '%s' (%d)", led->path, ret); + return ret; + } else if ((size_t)ret > sizeof pattern) { + zlog_error("failed to construct on/off pattern string for led '%s' (pattern too long)", led->path); + return -ENAMETOOLONG; + } + + return led_ctrl_set_pattern(led, pattern); +} + +/** + * @brief Turns the LED on. + * + * @param led The led to turn on. + * @return int 0 if the led was turned on, non-zero otherwise. + */ +int +led_ctrl_set_on(struct led_ctrl *led) +{ + return led_ctrl_set_state(led, LED_ON); +} + +/** + * @brief Turns the LED off. + * + * @param led The led to turn off. + * @return int 0 if the led was turned off, non-zero otherwise. + */ +int +led_ctrl_set_off(struct led_ctrl *led) +{ + return led_ctrl_set_state(led, LED_OFF); +} diff --git a/src/utils/led_ctrl.h b/src/utils/led_ctrl.h new file mode 100644 index 0000000..7e39f67 --- /dev/null +++ b/src/utils/led_ctrl.h @@ -0,0 +1,121 @@ + +#ifndef __LED_CTRL_H__ +#define __LED_CTRL_H__ + +#include + +/** + * @brief LED control object, exposing common functionality interacting with an + * LED. + */ +struct led_ctrl { + int fd_pattern; + int fd_repeat; + char path[]; +}; + +/** + * @brief Coarse LED state, either on (max brightness) or off (min brightness). + */ +enum led_state { + LED_ON, + LED_OFF +}; + +/** + * @brief Destroys an led_ctrl object, freeing all owned resources. + * + * @param led The led control object to destroy. + */ +void +led_ctrl_destroy(struct led_ctrl *led); + +/** + * @brief Creates a new led control object. This can be used to perform various + * common tasks controlling an LED. + * + * @param node The sysfs node name of the led. Eg. /sys/class/leds/ + * @param led_ctrl Output argument to hold to allocated control object. + * @return int 0 if the control object was successfully created, non-zero otherwise. + */ +int +led_ctrl_create(const char *node, struct led_ctrl **led_ctrl); + +/** + * @brief Sets a pattern on the led node. The pattern must be a series of + * tuples of brightness and duration. + * + * The pattern will repeat indefinitely. + * + * @param led The led control object for the led to set the pattern for. + * @param pattern The pattern to set. Must be a serious of tuples. See above + * for more details. + * @return int 0 if the pattern was set, non-zero otherwise. + */ +int +led_ctrl_set_pattern(struct led_ctrl *led, const char *pattern); + +/** + * @brief Sets a pattern interval, or blink pattern, for an led. The blink + * pattern toggles between maximum brightness and no brightness, making it an + * on/off pattern. + * + * The amount of time the led stays on and off is specified by the 'ms_on' and + * 'ms_off' arguments respectively. Both intervals are specified in milliseconds. + * + * The pattern will repeat indefinitely. + * + * @param led The led control object to set the pattern for. + * @param ms_off The number of milliseconds the led should stay off. + * @param ms_on The number of milliseconds the led should stay illuminated. + * @return int 0 if the pattern was set, non-zero otherwise. + */ +int +led_ctrl_set_repeating_pattern_interval(struct led_ctrl *led, uint32_t ms_off, uint32_t ms_on); + +/** + * @brief Sets a pattern interval, or blink pattern. The blink pattern toggles + * between maximum brightness and no brightness, for equal amounts of time. + * + * Eg. period_ms = 500 means the led will stay on for 500 ms (0.5s) and then + * stay off for 500 ms. + * + * The pattern will repeat indefinitely. + * + * @param led The led control object to set the pattern for. + * @param period_ms The number of milliseconds the led should stay on and off. + * @return int 0 if the pattern was set, non-zero otherwise. + */ +int +led_ctrl_set_repeating_pattern(struct led_ctrl *led, uint32_t period_ms); + +/** + * @brief Sets the coarse on/off state of the led, either on (max brightness) + * or off (min brightness). + * + * @param led The led control object to set the pattern for. + * @param state The desired state of the led, either LED_ON or LED_OFF. + * @return int 0 if the pattern was set, non-zero otherwise. + */ +int +led_ctrl_set_state(struct led_ctrl *led, enum led_state state); + +/** + * @brief Turns the LED on. + * + * @param led The led to turn on. + * @return int 0 if the led was turned on, non-zero otherwise. + */ +int +led_ctrl_set_on(struct led_ctrl *led); + +/** + * @brief Turns the LED off. + * + * @param led The led to turn off. + * @return int 0 if the led was turned off, non-zero otherwise. + */ +int +led_ctrl_set_off(struct led_ctrl *led); + +#endif //__LED_CTRL_H__ diff --git a/src/utils/string_utils.c b/src/utils/string_utils.c new file mode 100644 index 0000000..d33bbcd --- /dev/null +++ b/src/utils/string_utils.c @@ -0,0 +1,150 @@ + +#include +#include +#include +#include +#include +#include +#include + +#include "string_utils.h" + +/** + * @brief Decodes a single hex binary value. + * + * @param hex A pointer to the string to decode the next hex digit for. + * @param skip The function used to determine if characters should be skipped. + * @return int The value of the hex digit, if valid. Otherwise -1. + */ +static int +hex_decode_next(const char **hex, skip_fn skip) +{ + while (skip(**hex)) + (*hex)++; + + if (!**hex || !isxdigit(**hex)) + return -1; + + char c = *((*hex)++); + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + return -1; +} + +/** + * @brief Decodes a hex string into a raw buffer, skipping characters according + * to a specified skip function. + * + * @param hex The hex string to decode. + * @param buffer The buffer to write the binary output to. + * @param length The length of 'buffer', in bytes. + * @param skip The function used to determine if a character in the string should be skipped. + * @return ssize_t The number of bytes written to 'buffer' if the string was + * decoded successfully, -1 otherwise. + */ +ssize_t +hex_decode_withskip(const char *hex, uint8_t *buffer, size_t length, skip_fn skip) +{ + size_t length_original = length; + + for (;;) { + if (!*hex) + return (ssize_t)(length_original - length); + + int high = hex_decode_next(&hex, skip); + int low = hex_decode_next(&hex, skip); + if (high == -1 || low == -1 || length == 0) + return -1; + + *buffer++ = (uint8_t)(high << 4) | (uint8_t)low; + length--; + } +} + +/** + * @brief Decodes a hex string into a raw buffer, skipping whitespace. + * + * @param hex The hex string to decode. + * @param buffer The buffer to write the binary output to. + * @param length The length of 'buffer', in bytes. + * @return ssize_t The number of bytes written to 'buffer' if the string was + * decoded successfully, -1 otherwise. + */ +ssize_t +hex_decode(const char *hex, uint8_t *buffer, size_t length) +{ + return hex_decode_withskip(hex, buffer, length, isspace); +} + +/** + * @brief Encodes a byte array as a hex string. + * + * @param buffer The buffer to encode. + * @param length The length, in bytes, of 'buffer'. Must be > 0. + * @param dst The buffer to write the string. + * @param dstlength The length, in bytes. of 'dst'. + */ +void +hex_encode(const uint8_t *buffer, size_t length, char *dst, size_t dstlength) +{ + assert(length > 0); + assert(dstlength >= ((length * 2) + 1)); + + const uint8_t *end = buffer + length; + while (buffer < end) { + snprintf(dst, dstlength, "%02x", *buffer++); + dstlength -= 2; + dst += 2; + } + + *dst = '\0'; +} + +/** + * @brief Function to skip common MAC address separators. + * + * @param c The character to check. + * @return true If the character should be skipped. + * @return false Otherwise. + */ +int +mac_skip(int c) +{ + switch (c) { + case ':': + case '-': + return 1; + default: + return isspace(c); + } +} + +/** + * @brief Determine if a string starts with a substring, providing a pointer to + * the first character following the substring match. + * + * Eg. s1 = "<10> DPP-RX_CHIRP src=0A:1B:2C:3D:4E:5F hash=..." + * s2 = "DPP-RX-CHIRP " + * strstart(s1, s2) -> true, *out = "src=0A:1B:2C:3D:4E:5F hash=..." + * + * @param s1 The string to check. + * @param s2 The substring to find. + * @param out The output string to hold the first character following the + * occurrence of the substring in s1. + * @return true + * @return false + */ +bool +strstart(const char *s1, const char *s2, const char **out) +{ + size_t s2len = strlen(s2); + if (strncmp(s1, s2, s2len) != 0) + return false; + + *out = s1 + s2len; + return true; +} diff --git a/src/utils/string_utils.h b/src/utils/string_utils.h new file mode 100644 index 0000000..090767a --- /dev/null +++ b/src/utils/string_utils.h @@ -0,0 +1,80 @@ + +#ifndef __STRING_UTILS_H__ +#define __STRING_UTILS_H__ + +#include +#include +#include +#include + +/** + * @brief Function signature for checking if a character should be skipped. + */ +typedef int (*skip_fn)(int); + +/** + * @brief Decodes a hex string into a raw buffer, skipping characters according + * to a specified skip function. + * + * @param hex The hex string to decode. + * @param buffer The buffer to write the binary output to. + * @param length The length of 'buffer', in bytes. + * @param skip The function used to determine if a character in the string should be skipped. + * @return ssize_t The number of bytes written to 'buffer' if the string was + * decoded successfully, -1 otherwise. + */ +ssize_t +hex_decode_withskip(const char *hex, uint8_t *buffer, size_t length, skip_fn skip); + +/** + * @brief Decodes a hex string into a raw buffer, skipping whitespace. + * + * @param hex The hex string to decode. + * @param buffer The buffer to write the binary output to. + * @param length The length of 'buffer', in bytes. + * @return ssize_t The number of bytes written to 'buffer' if the string was + * decoded successfully, -1 otherwise. + */ +ssize_t +hex_decode(const char *hex, uint8_t *buffer, size_t length); + +/** + * @brief Encodes a byte array as a hex string. + * + * @param buffer The buffer to encode. + * @param length The length, in bytes, of 'buffer'. + * @param dst The buffer to write the string. + * @param dstlength The length, in bytes. of 'dst'. + */ +void +hex_encode(const uint8_t *buffer, size_t length, char *dst, size_t dstlength); + +/** + * @brief Function to skip common MAC address separators. + * + * @param c The character to check. + * @return true If the character should be skipped. + * @return false Otherwise. + */ +int +mac_skip(int c); + +/** + * @brief Determine if a string starts with a substring, providing a pointer to + * the first character following the substring match. + * + * Eg. s1 = "<10> DPP-RX_CHIRP src=0A:1B:2C:3D:4E:5F hash=..." + * s2 = "DPP-RX-CHIRP " + * strstart(s1, s2) -> true, *out = "src=0A:1B:2C:3D:4E:5F hash=..." + * + * @param s1 The string to check. + * @param s2 The substring to find. + * @param out The output string to hold the first character following the + * occurrence of the substring in s1. + * @return true + * @return false + */ +bool +strstart(const char *s1, const char *s2, const char **out); + +#endif //__STRING_UTILS_H__ diff --git a/src/utils/string_utils.hpp b/src/utils/string_utils.hpp new file mode 100644 index 0000000..4a0d351 --- /dev/null +++ b/src/utils/string_utils.hpp @@ -0,0 +1,102 @@ + +#ifndef __STRING_UTILS_HPP__ +#define __STRING_UTILS_HPP__ + +#include +#include +#include + +/** + * @brief Helper unary predicate function which uses 'unsigned char' as the + * input, as is required by std::isspace. + * + * @param c The value to determine whether it is a character. + * @return true If the value is a character. + * @return false Otherwise. + */ +static inline bool +is_not_space(unsigned char c) +{ + return !std::isspace(c); +} + +/** + * @brief Trims leading whitespace from a string in-place. + * + * @param input The string to trim leading whitespace from. + */ +static inline void +triml(std::string& input) +{ + input.erase(input.begin(), std::find_if(input.begin(), input.end(), is_not_space)); +} + +/** + * @brief Trims trailing whitespace from a string in-place. + * + * @param input The string to trim trailing whitespace from. + */ +static inline void +trimt(std::string& input) +{ + input.erase(std::find_if(input.rbegin(), input.rend(), is_not_space).base(), input.end()); +} + +/** + * @brief Trims leading and trailing whitespace from a string in-place. + * + * @param input The string to trim leading and trailing whitespace from. + */ +static inline void +trim(std::string& input) +{ + triml(input); + trimt(input); +} + +/** + * @brief Trims leading space from a string, and returns a copy. Note that since + * the argument ('input') is passed by value, the original string is copied and + * hence, not modified. + * + * @param input The string to trim leading whitespace from. + * @return std::string A new string with leading whitespace removed. + */ +static inline std::string +triml_copy(std::string input) +{ + triml(input); + return input; +} + +/** + * @brief Trims trailing space from a string, and returns a copy. Note that since + * the argument ('input') is passed by value, the original string is copied and + * hence, not modified. + * + * @param input The string to trim trailing whitespace from. + * @return std::string A new string with trailing whitespace removed. + */ +static inline std::string +trimt_copy(std::string input) +{ + trimt(input); + return input; +} + +/** + * @brief Trims leading and trailing space from a string, and returns a copy. + * Note that since the argument ('input') is passed by value, the original + * string is copied and hence, not modified. + * + * @param input The string to trim leading and trailing whitespace from. + * @return std::string A new string with leading and trailing whitespace removed. + */ +static inline std::string +trim_copy(std::string input) +{ + trim(input); + return input; +} + +#endif // __STRING_UTILS_HPP__ diff --git a/src/utils/time_utils.c b/src/utils/time_utils.c new file mode 100644 index 0000000..e8cbd72 --- /dev/null +++ b/src/utils/time_utils.c @@ -0,0 +1,28 @@ + +#include + +#include "time_utils.h" + +/** + * @brief Determines if a target time is earlier than a reference time. + * + * The time is assumed to be relative to some fixed epoch, and each of the + * timespecs must have been obtained from the same clock source. + * + * The time is also assumed to respect contextual limits. Specifically, it is + * expected that the 'tv_nsec' value does not exceed 1000000000 (the number of + * nanoseconds in a second). + * + * @param target The time to check. + * @param reference The time to compare against. + * @return true If 'target' is earlier than 'reference'. + * @return false If 'target' is later than or equal to 'reference'. + */ +bool +timespec_time_is_earlier(struct timespec *target, struct timespec *reference) +{ + assert((target->tv_nsec < NSEC_PER_SEC) && (reference->tv_nsec < NSEC_PER_SEC)); + + return (target->tv_sec < reference->tv_sec) + || ((target->tv_sec == reference->tv_sec) && (target->tv_nsec < reference->tv_nsec)); +} diff --git a/src/utils/time_utils.h b/src/utils/time_utils.h new file mode 100644 index 0000000..4c477b9 --- /dev/null +++ b/src/utils/time_utils.h @@ -0,0 +1,67 @@ + +#ifndef __TIME_UTILS_H__ +#define __TIME_UTILS_H__ + +#include +#include + +/** + * @brief Timespec related helpers. + */ +#define MSEC_PER_SEC (1000L) +#define NSEC_PER_MSEC (1000000L) +#define NSEC_PER_USEC (1000L) +#define NSEC_PER_SEC (1000000000L) +#define USEC_PER_SEC (1000000L) + +/** + * @brief Helper to compare two struct timespec values. This is copied from + * timercmp, changing the fields to work with timespec instead of timeval. + */ +#define timespeccmp(a, b, CMP) \ + (((a)->tv_sec == (b)->tv_sec) \ + ? ((a)->tv_nsec CMP(b)->tv_nsec) \ + : ((a)->tv_sec CMP(b)->tv_sec)) + +/** + * @brief Calculates and returns the time difference of timespecs. + * + * @param lhs Minuend. + * @param rhs Subtrahend. + * @return struct timespec difference. + */ +static inline struct timespec +timespec_diff(const struct timespec *lhs, const struct timespec *rhs) +{ + struct timespec diff; + + diff.tv_sec = lhs->tv_sec - rhs->tv_sec; + diff.tv_nsec = lhs->tv_nsec - rhs->tv_nsec; + + if (diff.tv_nsec < 0) { + diff.tv_sec--; + diff.tv_nsec += NSEC_PER_SEC; + } + + return diff; +} + +/** + * @brief Determines if a target time is earlier than a reference time. + * + * The time is assumed to be relative to some fixed epoch, and each of the + * timespecs must have been obtained from the same clock source. + * + * The time is also assumed to respect contextual limits. Specifically, it is + * expected that the 'tv_nsec' value does not exceed 1000000000 (the number of + * nanoseconds in a second). + * + * @param target The time to check. + * @param reference The time to compare against. + * @return true If 'target' is earlier than 'reference'. + * @return false If 'target' is later than or equal to 'reference'. + */ +bool +timespec_time_is_earlier(struct timespec *target, struct timespec *reference); + +#endif //__TIME_UTILS_H__ diff --git a/src/utils/userspace/linux/compiler.h b/src/utils/userspace/linux/compiler.h new file mode 100644 index 0000000..fc0b689 --- /dev/null +++ b/src/utils/userspace/linux/compiler.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _TOOLS_LINUX_COMPILER_H_ +#define _TOOLS_LINUX_COMPILER_H_ + +/* Are two types/vars the same type (ignoring qualifiers)? */ +#ifndef __same_type +# define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b)) +#endif + +#define __unused(x) (void)(x) + +#endif /* _TOOLS_LINUX_COMPILER_H */ diff --git a/src/utils/userspace/linux/kernel.h b/src/utils/userspace/linux/kernel.h new file mode 100644 index 0000000..58da44b --- /dev/null +++ b/src/utils/userspace/linux/kernel.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_KERNEL_H +#define _LINUX_KERNEL_H + +#include + +/** + * ARRAY_SIZE - get the number of elements in array @arr + * @arr: array to be sized + */ +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +/** + * container_of - cast a member of a structure out to the containing structure + * @ptr: the pointer to the member. + * @type: the type of the container struct this is embedded in. + * @member: the name of the member within the struct. + */ +#define container_of(ptr, type, member) \ + ((type *)((char *)(ptr) - offsetof(type, member))) + +#endif //_LINUX_KERNEL_H diff --git a/src/utils/userspace/linux/list.h b/src/utils/userspace/linux/list.h new file mode 100644 index 0000000..bb870f0 --- /dev/null +++ b/src/utils/userspace/linux/list.h @@ -0,0 +1,590 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_LIST_H +#define _LINUX_LIST_H + +#include +#include +#include +#include +#include +#include + +/* + * Simple doubly linked list implementation. + * + * Some of the internal functions ("__xxx") are useful when + * manipulating whole lists rather than single entries, as + * sometimes we already know the next/prev entries and we can + * generate better code by using them directly rather than + * using the generic single-entry routines. + */ + +#define LIST_HEAD_INIT(name) { &(name), &(name) } + +#define LIST_HEAD(name) \ + struct list_head name = LIST_HEAD_INIT(name) + +static inline void INIT_LIST_HEAD(struct list_head *list) +{ + list->next = list; + list->prev = list; +} + +/* + * Insert a new entry between two known consecutive entries. + * + * This is only for internal list manipulation where we know + * the prev/next entries already! + */ +static inline void __list_add(struct list_head *elem, + struct list_head *prev, + struct list_head *next) +{ + next->prev = elem; + elem->next = next; + elem->prev = prev; + prev->next = elem; +} + +/** + * list_add - add a new entry + * @elem: new entry to be added + * @head: list head to add it after + * + * Insert a new entry after the specified head. + * This is good for implementing stacks. + */ +static inline void list_add(struct list_head *elem, struct list_head *head) +{ + __list_add(elem, head, head->next); +} + +/** + * list_add_tail - add a new entry + * @elem: new entry to be added + * @head: list head to add it before + * + * Insert a new entry before the specified head. + * This is useful for implementing queues. + */ +static inline void list_add_tail(struct list_head *elem, struct list_head *head) +{ + __list_add(elem, head->prev, head); +} + +/* + * Delete a list entry by making the prev/next entries + * point to each other. + * + * This is only for internal list manipulation where we know + * the prev/next entries already! + */ +static inline void __list_del(struct list_head * prev, struct list_head * next) +{ + next->prev = prev; + prev->next = next; +} + +/** + * list_del - deletes entry from list. + * @entry: the element to delete from the list. + * Note: list_empty() on entry does not return true after this, the entry is + * in an undefined state. + */ +static inline void __list_del_entry(struct list_head *entry) +{ + __list_del(entry->prev, entry->next); +} + +static inline void list_del(struct list_head *entry) +{ + __list_del_entry(entry); + entry->next = (struct list_head *)LIST_POISON1; + entry->prev = (struct list_head *)LIST_POISON2; +} + +/** + * list_replace - replace old entry by new one + * @old : the element to be replaced + * @elem : the new element to insert + * + * If @old was empty, it will be overwritten. + */ +static inline void list_replace(struct list_head *old, + struct list_head *elem) +{ + elem->next = old->next; + elem->next->prev = elem; + elem->prev = old->prev; + elem->prev->next = elem; +} + +static inline void list_replace_init(struct list_head *old, + struct list_head *elem) +{ + list_replace(old, elem); + INIT_LIST_HEAD(old); +} + +/** + * list_del_init - deletes entry from list and reinitialize it. + * @entry: the element to delete from the list. + */ +static inline void list_del_init(struct list_head *entry) +{ + __list_del_entry(entry); + INIT_LIST_HEAD(entry); +} + +/** + * list_move - delete from one list and add as another's head + * @list: the entry to move + * @head: the head that will precede our entry + */ +static inline void list_move(struct list_head *list, struct list_head *head) +{ + __list_del_entry(list); + list_add(list, head); +} + +/** + * list_move_tail - delete from one list and add as another's tail + * @list: the entry to move + * @head: the head that will follow our entry + */ +static inline void list_move_tail(struct list_head *list, + struct list_head *head) +{ + __list_del_entry(list); + list_add_tail(list, head); +} + +/** + * list_is_last - tests whether @list is the last entry in list @head + * @list: the entry to test + * @head: the head of the list + */ +static inline int list_is_last(const struct list_head *list, + const struct list_head *head) +{ + return list->next == head; +} + +/** + * list_empty - tests whether a list is empty + * @head: the list to test. + */ +static inline int list_empty(const struct list_head *head) +{ + return head->next == head; +} + +/** + * list_empty_careful - tests whether a list is empty and not being modified + * @head: the list to test + * + * Description: + * tests whether a list is empty _and_ checks that no other CPU might be + * in the process of modifying either member (next or prev) + * + * NOTE: using list_empty_careful() without synchronization + * can only be safe if the only activity that can happen + * to the list entry is list_del_init(). Eg. it cannot be used + * if another CPU could re-list_add() it. + */ +static inline int list_empty_careful(const struct list_head *head) +{ + struct list_head *next = head->next; + return (next == head) && (next == head->prev); +} + +/** + * list_rotate_left - rotate the list to the left + * @head: the head of the list + */ +static inline void list_rotate_left(struct list_head *head) +{ + struct list_head *first; + + if (!list_empty(head)) { + first = head->next; + list_move_tail(first, head); + } +} + +/** + * list_is_singular - tests whether a list has just one entry. + * @head: the list to test. + */ +static inline int list_is_singular(const struct list_head *head) +{ + return !list_empty(head) && (head->next == head->prev); +} + +static inline void __list_cut_position(struct list_head *list, + struct list_head *head, struct list_head *entry) +{ + struct list_head *new_first = entry->next; + list->next = head->next; + list->next->prev = list; + list->prev = entry; + entry->next = list; + head->next = new_first; + new_first->prev = head; +} + +/** + * list_cut_position - cut a list into two + * @list: a new list to add all removed entries + * @head: a list with entries + * @entry: an entry within head, could be the head itself + * and if so we won't cut the list + * + * This helper moves the initial part of @head, up to and + * including @entry, from @head to @list. You should + * pass on @entry an element you know is on @head. @list + * should be an empty list or a list you do not care about + * losing its data. + */ +static inline void list_cut_position(struct list_head *list, + struct list_head *head, struct list_head *entry) +{ + if (list_empty(head)) + return; + if (list_is_singular(head) && + (head->next != entry && head != entry)) + return; + if (entry == head) + INIT_LIST_HEAD(list); + else + __list_cut_position(list, head, entry); +} + +static inline void __list_splice(const struct list_head *list, + struct list_head *prev, + struct list_head *next) +{ + struct list_head *first = list->next; + struct list_head *last = list->prev; + + first->prev = prev; + prev->next = first; + + last->next = next; + next->prev = last; +} + +/** + * list_splice - join two lists, this is designed for stacks + * @list: the new list to add. + * @head: the place to add it in the first list. + */ +static inline void list_splice(const struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) + __list_splice(list, head, head->next); +} + +/** + * list_splice_tail - join two lists, each list being a queue + * @list: the new list to add. + * @head: the place to add it in the first list. + */ +static inline void list_splice_tail(struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) + __list_splice(list, head->prev, head); +} + +/** + * list_splice_init - join two lists and reinitialise the emptied list. + * @list: the new list to add. + * @head: the place to add it in the first list. + * + * The list at @list is reinitialised + */ +static inline void list_splice_init(struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) { + __list_splice(list, head, head->next); + INIT_LIST_HEAD(list); + } +} + +/** + * list_splice_tail_init - join two lists and reinitialise the emptied list + * @list: the new list to add. + * @head: the place to add it in the first list. + * + * Each of the lists is a queue. + * The list at @list is reinitialised + */ +static inline void list_splice_tail_init(struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) { + __list_splice(list, head->prev, head); + INIT_LIST_HEAD(list); + } +} + +/** + * list_entry - get the struct for this entry + * @ptr: the &struct list_head pointer. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_head within the struct. + */ +#define list_entry(ptr, type, member) \ + container_of(ptr, type, member) + +/** + * list_first_entry - get the first element from a list + * @ptr: the list head to take the element from. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_head within the struct. + * + * Note, that list is expected to be not empty. + */ +#define list_first_entry(ptr, type, member) \ + list_entry((ptr)->next, type, member) + +/** + * list_last_entry - get the last element from a list + * @ptr: the list head to take the element from. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_head within the struct. + * + * Note, that list is expected to be not empty. + */ +#define list_last_entry(ptr, type, member) \ + list_entry((ptr)->prev, type, member) + +/** + * list_first_entry_or_null - get the first element from a list + * @ptr: the list head to take the element from. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_head within the struct. + * + * Note that if the list is empty, it returns NULL. + */ +#define list_first_entry_or_null(ptr, type, member) \ + (!list_empty(ptr) ? list_first_entry(ptr, type, member) : NULL) + +/** + * list_next_entry - get the next element in list + * @pos: the type * to cursor + * @member: the name of the list_head within the struct. + */ +#define list_next_entry(pos, member) \ + list_entry((pos)->member.next, __typeof__(*(pos)), member) + +/** + * list_prev_entry - get the prev element in list + * @pos: the type * to cursor + * @member: the name of the list_head within the struct. + */ +#define list_prev_entry(pos, member) \ + list_entry((pos)->member.prev, __typeof__(*(pos)), member) + +/** + * list_for_each - iterate over a list + * @pos: the &struct list_head to use as a loop cursor. + * @head: the head for your list. + */ +#define list_for_each(pos, head) \ + for (pos = (head)->next; pos != (head); pos = pos->next) + +/** + * list_for_each_prev - iterate over a list backwards + * @pos: the &struct list_head to use as a loop cursor. + * @head: the head for your list. + */ +#define list_for_each_prev(pos, head) \ + for (pos = (head)->prev; pos != (head); pos = pos->prev) + +/** + * list_for_each_safe - iterate over a list safe against removal of list entry + * @pos: the &struct list_head to use as a loop cursor. + * @n: another &struct list_head to use as temporary storage + * @head: the head for your list. + */ +#define list_for_each_safe(pos, n, head) \ + for (pos = (head)->next, n = pos->next; pos != (head); \ + pos = n, n = pos->next) + +/** + * list_for_each_prev_safe - iterate over a list backwards safe against removal of list entry + * @pos: the &struct list_head to use as a loop cursor. + * @n: another &struct list_head to use as temporary storage + * @head: the head for your list. + */ +#define list_for_each_prev_safe(pos, n, head) \ + for (pos = (head)->prev, n = pos->prev; \ + pos != (head); \ + pos = n, n = pos->prev) + +/** + * list_for_each_entry - iterate over list of given type + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + */ +#define list_for_each_entry(pos, head, member) \ + for (pos = list_first_entry(head, __typeof__(*pos), member); \ + &pos->member != (head); \ + pos = list_next_entry(pos, member)) + +/** + * list_for_each_entry_reverse - iterate backwards over list of given type. + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + */ +#define list_for_each_entry_reverse(pos, head, member) \ + for (pos = list_last_entry(head, __typeof__(*pos), member); \ + &pos->member != (head); \ + pos = list_prev_entry(pos, member)) + +/** + * list_prepare_entry - prepare a pos entry for use in list_for_each_entry_continue() + * @pos: the type * to use as a start point + * @head: the head of the list + * @member: the name of the list_head within the struct. + * + * Prepares a pos entry for use as a start point in list_for_each_entry_continue(). + */ +#define list_prepare_entry(pos, head, member) \ + ((pos) ? : list_entry(head, __typeof__(*pos), member)) + +/** + * list_for_each_entry_continue - continue iteration over list of given type + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Continue to iterate over list of given type, continuing after + * the current position. + */ +#define list_for_each_entry_continue(pos, head, member) \ + for (pos = list_next_entry(pos, member); \ + &pos->member != (head); \ + pos = list_next_entry(pos, member)) + +/** + * list_for_each_entry_continue_reverse - iterate backwards from the given point + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Start to iterate over list of given type backwards, continuing after + * the current position. + */ +#define list_for_each_entry_continue_reverse(pos, head, member) \ + for (pos = list_prev_entry(pos, member); \ + &pos->member != (head); \ + pos = list_prev_entry(pos, member)) + +/** + * list_for_each_entry_from - iterate over list of given type from the current point + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate over list of given type, continuing from current position. + */ +#define list_for_each_entry_from(pos, head, member) \ + for (; &pos->member != (head); \ + pos = list_next_entry(pos, member)) + +/** + * list_for_each_entry_from_reverse - iterate backwards over list of given type + * from the current point + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate backwards over list of given type, continuing from current position. + */ +#define list_for_each_entry_from_reverse(pos, head, member) \ + for (; &pos->member != (head); \ + pos = list_prev_entry(pos, member)) + +/** + * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry + * @pos: the type * to use as a loop cursor. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_head within the struct. + */ +#define list_for_each_entry_safe(pos, n, head, member) \ + for (pos = list_first_entry(head, __typeof__(*pos), member), \ + n = list_next_entry(pos, member); \ + &pos->member != (head); \ + pos = n, n = list_next_entry(n, member)) + +/** + * list_for_each_entry_safe_continue - continue list iteration safe against removal + * @pos: the type * to use as a loop cursor. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate over list of given type, continuing after current point, + * safe against removal of list entry. + */ +#define list_for_each_entry_safe_continue(pos, n, head, member) \ + for (pos = list_next_entry(pos, member), \ + n = list_next_entry(pos, member); \ + &pos->member != (head); \ + pos = n, n = list_next_entry(n, member)) + +/** + * list_for_each_entry_safe_from - iterate over list from current point safe against removal + * @pos: the type * to use as a loop cursor. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate over list of given type from current point, safe against + * removal of list entry. + */ +#define list_for_each_entry_safe_from(pos, n, head, member) \ + for (n = list_next_entry(pos, member); \ + &pos->member != (head); \ + pos = n, n = list_next_entry(n, member)) + +/** + * list_for_each_entry_safe_reverse - iterate backwards over list safe against removal + * @pos: the type * to use as a loop cursor. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate backwards over list of given type, safe against removal + * of list entry. + */ +#define list_for_each_entry_safe_reverse(pos, n, head, member) \ + for (pos = list_last_entry(head, __typeof__(*pos), member), \ + n = list_prev_entry(pos, member); \ + &pos->member != (head); \ + pos = n, n = list_prev_entry(n, member)) + +/** + * list_safe_reset_next - reset a stale list_for_each_entry_safe loop + * @pos: the loop cursor used in the list_for_each_entry_safe loop + * @n: temporary storage used in list_for_each_entry_safe + * @member: the name of the list_head within the struct. + * + * list_safe_reset_next is not safe to use in general if the list may be + * modified concurrently (eg. the lock is dropped in the loop body). An + * exception to this is if the cursor element (pos) is pinned in the list, + * and list_safe_reset_next is called after re-taking the lock and before + * completing the current iteration of the loop body. + */ +#define list_safe_reset_next(pos, n, member) \ + n = list_next_entry(pos, member) + +#endif diff --git a/src/utils/userspace/linux/poison.h b/src/utils/userspace/linux/poison.h new file mode 100644 index 0000000..cd0f2c9 --- /dev/null +++ b/src/utils/userspace/linux/poison.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_POISON_H +#define _LINUX_POISON_H + +#include + +#define POISON_POINTER_DELTA 0 + +/* + * These are non-NULL pointers that will result in page faults + * under normal circumstances, used to verify that nobody uses + * non-initialized list entries. + */ +#define LIST_POISON1 ((uint8_t *) 0x100 + POISON_POINTER_DELTA) +#define LIST_POISON2 ((uint8_t *) 0x122 + POISON_POINTER_DELTA) + +#endif //_LINUX_POISON_H diff --git a/src/utils/userspace/linux/types.h b/src/utils/userspace/linux/types.h new file mode 100644 index 0000000..07fb2e7 --- /dev/null +++ b/src/utils/userspace/linux/types.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _TOOLS_LINUX_TYPES_H_ +#define _TOOLS_LINUX_TYPES_H_ + +struct list_head { + struct list_head *next, *prev; +}; + +struct hlist_head { + struct hlist_node *first; +}; + +struct hlist_node { + struct hlist_node *next, **pprev; +}; + +#endif /* _TOOLS_LINUX_TYPES_H_ */ diff --git a/src/utils/ztp_log.h b/src/utils/ztp_log.h new file mode 100644 index 0000000..0be8c96 --- /dev/null +++ b/src/utils/ztp_log.h @@ -0,0 +1,77 @@ + +#ifndef __ZTP_LOG_H__ +#define __ZTP_LOG_H__ + +#include +#include + +/** + * @brief Macros for systemd journal log prefixes. These can be used to prefix + * printf/fprintf format strings which journald will use to catergorize the log + * message. + * + * Eg. printf(SYSTEMD_LOG_PRIORIRT_ALERT "system shutting down!"); + */ +#define SYSTEMD_LOG_PRIORITY_EMERGENCY "<0>" +#define SYSTEMD_LOG_PRIORITY_ALERT "<1>" +#define SYSTEMD_LOG_PRIORITY_CRITICAL "<2>" +#define SYSTEMD_LOG_PRIORITY_ERROR "<3>" +#define SYSTEMD_LOG_PRIORITY_WARNING "<4>" +#define SYSTEMD_LOG_PRIORITY_NOTICE "<5>" +#define SYSTEMD_LOG_PRIORITY_INFORMATIONAL "<6>" +#define SYSTEMD_LOG_PRIORITY_DEBUG "<7>" + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" +#endif //__clang__ + +/** + * @brief Macro to help build component log macros below. + */ +#define __ZLOG__(prio, fmt, ...) sd_journal_print(LOG_##prio, (fmt), ##__VA_ARGS__) + +/** + * @brief Wrappers for systemd journal logging. Must be macros to retain source + * line logging. + */ +#define zlog_panic(fmt, ...) __ZLOG__(EMERG, (fmt), ##__VA_ARGS__) +#define zlog_alert(fmt, ...) __ZLOG__(ALERT, (fmt), ##__VA_ARGS__) +#define zlog_critical(fmt, ...) __ZLOG__(CRIT, (fmt), ##__VA_ARGS__) +#define zlog_error(fmt, ...) __ZLOG__(ERR, (fmt), ##__VA_ARGS__) +#define zlog_warning(fmt, ...) __ZLOG__(WARNING, (fmt), ##__VA_ARGS__) +#define zlog_notice(fmt, ...) __ZLOG__(NOTICE, (fmt), ##__VA_ARGS__) +#define zlog_info(fmt, ...) __ZLOG__(INFO, (fmt), ##__VA_ARGS__) +#define zlog_debug(fmt, ...) __ZLOG__(DEBUG, (fmt), ##__VA_ARGS__) + +/** + * @brief Short-form aliases for above macros. + */ +#define zpanic zlog_panic +#define zalert zlog_alert +#define zcrit zlog_critical +#define zerr zlog_error +#define zwarn zlog_warning +#define znotify zlog_notify +#define zinfo zlog_info +#define zdbg zlog_debug + +/** + * @brief Helper macros for logging interface-specific messages. Will prefix + * each message with '[]'. + */ +#define zlog_if(_prio, _if, _fmt, ...) zlog_##_prio("[%s] " _fmt, _if, ##__VA_ARGS__) +#define zlog_panic_if(_if, _fmt, ...) zlog_if(panic, _if, _fmt, ##__VA_ARGS__) +#define zlog_alert_if(_if, _fmt, ...) zlog_if(alert, _if, _fmt, ##__VA_ARGS__) +#define zlog_critical_if(_if, _fmt, ...) zlog_if(critical, _if, _fmt, ##__VA_ARGS__) +#define zlog_error_if(_if, _fmt, ...) zlog_if(error, _if, _fmt, ##__VA_ARGS__) +#define zlog_warning_if(_if, _fmt, ...) zlog_if(warning, _if, _fmt, ##__VA_ARGS__) +#define zlog_notice_if(_if, _fmt, ...) zlog_if(notice, _if, _fmt, ##__VA_ARGS__) +#define zlog_info_if(_if, _fmt, ...) zlog_if(info, _if, _fmt, ##__VA_ARGS__) +#define zlog_debug_if(_if, _fmt, ...) zlog_if(debug, _if, _fmt, ##__VA_ARGS__) + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif //__clang__ + +#endif //__ZTP_LOG_H__ diff --git a/src/wifi/CMakeLists.txt b/src/wifi/CMakeLists.txt new file mode 100644 index 0000000..9a30539 --- /dev/null +++ b/src/wifi/CMakeLists.txt @@ -0,0 +1,22 @@ + +project(ztpwifi) + +add_library(ztpwifi + STATIC + "" +) + +target_sources(ztpwifi + PRIVATE + dpp.c +) + +target_include_directories(ztpwifi + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../utils +) + +target_link_libraries(ztpwifi + ztputils +) diff --git a/src/wifi/dpp.c b/src/wifi/dpp.c new file mode 100644 index 0000000..ebe4ced --- /dev/null +++ b/src/wifi/dpp.c @@ -0,0 +1,764 @@ + +#include +#include +#include + +#include "dpp.h" +#include "string_utils.h" +#include "ztp_log.h" + +/** + * @brief Parses an integer, converting it to a dpp public action frame type. + * + * @param value The value to parse. + * @return enum dpp_public_action_frame_type The public action frame type + * associated with the value. + */ +enum dpp_public_action_frame_type +dpp_public_action_frame_parse_int(int value) +{ + switch (value) { + case 0: + return DPP_PAF_AUTHENTICATION_REQUEST; + case 1: + return DPP_PAF_AUTHENTICATION_RESPONSE; + case 2: + return DPP_PAF_AUTHENTICATION_CONFIRM; + case 5: + return DPP_PAF_PEER_DISCOVERY_REQUEST; + case 6: + return DPP_PAF_PEER_DISCOVERY_RESPONE; + case 7: + return DPP_PAF_PKEX_V1_EXCHANGE_REQUEST; + case 8: + return DPP_PAF_PKEX_EXCHANGE_RESPONSE; + case 9: + return DPP_PAF_PKEX_REVEAL_REQUEST; + case 10: + return DPP_PAF_PKEX_REVEAL_RESPONSE; + case 11: + return DPP_PAF_CONFIGURATION_RESULT; + case 12: + return DPP_PAF_CONNECTION_STATUS; + case 13: + return DPP_PAF_PRESENCE_ANNOUNCEMENT; + case 14: + return DPP_PAF_RECONFIGURATION_ANNOUNCEMENT; + case 15: + return DPP_PAF_RECONFIGURATION_AUTHENTICATION_REQUEST; + case 16: + return DPP_PAF_RECONFIGURATION_AUTHENTICATION_RESPONSE; + case 17: + return DPP_PAF_RECONFIGURATION_AUTHENTICATION_CONFIRM; + case 18: + return DPP_PAF_PKEX_EXCHANGE_REQUEST; + default: + return DPP_PAF_INVALID; + } +} + +/** + * @brief Converts a dpp public action frame type to a string. + * + * @param type The public actio nframe type to parse. + * @return const char* The string representation of the type. + */ +const char * +dpp_public_action_frame_str(enum dpp_public_action_frame_type type) +{ + switch (type) { + case DPP_PAF_AUTHENTICATION_REQUEST: + return "auth-request"; + case DPP_PAF_AUTHENTICATION_RESPONSE: + return "auth-response"; + case DPP_PAF_AUTHENTICATION_CONFIRM: + return "auth-confirm"; + case DPP_PAF_PEER_DISCOVERY_REQUEST: + return "peer-discovery-request"; + case DPP_PAF_PEER_DISCOVERY_RESPONE: + return "peer-discovery-response"; + case DPP_PAF_PKEX_V1_EXCHANGE_REQUEST: + return "pkex-exchange-request-v1"; + case DPP_PAF_PKEX_EXCHANGE_RESPONSE: + return "pkex-exchange-response"; + case DPP_PAF_PKEX_REVEAL_REQUEST: + return "pkex-reveal-request"; + case DPP_PAF_PKEX_REVEAL_RESPONSE: + return "pkex-reveal-response"; + case DPP_PAF_CONFIGURATION_RESULT: + return "config-result"; + case DPP_PAF_CONNECTION_STATUS: + return "connection-status"; + case DPP_PAF_PRESENCE_ANNOUNCEMENT: + return "presence-announce"; + case DPP_PAF_RECONFIGURATION_ANNOUNCEMENT: + return "reconfig-auth-announce"; + case DPP_PAF_RECONFIGURATION_AUTHENTICATION_REQUEST: + return "reconfig-auth-request"; + case DPP_PAF_RECONFIGURATION_AUTHENTICATION_RESPONSE: + return "reconfig-auth-response"; + case DPP_PAF_RECONFIGURATION_AUTHENTICATION_CONFIRM: + return "reconfig-auth-confirm"; + case DPP_PAF_PKEX_EXCHANGE_REQUEST: + return "pkex-exchange-request"; + case DPP_PAF_INVALID: + default: + return "invalid"; + } +} + +/** + * @brief Parses a string and converts it to a dpp device role. + * + * @param str The string to parse. + * @return enum dpp_device_role + */ +enum dpp_device_role +dpp_device_role_parse(const char *str) +{ + if (strcmp(str, "enrollee") == 0) { + return DPP_DEVICE_ROLE_ENROLLEE; + } else if (strcmp(str, "configurator") == 0) { + return DPP_DEVICE_ROLE_CONFIGURATOR; + } else { + return DPP_DEVICE_ROLE_UNKNOWN; + } +} + +/** + * @brief Determines if the specified device role is valid. + * + * @param role The role to check. + * @return true If the device role is valid. + * @return false If the device role is invalid. + */ +bool +dpp_device_role_is_valid(enum dpp_device_role role) +{ + switch (role) { + case DPP_DEVICE_ROLE_ENROLLEE: + case DPP_DEVICE_ROLE_CONFIGURATOR: + return true; + default: + return false; + } +} + +/** + * @brief Returns the peer role. + * + * @param role The role to get the peer for. + * @return enum dpp_device_role The peer role. + */ +enum dpp_device_role +dpp_device_role_peer(enum dpp_device_role role) +{ + switch (role) { + case DPP_DEVICE_ROLE_ENROLLEE: + return DPP_DEVICE_ROLE_CONFIGURATOR; + case DPP_DEVICE_ROLE_CONFIGURATOR: + return DPP_DEVICE_ROLE_ENROLLEE; + default: + return DPP_DEVICE_ROLE_UNKNOWN; + } +} + +/** + * @brief Converts a dpp device role to a string. + * + * @return const char* A string representation of the role. + */ +const char * +dpp_device_role_str(enum dpp_device_role role) +{ + switch (role) { + case DPP_DEVICE_ROLE_ENROLLEE: + return "enrollee"; + case DPP_DEVICE_ROLE_CONFIGURATOR: + return "configurator"; + default: + return "??"; + } +} + +/** + * @brief Parses a string and converts it to a dpp network role. + * + * @param str The string to parse. + * @return enum dpp_network_role The network role corresponding to the string, + * if it is valid. Otherwise DPP_NETWORK_ROLE_UNKNOWN is returned. + */ +enum dpp_network_role +dpp_network_role_parse(const char *str) +{ + if (strcmp(str, "sta") == 0 || strcmp(str, "station") == 0) { + return DPP_NETWORK_ROLE_STATION; + } else if (strcmp(str, "ap") == 0 || strcmp(str, "accesspoint") == 0) { + return DPP_NETWORK_ROLE_AP; + } else if (strcmp(str, "configurator") == 0) { + return DPP_NETWORK_ROLE_CONFIGURATOR; + } else { + return DPP_NETWORK_ROLE_UNKNOWN; + } +} + +/** + * @brief Convert a dpp network role to a string. + * + * The values here must be defined as specified in the Device Provisioning + * Protocol Specification, section 4.4, Table 6, 'DPP Configuration Request + * object. + * + * @param role The role to convert. + * @return const char* A string representation of the network role. + */ +const char * +dpp_network_role_str(enum dpp_network_role role) +{ + switch (role) { + case DPP_NETWORK_ROLE_STATION: + return "sta"; + case DPP_NETWORK_ROLE_AP: + return "ap"; + case DPP_NETWORK_ROLE_CONFIGURATOR: + return "configurator"; + default: + return "??"; + } +} + +/** + * @brief Parses a string and converts it to a dpp_state. + * + * @param state The string to parse. + * @return enum dpp_state The corresponding dpp_state. Returns + * DPP_STATS_UNKNOWN for invalid and unknown input strings. + */ +enum dpp_state +parse_dpp_state(const char *state) +{ + if (strcmp(state, "inactive") == 0) { + return DPP_STATE_INACTIVE; + } else if (strcmp(state, "terminated") == 0) { + return DPP_STATE_TERMINATED; + } else if (strcmp(state, "presence_announce") == 0) { + return DPP_STATE_CHIRPING; + } else if (strcmp(state, "provisioning") == 0) { + return DPP_STATE_PROVISIONING; + } else if (strcmp(state, "bootstrap_key_acquiring") == 0) { + return DPP_STATE_BOOTSTRAP_KEY_ACQUIRING; + } else if (strcmp(state, "bootstrapped") == 0) { + return DPP_STATE_BOOTSTRAPPED; + } else if (strcmp(state, "authenticating") == 0) { + return DPP_STATE_AUTHENTICATING; + } else if (strcmp(state, "authenticated") == 0) { + return DPP_STATE_AUTHENTICATED; + } else if (strcmp(state, "provisioned") == 0) { + return DPP_STATE_PROVISIONED; + } else { + return DPP_STATE_UNKNOWN; + } +} + +/** + * @brief Converts a dpp state into a string. + * + * @param dpp_state The dpp state to convert. + * @return const char* A string representation of the state. + */ +const char * +dpp_state_str(enum dpp_state dpp_state) +{ + switch (dpp_state) { + case DPP_STATE_INACTIVE: + return "inactive"; + case DPP_STATE_TERMINATED: + return "terminated"; + case DPP_STATE_CHIRPING: + return "presence_announce"; + case DPP_STATE_PROVISIONING: + return "provisioning"; + case DPP_STATE_BOOTSTRAP_KEY_ACQUIRING: + return "bootstrap_key_acquiring"; + case DPP_STATE_BOOTSTRAPPED: + return "bootstrapped"; + case DPP_STATE_AUTHENTICATING: + return "authenticating"; + case DPP_STATE_AUTHENTICATED: + return "authenticated"; + case DPP_STATE_PROVISIONED: + return "provisioned"; + case DPP_STATE_UNKNOWN: + default: + return "??"; + } +} + +/** + * @brief Parses a string and converts it to a dpp_bootstrap_type. + * + * @param str The string to parse. + * @return enum dpp_bootstrap_type The corresponding type. Returns + * DPP_BOOTSTRAP_TYPE_UNKNOWN for invalid and unknown input strings. + */ +enum dpp_bootstrap_type +parse_dpp_bootstrap_type(const char *str) +{ + if (strcmp(str, "qrcode") == 0) { + return DPP_BOOTSTRAP_QRCODE; + } else if (strcmp(str, "pkex") == 0) { + return DPP_BOOTSTRAP_PKEX; + } else if (strcmp(str, "nfc") == 0) { + return DPP_BOOTSTRAP_NFC; + } else if (strcmp(str, "ble") == 0 || strcmp(str, "bluetooth") == 0) { + return DPP_BOOTSTRAP_BLE; + } else if (strcmp(str, "cloud") == 0) { + return DPP_BOOTSTRAP_CLOUD; + } else { + return DPP_BOOTSTRAP_UNKNOWN; + } +} + +/** + * @brief Converts a dpp bootstrap type into a string. + * + * @param dpp_bootstrap_type The dpp bootstrap type to convert. + * @return const char* A string represtation of the type. + */ +const char * +dpp_bootstrap_type_str(enum dpp_bootstrap_type dpp_bootstrap_type) +{ + switch (dpp_bootstrap_type) { + case DPP_BOOTSTRAP_QRCODE: + return "qrcode"; + case DPP_BOOTSTRAP_PKEX: + return "pkex"; + case DPP_BOOTSTRAP_NFC: + return "nfc"; + case DPP_BOOTSTRAP_BLE: + return "bluetooth"; + case DPP_BOOTSTRAP_CLOUD: + return "cloud"; + case DPP_BOOTSTRAP_UNKNOWN: + default: + return "??"; + } +} + +/** + * @brief Determines if DPP provisioning is in progress based on the DPP state. + * + * @param dpp_state The state to check. + * @return true If the state reflects that DPP provisioning is in progress. + * @return false If the state reflects that DPP provisioning is not in progress. + */ +bool +is_dpp_provisioning_in_progress(enum dpp_state dpp_state) +{ + switch (dpp_state) { + case DPP_STATE_CHIRPING: + case DPP_STATE_PROVISIONING: + case DPP_STATE_BOOTSTRAP_KEY_ACQUIRING: + case DPP_STATE_BOOTSTRAPPED: + case DPP_STATE_AUTHENTICATING: + case DPP_STATE_AUTHENTICATED: + return true; + default: + return false; + } +} + +/** + * @brief Parses a string and converts it to a dpp akm. + * + * @param str The string to parse. + * @return enum dpp_akm The role corresponding with the specified string. + */ +enum dpp_akm +parse_dpp_akm(const char *str) +{ + if (strcmp(str, "psk") == 0) { + return DPP_AKM_PSK; + } else if (strcmp(str, "sae") == 0) { + return DPP_AKM_SAE; + } else if (strcmp(str, "dpp") == 0) { + return DPP_AKM_DPP; + } else if (strcmp(str, "dot1x") == 0) { + return DPP_AKM_DOT1X; + } else { + return DPP_AKM_INVALID; + } +} + +/** + * @brief Converts a dpp akm to a string. + * + * @param akm The akm to convert. + * @return const char* A string representing the dpp akm. + */ +const char * +dpp_akm_str(enum dpp_akm akm) +{ + switch (akm) { + case DPP_AKM_PSK: + return "psk"; + case DPP_AKM_SAE: + return "sae"; + case DPP_AKM_DPP: + return "dpp"; + case DPP_AKM_DOT1X: + return "dot1x"; + default: + return "??"; + } +} + +/** + * @brief Parses a string and converts it to a dpp psk credential type. + * + * @param str The strong to parse. + * @return enum dpp_psk_credential_type The psk credential type corresponding + * to the string. + */ +enum dpp_psk_credential_type +parse_dpp_psk_credential_type(const char *str) +{ + if (strcmp(str, "passphrase") == 0) { + return PSK_CREDENTIAL_TYPE_PASSPHRASE; + } else if (strcmp(str, "psk") == 0) { + return PSK_CREDENTIAL_TYPE_PSK; + } else { + return PSK_CREDENTIAL_TYPE_INVALID; + } +} + +/** + * @brief Converts a dpp psk credential type to a string. + * + * @param type The psk credential type to convert. + * @return const char* A string representing the psk credential type. + */ +const char * +dpp_psk_credential_type_str(const enum dpp_psk_credential_type type) +{ + switch (type) { + case PSK_CREDENTIAL_TYPE_PASSPHRASE: + return "passphrase"; + case PSK_CREDENTIAL_TYPE_PSK: + return "psk"; + default: + return "??"; + } +} + +/** + * @brief Allocates and initializes a new network credential object. The + * initial state describes an invalid network credential. It must be filled in + * to be made valid. + * + * @return struct dpp_network_credential* + */ +struct dpp_network_credential * +dpp_network_credential_alloc(void) +{ + struct dpp_network_credential *credential = calloc(1, sizeof *credential); + if (!credential) + return NULL; + + INIT_LIST_HEAD(&credential->list); + credential->akm = DPP_AKM_INVALID; + + return credential; +} + +/** + * @brief Sets a passphrase for the credential. + * + * @param credential The psk credential to set the passphrase for. + * @param passphrase The passphrase to set. + * @return int 0 if the passphrase was successfully set. -ERANGE if the + * passphrase was outside of the allowed bounds. + */ +int +dpp_credential_psk_set_passphrase(struct dpp_network_credential_psk *credential, const char *passphrase) +{ + size_t length = strlen(passphrase); + if (length < DPP_PASSPHRASE_LENGTH_MIN || length > DPP_PASSPHRASE_LENGTH_MAX) { + zlog_warning("psk credential passphrase length (%lu) out of bounds [%u, %u]", length, DPP_PASSPHRASE_LENGTH_MIN, DPP_PASSPHRASE_LENGTH_MAX); + return -ERANGE; + } + + hex_encode((const uint8_t *)passphrase, length, credential->passphrase.hex, sizeof credential->passphrase.hex); + memcpy(credential->passphrase.ascii, passphrase, length + 1); + credential->passphrase.length = length; + + return 0; +} + +/** + * @brief Sets a pre-shared key for the credential. + * + * @param credential The psk credential to set the psk for. + * @param key_hex The hex encoded pre-shared key. + * @return int 0 if the key was successfully set. -EINVAL if the key was + * invalid, -ERANGE if the key was outside the allowed bounds. + */ +int +dpp_credential_psk_set_key(struct dpp_network_credential_psk *credential, const char *key_hex) +{ + if (hex_decode(key_hex, credential->key.buffer, sizeof credential->key.buffer) < 0) { + zlog_error("invalid pre-shared key value specified"); + return -EINVAL; + } + + size_t key_length = strlen(key_hex); + if (key_length > (sizeof credential->key.buffer * 2)) { + zlog_error("invalid pre-shared key length"); + return -ERANGE; + } + + memcpy(credential->key.hex, key_hex, (sizeof credential->key.buffer * 2) + 1); + + return 0; +} + +/** + * @brief Sets a passphrase for the credential. + * + * @param credential The sae credential to set the passphrase for. + * @param passphrase The passphrase to set. + * @return int 0 if the passphrase was successfully set, non-zero otherwise. + */ +int +dpp_credential_sae_set_passphrase(struct dpp_network_credential_sae *credential, const char *passphrase) +{ + size_t length = strlen(passphrase); + size_t length_hex = length * 2; + + char *ptr = malloc((length + 1) + (length_hex + 1)); + if (!ptr) { + zlog_error("failed to allocate memory for sae credential passphrase"); + return -ENOMEM; + } + + char *passphrase_hex = ptr + length + 1; + + credential->passphrase = ptr; + credential->passphrase_hex = (ptr + length + 1); + memcpy(credential->passphrase, passphrase, length + 1); + hex_encode((const uint8_t *)passphrase, length, passphrase_hex, length_hex + 1); + + return 0; +} + +/** + * @brief Determines if a sae-based network credential is valid. + * + * @param sae The credential to check. + * @return true If the credential describes a valid sae. + * @return false If the credential is invalid. + */ +static bool +dpp_credential_sae_is_valid(const struct dpp_network_credential_sae *sae) +{ + return (sae->passphrase && strlen(sae->passphrase) > 0); +} + +/** + * @brief Uninitializes an sae credential. + * + * @param sae The sae-based credential to uninitialize. + */ +static void +dpp_credential_sae_uninitialize(struct dpp_network_credential_sae *sae) +{ + if (sae->passphrase) { + free(sae->passphrase); + sae->passphrase = NULL; + sae->passphrase_hex = NULL; + } +} + +/** + * @brief Determines if a psk-based network credential is valid. + * + * @param credential The credential to check. + * @return true If the credential describes a valid psk. + * @return false If the credential is invalid. + */ +static bool +dpp_credential_psk_is_valid(const struct dpp_network_credential_psk *credential) +{ + switch (credential->type) { + case PSK_CREDENTIAL_TYPE_PASSPHRASE: { + if (credential->passphrase.length == 0) { + zlog_error("psk credential missing passphrase"); + return false; + } else if (credential->passphrase.length < DPP_PASSPHRASE_LENGTH_MIN || credential->passphrase.length > DPP_PASSPHRASE_LENGTH_MAX) { + zlog_error("psk credential passphrase length invalid (%u <= passphrase <= %u)", DPP_PASSPHRASE_LENGTH_MIN, DPP_PASSPHRASE_LENGTH_MAX); + return false; + } else if (strlen(credential->passphrase.hex) != credential->passphrase.length * 2) { + zlog_error("psk credential has missing or invalid passphrase hex encoding"); + return false; + } + break; + } + case PSK_CREDENTIAL_TYPE_PSK: { + if (strlen(credential->key.hex) != sizeof credential->key.buffer * 2) { + zlog_error("psk credential has missing or invalid psk hex encoding"); + return false; + } + break; + } + default: + zlog_error("psk credential has invalid type"); + return false; + } + + return true; +} + +/** + * @brief Determines if a network credential is valid. + * + * @param credential The credential to check. + * @return true If the credential is valid. + * @return false If the credential is invalid. + */ +bool +dpp_network_credential_is_valid(const struct dpp_network_credential *credential) +{ + switch (credential->akm) { + case DPP_AKM_PSK: + return dpp_credential_psk_is_valid(&credential->psk); + case DPP_AKM_SAE: + return dpp_credential_sae_is_valid(&credential->sae); + case DPP_AKM_INVALID: + default: + zlog_error("credential has invalid akm"); + return false; + } +} + +/** + * @brief Uninitializes a dpp network credential, releasing any owned resources. + * + * @param credential The credential to uninitialize. + */ +void +dpp_network_credential_uninitialize(struct dpp_network_credential *credential) +{ + switch (credential->akm) { + case DPP_AKM_SAE: + dpp_credential_sae_uninitialize(&credential->sae); + break; + default: + break; + } + + if (!list_empty(&credential->list)) + list_del(&credential->list); +} + +/** + * @brief Allocates and initializes a new dpp network object. + * + * @return struct dpp_network + */ +struct dpp_network * +dpp_network_alloc(void) +{ + struct dpp_network *network = calloc(1, sizeof *network); + if (!network) + return NULL; + + INIT_LIST_HEAD(&network->credentials); + return network; +} + +/** + * @brief Uninitializes a dpp network, releasing any owned resources. If the + * network is part of a list, it is removed from that list. + * + * @param network The network to uninitialize. + */ +void +dpp_network_uninitialize(struct dpp_network *network) +{ + if (!list_empty(&network->credentials)) { + struct dpp_network_credential *credential; + struct dpp_network_credential *credentialtmp; + list_for_each_entry_safe (credential, credentialtmp, &network->credentials, list) { + dpp_network_credential_uninitialize(credential); + } + } +} + +/** + * @brief Adds a new credential to the network. + * + * @param network The network to add the credential to. + * @param credential The credential to add. + */ +void +dpp_network_add_credential(struct dpp_network *network, struct dpp_network_credential *credential) +{ + list_add(&credential->list, &network->credentials); +} + +/** + * @brief Determines if a DPP network structure is valid. + * + * @param network The network to validate. + * @return true If the network described is valid. + * @return false Otherwise. + */ +bool +dpp_network_is_valid(const struct dpp_network *network) +{ + if (list_empty(&network->credentials)) { + zlog_error("network missing credentials"); + return false; + } else if (network->discovery.ssid_length == 0) { + zlog_error("network missing ssid"); + return false; + } + + struct dpp_network_credential *credential; + list_for_each_entry (credential, &network->credentials, list) { + if (!dpp_network_credential_is_valid(credential)) { + zlog_error("network has invalid credential"); + return false; + } + } + + return true; +} + +/** + * @brief Uninitializes a dpp bootstrap info structure, releasing any owned + * resources. + * + * @param bi The bootstrap info structure to uninitialize. + */ +void +dpp_bootstrap_info_uninitialize(struct dpp_bootstrap_info *bi) +{ + char **strs[] = { + &bi->channel, + &bi->curve, + &bi->engine_id, + &bi->engine_path, + &bi->info, + &bi->key, + &bi->key_id, + &bi->mac, + }; + + for (size_t i = 0; i < ARRAY_SIZE(strs); i++) { + if (*strs[i]) { + free(*strs[i]); + *strs[i] = NULL; + } + } +} diff --git a/src/wifi/dpp.h b/src/wifi/dpp.h new file mode 100644 index 0000000..55ce5cc --- /dev/null +++ b/src/wifi/dpp.h @@ -0,0 +1,543 @@ + +#ifndef __DPP_H__ +#define __DPP_H__ + +#include +#include + +#include + +/** + * @brief The digest length of a SHA256 hash. + */ +#define SHA256_HASH_LENGTH 32 + +/** + * @brief Macro defining the length of the DPP bootstrapping public key hash. + * See Device Provisioning Protocol Specification, version 1.2.5, section + * 6.3.3, 'DPP authentication response'. + */ +#define DPP_BOOTSTRAP_PUBKEY_HASH_LENGTH SHA256_HASH_LENGTH + +/** + * @brief Macro defining the length of a WiFi hardware address, or MAC. + */ +#define DPP_MAC_LENGTH 6 + +/** + * @brief The maximum length of an SSID (as used by DPP). + */ +#define DPP_SSID_LENGTH_MAX 32 + +/** + * @brief DPP Public Action Frame Type. + * @see Wi-Fi EasyConnect Specification v2.0, Table 31. + */ +enum dpp_public_action_frame_type { + DPP_PAF_AUTHENTICATION_REQUEST = 0, + DPP_PAF_AUTHENTICATION_RESPONSE = 1, + DPP_PAF_AUTHENTICATION_CONFIRM = 2, + DPP_PAF_PEER_DISCOVERY_REQUEST = 5, + DPP_PAF_PEER_DISCOVERY_RESPONE = 6, + DPP_PAF_PKEX_V1_EXCHANGE_REQUEST = 7, + DPP_PAF_PKEX_EXCHANGE_RESPONSE = 8, + DPP_PAF_PKEX_REVEAL_REQUEST = 9, + DPP_PAF_PKEX_REVEAL_RESPONSE = 10, + DPP_PAF_CONFIGURATION_RESULT = 11, + DPP_PAF_CONNECTION_STATUS = 12, + DPP_PAF_PRESENCE_ANNOUNCEMENT = 13, + DPP_PAF_RECONFIGURATION_ANNOUNCEMENT = 14, + DPP_PAF_RECONFIGURATION_AUTHENTICATION_REQUEST = 15, + DPP_PAF_RECONFIGURATION_AUTHENTICATION_RESPONSE = 16, + DPP_PAF_RECONFIGURATION_AUTHENTICATION_CONFIRM = 17, + DPP_PAF_PKEX_EXCHANGE_REQUEST = 18, + DPP_PAF_INVALID = 256, +}; + +/** + * @brief Parses an integer, converting it to a dpp public action frame type. + * + * @param value The value to parse. + * @return enum dpp_public_action_frame_type The public action frame type + * associated with the value. + */ +enum dpp_public_action_frame_type +dpp_public_action_frame_parse_int(int value); + +/** + * @brief Converts a dpp public action frame type to a string. + * + * @param type The public actio nframe type to parse. + * @return const char* The string representation of the type. + */ +const char * +dpp_public_action_frame_str(enum dpp_public_action_frame_type type); + +/** + * @brief Describes the DPP device role. Note that this is distinct from the + * DPP network role. + */ +enum dpp_device_role { + DPP_DEVICE_ROLE_UNKNOWN = 0, + DPP_DEVICE_ROLE_ENROLLEE, + DPP_DEVICE_ROLE_CONFIGURATOR, + DPP_DEVICE_ROLE_COUNT, +}; + +/** + * @brief Parses a string and converts it to a dpp device role. + * + * @param str The string to parse. + * @return enum dpp_device_role + */ +enum dpp_device_role +dpp_device_role_parse(const char *str); + +/** + * @brief Determines if the specified device role is valid. + * + * @param role The role to check. + * @return true If the device role is valid. + * @return false If the device role is invalid. + */ +bool +dpp_device_role_is_valid(enum dpp_device_role role); + +/** + * @brief Returns the peer role. + * + * @param role The role to get the peer for. + * @return enum dpp_device_role The peer role. + */ +enum dpp_device_role +dpp_device_role_peer(enum dpp_device_role role); + +/** + * @brief Converts a dpp device role to a string. + * + * @return const char* A string representation of the role. + */ +const char * +dpp_device_role_str(enum dpp_device_role role); + +/** + * @brief DPP Network role, describing the role/function of an enrollee device. + */ +enum dpp_network_role { + DPP_NETWORK_ROLE_UNKNOWN = 0, + DPP_NETWORK_ROLE_STATION, + DPP_NETWORK_ROLE_AP, + DPP_NETWORK_ROLE_CONFIGURATOR, +}; + +/** + * @brief Parses a string and converts it to a dpp network role. + * + * @param str The string to parse. + * @return enum dpp_network_role The network role corresponding to the string, + * if it is valid. Otherwise DPP_NETWORK_ROLE_UNKNOWN is returned. + */ +enum dpp_network_role +dpp_network_role_parse(const char *str); + +/** + * @brief Convert a dpp network role to a string. + * + * @param role The role to convert. + * @return const char* A string representation of the network role. + */ +const char * +dpp_network_role_str(enum dpp_network_role role); + +/** + * @brief Device provisioning protocol (DPP) device state. This applies to the + * overall process and so can be applied to an enrollee or a configurator. Some + * states will never be reported for certain roles. For example, + * DPP_STATE_CHIRPING will never be attributed to a device with + * role=configurator. + */ +enum dpp_state { + DPP_STATE_INACTIVE, + DPP_STATE_TERMINATED, + DPP_STATE_UNKNOWN, + DPP_STATE_CHIRPING, + DPP_STATE_PROVISIONING, + DPP_STATE_BOOTSTRAP_KEY_ACQUIRING, + DPP_STATE_BOOTSTRAPPED, + DPP_STATE_AUTHENTICATING, + DPP_STATE_AUTHENTICATED, + DPP_STATE_PROVISIONED, +}; + +/** + * @brief Parses a string and converts it to a dpp_state. + * + * @param str The string to parse. + * @return enum dpp_state The corresponding dpp_state. Returns + * DPP_STATE_UNKNOWN for invalid and unknown input strings. + */ +enum dpp_state +parse_dpp_state(const char *str); + +/** + * @brief Converts a dpp state into a string. + * + * @param dpp_state The dpp state to convert. + * @return const char* A string representation of the state. + */ +const char * +dpp_state_str(enum dpp_state dpp_state); + +/** + * @brief Describes DPP bootstrapping methods. + */ +enum dpp_bootstrap_type { + DPP_BOOTSTRAP_UNKNOWN = 0, + DPP_BOOTSTRAP_QRCODE, + DPP_BOOTSTRAP_PKEX, + DPP_BOOTSTRAP_NFC, + DPP_BOOTSTRAP_BLE, + DPP_BOOTSTRAP_CLOUD, + DPP_BOOTSTRAP_COUNT, +}; + +/** + * @brief Parses a string and converts it to a dpp_bootstrap_type. + * + * @param str The string to parse. + * @return enum dpp_bootstrap_type The corresponding type. Returns + * DPP_BOOTSTRAP_TYPE_UNKNOWN for invalid and unknown input strings. + */ +enum dpp_bootstrap_type +parse_dpp_bootstrap_type(const char *str); + +/** + * @brief Converts a dpp bootstrap type into a string. + * + * @param dpp_bootstrap_type The dpp bootstrap type to convert. + * @return const char* A string represtation of the type. + */ +const char * +dpp_bootstrap_type_str(enum dpp_bootstrap_type dpp_bootstrap_type); + +/** + * @brief Determines if DPP provisioning is in progress based on the DPP state. + * + * @param dpp_state The state to check. + * @return true If the state reflects that DPP provisioning is in progress. + * @return false If the state reflects that DPP provisioning is not in progress. + */ +bool +is_dpp_provisioning_in_progress(enum dpp_state dpp_state); + +/** + * @brief Describes dpp bootstrapping information. + */ +struct dpp_bootstrap_info { + enum dpp_bootstrap_type type; + char *channel; + char *curve; + char *mac; + char *info; + char *key; + char *key_id; + char *engine_id; + char *engine_path; +}; + +/** + * @brief Uninitializes a dpp bootstrap info structure, releasing any owned + * resources. + * + * @param bi The bootstrap info structure to uninitialize. + */ +void +dpp_bootstrap_info_uninitialize(struct dpp_bootstrap_info *bi); + +/** + * @brief Represents a binary dpp bootstrapping key. + * + * This is a DER-encoded ASN.1 SubjectPublicKeyInfo data structure as defined + * in RFC 5280,Internet X.509 Public Key Infrastructure Certificate and + * Certificate Revocation List (CRL) Profile + */ +struct dpp_bootstrap_publickey { + uint8_t *data; + size_t length; +}; + +/** + * @brief Represents a cryptographic hash of a dpp bootstrapping key. + */ +struct dpp_bootstrap_publickey_hash { + uint8_t data[DPP_BOOTSTRAP_PUBKEY_HASH_LENGTH]; +}; + +/** + * @brief Helper macro for printing a public key hash as hex. + */ +#define DPP_BOOTSTRAP_PUBLICKEY_HASH_FMT \ + "%02x%02x%02x%02x%02x%02x%02x%02x" \ + "%02x%02x%02x%02x%02x%02x%02x%02x" \ + "%02x%02x%02x%02x%02x%02x%02x%02x" \ + "%02x%02x%02x%02x%02x%02x%02x%02x" + +/** + * @brief Helper macro to convert public key buffer to hex string. + */ +#define DPP_BOOTSTRAP_PUBLICKEY_HASH_TOSTRING(a) \ + a[ 0], a[ 1], a[ 2], a[ 3], a[ 4], a[ 5], a[ 6], a[ 7], \ + a[ 8], a[ 9], a[10], a[11], a[12], a[13], a[14], a[15], \ + a[16], a[17], a[18], a[19], a[20], a[21], a[22], a[23], \ + a[24], a[25], a[26], a[27], a[28], a[29], a[30], a[31] + +/** + * @brief Helper macro for printing a shortened (7-byte) public key hash as + * hex. + */ +#define DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_FMT \ + "%02x%02x%02x%02x%02x%02x%02x" + +/** + * @brief Helper macro to convert a shortened (7-byte) public key buffer to hex + * string. + */ +#define DPP_BOOTSTRAP_PUBLICKEY_HASH_SHORT_TOSTRING(a) \ + a[0], a[1], a[2], a[3], a[4], a[5], a[6] + +/** + * @brief WiFi Authentication and Key Management (AKM). + */ +enum dpp_akm { + DPP_AKM_INVALID = 0, + DPP_AKM_PSK, + DPP_AKM_SAE, + DPP_AKM_DPP, + DPP_AKM_DOT1X, + DPP_AKM_COUNT, +}; + +/** + * @brief Converts a string to a wifi akm enumeration value. + * + * @param str + * @return enum wifi_akm The wifi akm corresponding to the specified string, if + * it exists. Otherwise, AKM_COUNT is returned. + */ +enum dpp_akm +parse_dpp_akm(const char *str); + +/** + * @brief Converts a dpp akm to a string. + * + * @param akm The akm to convert. + * @return const char* A string representing the dpp akm. + */ +const char * +dpp_akm_str(enum dpp_akm akm); + +/** + * @brief DPP network discovery information. + */ +struct dpp_network_discovery { + uint8_t ssid[DPP_SSID_LENGTH_MAX]; + size_t ssid_length; + int32_t ssid_charset; +}; + +/** + * @brief Type of data for pre-shared key (DPP_AKM_PSK) authentication. + */ +enum dpp_psk_credential_type { + PSK_CREDENTIAL_TYPE_PSK, + PSK_CREDENTIAL_TYPE_PASSPHRASE, + PSK_CREDENTIAL_TYPE_INVALID +}; + +/** + * @brief Parses a string and converts it to a dpp psk credential type. + * + * @param str The strong to parse. + * @return enum dpp_psk_credential_type The psk credential type corresponding + * to the string. + */ +enum dpp_psk_credential_type +parse_dpp_psk_credential_type(const char *str); + +/** + * @brief Converts a dpp psk credential type to a string. + * + * @param type The psk credential type to convert. + * @return const char* A string representing the psk credential type. + */ +const char * +dpp_psk_credential_type_str(const enum dpp_psk_credential_type type); + +/** + * @brief Maximum length (bytes) of a pre-shared key (psk). + */ +#define DPP_PSK_LENGTH_MAX 32 + +/** + * @brief Minimum length (ASCII characters) of a passphrase. + */ +#define DPP_PASSPHRASE_LENGTH_MIN 8 + +/** + * @brief Maximum length (ASCII characters) of a passphrase. + */ +#define DPP_PASSPHRASE_LENGTH_MAX 63 + +/** + * @brief Represents a PSK (raw) key. + */ +struct dpp_network_credential_psk_key { + uint8_t buffer[DPP_PSK_LENGTH_MAX]; + char hex[(DPP_PSK_LENGTH_MAX * 2) + 1]; +}; + +/** + * @brief Represents a PSK (ascii) passphrase. + */ +struct dpp_network_credential_psk_passphrase { + size_t length; + char ascii[DPP_PASSPHRASE_LENGTH_MAX + 1]; + char hex[(DPP_PASSPHRASE_LENGTH_MAX * 2) + 1]; +}; + +/** + * @brief Credential type for pre-shared key (DPP_AKM_PSK) authentication. + */ +struct dpp_network_credential_psk { + enum dpp_psk_credential_type type; + union { + struct dpp_network_credential_psk_passphrase passphrase; + struct dpp_network_credential_psk_key key; + }; +}; + +/** + * @brief Credential type for SAE (DPP_AKM_SAE) authentication. The + * passphrase/password is similar to the one used for PSK, except does not have + * length limits. + */ +struct dpp_network_credential_sae { + char *passphrase; + const char *passphrase_hex; +}; + +/** + * @brief DPP network credential. + */ +struct dpp_network_credential { + struct list_head list; + enum dpp_akm akm; + union { + struct dpp_network_credential_psk psk; + struct dpp_network_credential_sae sae; + }; +}; + +/** + * @brief Allocates and initializes a new network credential object. The initial state describes an invalid network credential. It must be filled in to be made valid. + * + * @return struct dpp_network_credential* + */ +struct dpp_network_credential * +dpp_network_credential_alloc(void); + +/** + * @brief Sets a passphrase for the credential. + * + * @param credential The psk credential to set the passphrase for. + * @param passphrase The passphrase to set. + * @return int 0 if the passphrase was successfully set. -ERANGE is the + * passphrase was outside of the allowed bounds for the passphrase. + */ +int +dpp_credential_psk_set_passphrase(struct dpp_network_credential_psk *credential, const char *passphrase); + +/** + * @brief Sets a pre-shared key for the credential. + * + * @param credential The psk credential to set the psk for. + * @param key_hex The hex encoded pre-shared key. + * @return int 0 if the key was successfully set. -ERANGE if the key was + * outside of the allowed bounds. + */ +int +dpp_credential_psk_set_key(struct dpp_network_credential_psk *credential, const char *key_hex); + +/** + * @brief Sets a passphrase for the credential. + * + * @param credential The sae credential to set the passphrase for. + * @param passphrase The passphrase to set. + * @return int 0 if the passphrase was successfully set, non-zero otherwise. + */ +int +dpp_credential_sae_set_passphrase(struct dpp_network_credential_sae *credential, const char *passphrase); + +/** + * @brief Determines if a network credential is valid. + * + * @param credential The credential to check. + * @return true If the credential is valid. + * @return false If the credential is invalid. + */ +bool +dpp_network_credential_is_valid(const struct dpp_network_credential *credential); + +/** + * @brief Uninitializes a dpp network credential, releasing any owned resources. + * + * @param credential The credential to uninitialize. + */ +void +dpp_network_credential_uninitialize(struct dpp_network_credential *credential); + +/** + * @brief DPP network configuration. + */ +struct dpp_network { + struct dpp_network_discovery discovery; + struct list_head credentials; +}; + +/** + * @brief Allocates and initializes a new dpp network object. + * + * @return struct dpp_network + */ +struct dpp_network * +dpp_network_alloc(void); + +/** + * @brief Uninitializes a dpp network, releasing any owned resources. If the + * network is part of a list, it is removed from that list. + * + * @param network The network to uninitialize. + */ +void +dpp_network_uninitialize(struct dpp_network *network); + +/** + * @brief Adds a new credential to the network. + * + * @param network The network to add the credential to. + * @param credential The credential to add. + */ +void +dpp_network_add_credential(struct dpp_network *network, struct dpp_network_credential *credential); + +/** + * @brief Determines if a DPP network structure is valid. + * + * @param network The network to validate. + * @return true If the network described is valid. + * @return false Otherwise. + */ +bool +dpp_network_is_valid(const struct dpp_network *network); + +#endif //__DPP_H__ diff --git a/src/wpas/CMakeLists.txt b/src/wpas/CMakeLists.txt new file mode 100644 index 0000000..ceb22e2 --- /dev/null +++ b/src/wpas/CMakeLists.txt @@ -0,0 +1,31 @@ + +project(ztpwpas) + +add_library(ztpwpas + STATIC + "" +) + +if (BUILD_HOSTAP_EXTERNAL) + add_dependencies(ztpwpas hostap) +endif() + +target_sources(ztpwpas + PRIVATE + wpa_controller.c + wpa_controller_watcher.c + wpa_core.c + wpa_supplicant.c +) + +target_include_directories(ztpwpas + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../utils + ${CMAKE_CURRENT_SOURCE_DIR}/../wifi +) + +target_link_libraries(ztpwpas + ${LIBWPA_CLIENT_TARGET} + ztputils +) diff --git a/src/wpas/wpa_controller.c b/src/wpas/wpa_controller.c new file mode 100644 index 0000000..1f3a5d4 --- /dev/null +++ b/src/wpas/wpa_controller.c @@ -0,0 +1,1111 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "dpp.h" +#include "event_loop.h" +#include "string_utils.h" +#include "wpa_controller.h" +#include "wpa_controller_watcher.h" +#include "ztp_log.h" + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" +#endif //__clang__ + +/** + * @brief Helper function to track registered event handlers. + */ +struct wpa_event_handler_instance { + struct list_head list; + struct wpa_event_handler *handler; + void *userdata; +}; + +/** + * @brief Macro to help invoking event handlers consistently. + * + * @param _ctrl A pointer to the control interface, of type struct wpa_controller *. + * @param _name The name of the event handler symbol within struct wpa_event_handler. + * @param ... The arguments to pass to the event handler. + */ +#define WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(_ctrl, _name, ...) \ + do { \ + struct wpa_event_handler_instance *_instance; \ + list_for_each_entry (_instance, &_ctrl->event_handlers, list) { \ + if (_instance->handler->_name != NULL) \ + _instance->handler->_name(_instance->userdata, ##__VA_ARGS__); \ + } \ + } while (0) + +/** + * @brief Helper to stringify macro arguments. + */ +#define xstr(s) str(s) +#define str(s) #s + +/** + * @brief Helpers for parsing a mac address string as encoded in wpa control + * socket event messages. + */ +#define MAC_FORMAT "aa:bb:cc:dd:ee:ff" +#define MAC_SIZE (sizeof MAC_FORMAT) +#define MAC_SIZE_C 18 +#define MAC_SIZE_STR xstr(MAC_SIZE_C) +static_assert(MAC_SIZE == MAC_SIZE_C, "invalid mac size"); + +/** + * @brief Helpers for pasing a DPP bootstrap hash string as encoded in wpa + * control socket event messages. + */ +#define DPP_HASH_FORMAT "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +#define DPP_HASH_SIZE (sizeof DPP_HASH_FORMAT) +#define DPP_HASH_SIZE_C 65 +#define DPP_HASH_SIZE_STR xstr(DPP_HASH_SIZE_C) +static_assert(DPP_HASH_SIZE == DPP_HASH_SIZE_C, "invalid dpp hash size"); + +/** + * @brief Processes the chirp received event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_event_chirp_received(struct wpa_controller *ctrl, const char *payload) +{ + int32_t id; + uint32_t frequency; + char hash[DPP_HASH_SIZE]; + char src[MAC_SIZE]; + + int ret = sscanf(payload, "id=%d src=%" MAC_SIZE_STR "s freq=%" PRIu32 " hash=%" DPP_HASH_SIZE_STR "s", &id, src, &frequency, hash); + if (ret != 4) { + zlog_error_if(ctrl->interface, "invalid chirp event received"); + return; + } + + zlog_debug_if(ctrl->interface, "chirp received, id=%d src=%s, freq=%u, hash=%s", id, src, frequency, hash); + + struct dpp_bootstrap_publickey_hash publickey_hash; + if (hex_decode(hash, publickey_hash.data, sizeof publickey_hash.data) < 0) { + zlog_error_if(ctrl->interface, "ifailed to decode public key hash (invalid format)"); + return; + } + + const char(*mac)[MAC_SIZE] = (const char(*)[MAC_SIZE])src; + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_chirp_received, id, mac, frequency, &publickey_hash); +} + +/** + * @brief Process the dpp frame received event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_dpp_frame_received(struct wpa_controller *ctrl, const char *payload) +{ + char src[MAC_SIZE]; + int type_value; + uint32_t frequency; + + int ret = sscanf(payload, "src=%" MAC_SIZE_STR "s freq=%" PRIu32 " type=%d", src, &frequency, &type_value); + if (ret != 3) { + zlog_error("invalid dpp frame rx event received"); + return; + } + + enum dpp_public_action_frame_type type = dpp_public_action_frame_parse_int(type_value); + if (type == DPP_PAF_INVALID) { + zlog_error("invalid dpp frame type in dpp frame rx event (type=%d)", type_value); + return; + } + + zlog_debug_if(ctrl->interface, "dpp frame event received src=%s freq=%" PRIu32 " type=%s", src, frequency, dpp_public_action_frame_str(type)); + + const char(*mac)[MAC_SIZE] = (const char(*)[MAC_SIZE])src; + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_frame_received, mac, type, frequency); +} + +/** + * @brief Process the dpp frame transmitted event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_dpp_frame_transmitted(struct wpa_controller *ctrl, const char *payload) +{ + __unused(payload); + + char dst[MAC_SIZE]; + int type_value; + uint32_t frequency; + + int ret = sscanf(payload, "dst=%" MAC_SIZE_STR "s freq=%" PRIu32 " type=%d", dst, &frequency, &type_value); + if (ret != 3) { + zlog_error("invalid dpp frame tx event received"); + return; + } + + enum dpp_public_action_frame_type type = dpp_public_action_frame_parse_int(type_value); + if (type == DPP_PAF_INVALID) { + zlog_error("invalid dpp frame type in dpp frame tx event (type=%d)", type_value); + return; + } + + const char(*mac)[MAC_SIZE] = (const char(*)[MAC_SIZE])dst; + zlog_debug_if(ctrl->interface, "dpp frame event transmitted dst=%s freq=%" PRIu32 " type=%s", dst, frequency, dpp_public_action_frame_str(type)); + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_frame_transmitted, mac, type, frequency); +} + +/** + * @brief Process the dpp frame transmitted status event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_dpp_frame_transmitted_status(struct wpa_controller *ctrl, const char *payload) +{ + char dst[MAC_SIZE]; + char result[32]; + uint32_t frequency; + + int ret = sscanf(payload, "dst=%" MAC_SIZE_STR "s freq=%" PRIu32 " result=%31s", dst, &frequency, result); + if (ret != 3) { + zlog_error("invalid dpp frame tx status event received"); + return; + } + + const char(*mac)[MAC_SIZE] = (const char(*)[MAC_SIZE])dst; + zlog_debug_if(ctrl->interface, "dpp frame event transmitted status dst=%s freq=%" PRIu32 " result=%s", dst, frequency, result); + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_frame_transmitted_status, mac, frequency, result); +} + +/** + * @brief Processes the dpp chirp stopped event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_event_dpp_chirp_stopped(struct wpa_controller *ctrl, const char *payload) +{ + __unused(payload); + + zlog_debug_if(ctrl->interface, "dpp chirp stopped event received"); + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_chirp_stopped); +} + +/** + * @brief Processes the dpp failure event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_event_dpp_failure(struct wpa_controller *ctrl, const char *payload) +{ + zlog_debug_if(ctrl->interface, "dpp failure event received (details=%s)", payload); + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_failure, payload); +} + +/** + * @brief Processes the dpp authentication initialization failure event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_event_dpp_authentication_failure(struct wpa_controller *ctrl, const char *payload) +{ + __unused(payload); + + zlog_debug_if(ctrl->interface, "dpp authentication failure event received"); + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_authentication_failure); +} + +/** + * @brief Processes the dpp authentication success event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_event_dpp_authentication_success(struct wpa_controller *ctrl, const char *payload) +{ + int initiator; + int ret = sscanf(payload, "init=%d", &initiator); + if (ret != 1) { + zlog_error("invalid dpp authentication event received"); + return; + } + + zlog_debug_if(ctrl->interface, "dpp authentication success event received, initiator=%01d", initiator); + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_authentication_success, !!initiator); +} + +/** + * @brief Processes the dpp configuration failure event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_event_dpp_configuration_failure(struct wpa_controller *ctrl, const char *payload) +{ + __unused(payload); + + zlog_debug_if(ctrl->interface, "dpp configuration failure event received"); + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_configuration_failure); +} + +/** + * @brief Processes the dpp configuration success event. + * + * @param ctrl The wpa controller instance. + * @param payload The event payload. + */ +static void +wpa_controller_process_event_dpp_configuration_success(struct wpa_controller *ctrl, const char *payload) +{ + __unused(payload); + + zlog_debug_if(ctrl->interface, "dpp configuration success event received"); + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, dpp_configuration_success); +} + +/** + * @brief This describes an upper bound for a control message. The wpa_cli tool + * uses this as an upper bound, so is used similarly here. + */ +#define WPA_MAX_MSG_SIZE 4096 + +/** + * @brief wpa controller message handler. This handles a single event message + * that has become available on the event control interface file descriptor. + * + * @param ctrl The controller associated with the event. + */ +static void +wpa_controller_process_event(struct wpa_controller *ctrl) +{ + char buf[WPA_MAX_MSG_SIZE]; + size_t buf_length = (sizeof buf) - 1; + + int ret = wpa_ctrl_recv(ctrl->event, buf, &buf_length); + if (ret < 0) { + zlog_error("[%s] failed to retrieve pending wpa control event (%d)", ctrl->interface, ret); + return; + } + + buf[buf_length] = '\0'; + zlog_debug("[%s] event=%s", ctrl->interface, buf); + + // Each message begins with an integer priority between angle brackets, + // . Find the ending angle bracket. + const char *start = strchr(buf, '>'); + if (!start) { + zlog_warning("[%s] malformed wpa control message detected", ctrl->interface); + return; + } + + // Advance 1 character to the beginning of the message contents. + start++; + + const char *message = NULL; + if (strstart(start, DPP_EVENT_CHIRP_RX, &message)) { + wpa_controller_process_event_chirp_received(ctrl, message); + } else if (strstart(start, DPP_EVENT_CHIRP_STOPPED, &message)) { + wpa_controller_process_event_dpp_chirp_stopped(ctrl, message); + } else if (strstart(start, DPP_EVENT_FAIL, &message)) { + wpa_controller_process_event_dpp_failure(ctrl, message); + } else if (strstart(start, DPP_EVENT_AUTH_INIT_FAILED, &message)) { + wpa_controller_process_event_dpp_authentication_failure(ctrl, message); + } else if (strstart(start, DPP_EVENT_CONF_FAILED, &message)) { + wpa_controller_process_event_dpp_configuration_failure(ctrl, message); + } else if (strstart(start, DPP_EVENT_AUTH_SUCCESS, &message)) { + wpa_controller_process_event_dpp_authentication_success(ctrl, message); + } else if (strstart(start, DPP_EVENT_CONF_RECEIVED, &message)) { + wpa_controller_process_event_dpp_configuration_success(ctrl, message); + } else if (strstart(start, DPP_EVENT_RX, &message)) { + wpa_controller_process_dpp_frame_received(ctrl, message); + } else if (strstart(start, DPP_EVENT_TX, &message)) { + wpa_controller_process_dpp_frame_transmitted(ctrl, message); + } else if (strstart(start, DPP_EVENT_TX_STATUS, &message)) { + wpa_controller_process_dpp_frame_transmitted_status(ctrl, message); + } +} + +/** + * @brief Primary control interface event handler. This gets + * invoked for all "unsolicited" events on the control interface. + * + * @param fd The wpa controller control interface file descriptor for events. + * @param context The wpa controller instance. + */ +static void +on_wpa_controller_event(int fd, void *context) +{ + __unused(fd); + + struct wpa_controller *ctrl = (struct wpa_controller *)context; + assert(fd == ctrl->event_fd); + + for (;;) { + int ret = wpa_ctrl_pending(ctrl->event); + if (ret < 0) { + zlog_error("[%s] failed to determine if wpa control event is pending; possible disconnection", ctrl->interface); + break; + } else if (ret == 0) { + break; + } + + wpa_controller_process_event(ctrl); + } +} + +/** + * @brief Attempt to establish a connection with the control socket. + * + * @param ctrl The control interface instance. + * @return int 0 if the connection was established, non-zero otherwise. + */ +static int +wpa_controller_connection_establish(struct wpa_controller *ctrl) +{ + int ret; + struct wpa_ctrl *ctrl_command = wpa_ctrl_open(ctrl->path); + if (!ctrl_command) { + zlog_error("[%s] failed to establish wpa control command connection using control file %s", ctrl->interface, ctrl->path); + return -ENODEV; + } + + struct wpa_ctrl *ctrl_event = wpa_ctrl_open(ctrl->path); + if (!ctrl_event) { + zlog_error("[%s] failed to establish wpa control event connection using control file %s", ctrl->interface, ctrl->path); + ret = -ENODEV; + goto fail; + } + + ret = wpa_ctrl_attach(ctrl_event); + if (ret < 0) { + zlog_error("[%s] failed to attach to wpa control event feed using control file %s (%d)", ctrl->interface, ctrl->path, ret); + goto fail; + } + + int ctrl_event_fd = wpa_ctrl_get_fd(ctrl_event); + ret = event_loop_register_event(ctrl->loop, EPOLLIN, ctrl_event_fd, on_wpa_controller_event, ctrl); + if (ret < 0) { + zlog_error("[%s] failed to register wpa control event monitor (%d)", ctrl->interface, ret); + goto fail; + } + + ctrl->command = ctrl_command; + ctrl->event = ctrl_event; + ctrl->event_fd = ctrl_event_fd; + ctrl->connected = true; + + zlog_info("[%s] control socket connection established", ctrl->interface); + +out: + return ret; +fail: + if (ctrl_event) { + wpa_ctrl_detach(ctrl_event); + wpa_ctrl_close(ctrl_event); + } + + if (ctrl_command) { + wpa_ctrl_close(ctrl_command); + } + + goto out; +} + +/** + * @brief Teardown a control socket connection. + * + * @param ctrl The control interface instance. + */ +static void +wpa_controller_connection_teardown(struct wpa_controller *ctrl) +{ + if (ctrl->event_fd != -1) { + event_loop_unregister_event(ctrl->loop, ctrl->event_fd); + ctrl->event_fd = -1; + } + + if (ctrl->event) { + wpa_ctrl_detach(ctrl->event); + wpa_ctrl_close(ctrl->event); + ctrl->event = NULL; + } + + if (ctrl->command) { + wpa_ctrl_close(ctrl->command); + ctrl->command = NULL; + } + + ctrl->connected = false; + + zlog_info("[%s] control socket connection severed", ctrl->interface); +} + +/** + * @brief Default parameters for pending connection retries. + */ +#define CONNECTION_RETRY_MAX_ATTEMPTS 10 +#define CONNECTION_RETRY_PERIOD_US 1000000 + +/** + * @brief Pending connection attempt context. + */ +struct wpa_controller_pending_connection_context { + struct wpa_controller *ctrl; + uint32_t num_attempts; + uint32_t max_attempts; + uint32_t period_us; +}; + +static void +on_pending_connection_attempt(void *context); + +/** + * @brief Cancels a pending connection attempt. + * + * @param ctrl The control interface instance. + */ +static void +pending_connection_cancel(struct wpa_controller *ctrl) +{ + if (!ctrl->pending_connection_context) + return; + + event_loop_task_cancel(ctrl->loop, on_pending_connection_attempt, ctrl->pending_connection_context); + free(ctrl->pending_connection_context); + ctrl->pending_connection_context = NULL; +} + +/** + * @brief Schedules a timer for periodic connection attempts. + * + * This will schedule a timer that will be invoked at the specified period upon + * connection attempt failures. The timer will be automatically canceled if the + * maximum number of attempts have been reached or a connection has been + * successfully established. + * + * @param ctrl The control interface instance. + * @param period_us The period at which to attempt connections, in microseconds. + * @param max_attempts The maximum number of attempts to make. + * @return int 0 if the timer was scheduled, non-zero otherwise. + */ +static int +pending_connection_schedule(struct wpa_controller *ctrl, uint32_t period_us, uint32_t max_attempts) +{ + pending_connection_cancel(ctrl); + + struct wpa_controller_pending_connection_context *context = calloc(1, sizeof *context); + if (!context) { + zlog_error("failed to schedule pending connection attempt (no memory)"); + return -ENOMEM; + } + + context->ctrl = ctrl; + context->num_attempts = 0; + context->max_attempts = max_attempts; + + int ret = event_loop_task_schedule(ctrl->loop, 0, period_us, TASK_PERIODIC, on_pending_connection_attempt, context); + if (ret < 0) { + zlog_error("failed to schedule pending connection attempt timer (%d)", ret); + free(context); + return ret; + } + + ctrl->pending_connection_context = context; + + return 0; +} + +/** + * @brief Pending connection attempt handler. + * + * This will attempt to establish a connection with the control socket. If + * successful, the controller presence event handlers will be signaled and the + * periodic pending connection attempt timer will be cancled. + * + * @param context The pending connection attempt context. + */ +static void +on_pending_connection_attempt(void *context) +{ + struct wpa_controller_pending_connection_context *pending = (struct wpa_controller_pending_connection_context *)context; + if (!pending) + return; + + struct wpa_controller *ctrl = pending->ctrl; + + if (pending->num_attempts >= pending->max_attempts) { + zlog_error_if(ctrl->interface, "failed to re-connect to control socket after %" PRIu32 " attempts; giving up", pending->max_attempts); + pending_connection_cancel(ctrl); + return; + } else if (pending->num_attempts++ > 0) { + zlog_info_if(ctrl->interface, "attempting to re-connect to control socket [%" PRIu32 "/%" PRIu32 "]", pending->num_attempts, pending->max_attempts); + } + + if (wpa_controller_connection_establish(ctrl) < 0) + return; + + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, interface_presence_changed, WPA_CONTROLLER_ARRIVED); + pending_connection_cancel(ctrl); +} + +/** + * @brief Control socket presence event handler. + * + * @param context The wpa controller instance. + * @param event The event that occurred. + * @param path The parent path of the control interface. + * @param interface The control socket file name. + */ +static void +on_ctrl_presence_changed(void *context, enum wpa_controller_presence_event event, const char *path, const char *interface) +{ + zlog_debug("wpa control socket %s/%s %s", path, interface, event == WPA_CONTROLLER_ARRIVED ? "arrived" : "departed"); + + struct wpa_controller *ctrl = (struct wpa_controller *)context; + if (strcmp(ctrl->interface, interface) != 0) { + zlog_debug("ignoring wpa controller presence event for interface %s", interface); + return; + } + + pending_connection_cancel(ctrl); + + switch (event) { + case WPA_CONTROLLER_DEPARTED: { + wpa_controller_connection_teardown(ctrl); + break; + } + case WPA_CONTROLLER_ARRIVED: { + int ret = wpa_controller_connection_establish(ctrl); + if (ret < 0) { + zlog_warning_if(ctrl->interface, "failed to connect to control socket (%d); scheduling pending connection", ret); + ret = pending_connection_schedule(ctrl, CONNECTION_RETRY_PERIOD_US, CONNECTION_RETRY_MAX_ATTEMPTS); + if (ret < 0) + zlog_error_if(ctrl->interface, "failed to schedule pending control socket connection (%d)", ret); + return; + } + break; + } + default: + return; + } + + WPA_CONTROLLER_INVOKE_EVENT_HANDLERS(ctrl, interface_presence_changed, WPA_CONTROLLER_DEPARTED); +} + +/** + * @brief Uninitializes a wpa controller. + * + * @param interface The interface to uninitialize the control interface for. + */ +void +wpa_controller_uninitialize(struct wpa_controller *ctrl) +{ + pending_connection_cancel(ctrl); + wpa_controller_connection_teardown(ctrl); + + if (ctrl->watcher) { + wpa_controller_watcher_destroy(&ctrl->watcher); + } + + if (ctrl->path) { + free(ctrl->path); + ctrl->path = NULL; + } +} + +/** + * @brief Initializes a wpa controller. + * + * @param ctrl The controller to initialize. + * @param ctrl_path The full path to the wpa control socket. + * @return int 0 if the control interface was successfully initialized non-zero otherwise. + */ +int +wpa_controller_initialize(struct wpa_controller *ctrl, const char *ctrl_path, struct event_loop *loop) +{ + char *path = strdup(ctrl_path); + if (!path) { + zlog_error("failed to allocate memory for wpa controller path %s", ctrl_path); + return -ENOMEM; + } + + char *separator = strrchr(path, '/'); + if (!separator) { + free(path); + zlog_error("control path %s is malformed (missing parent directory)", ctrl_path); + return -EINVAL; + } + + size_t leaf_index = (size_t)(separator - path); + ctrl->loop = loop; + ctrl->path = path; + ctrl->interface = ctrl->path + leaf_index + 1; + + ctrl->path[leaf_index] = '\0'; + ctrl->watcher = wpa_controller_watcher_create(loop, path); + ctrl->path[leaf_index] = '/'; + + if (!ctrl->watcher) { + zlog_error_if(ctrl->interface, "failed to create wpa controller watcher"); + wpa_controller_uninitialize(ctrl); + return -EINVAL; + } + + int ret = wpa_controller_watcher_register_interface_presence_event_handler(ctrl->watcher, on_ctrl_presence_changed, ctrl); + if (ret < 0) { + zlog_error_if(ctrl->interface, "failed to register for control interface presence change events (%d)", ret); + goto fail; + } + + // Attempt to establish a connection with the control interface. Do not + // count failure here as catastrophic since the watcher will establish a + // connection when the control socket becomes available. + if (wpa_controller_connection_establish(ctrl) < 0) + zlog_info_if(ctrl->interface, "control socket for %s unavailable; deferring connection", ctrl_path); + + ret = 0; +out: + return ret; +fail: + wpa_controller_uninitialize(ctrl); + goto out; +} + +/** + * @brief Allocates a new wpa controller instance. The returned pointer must be + * initialized with wpa_controller_initialize. + * + * @return struct wpa_controller* + */ +struct wpa_controller * +wpa_controller_alloc(void) +{ + struct wpa_controller *ctrl = (struct wpa_controller *)calloc(1, sizeof *ctrl); + if (!ctrl) { + zlog_error("failed to allocate memory for wpa controller"); + return NULL; + } + + INIT_LIST_HEAD(&ctrl->event_handlers); + + return ctrl; +} + +/** + * @brief Destroys a wpa controller, as allocated by wpa_controller_alloc. + * + * @param ctrl The controller instance to destroy. + */ +void +wpa_controller_destroy(struct wpa_controller **ctrl) +{ + if (*ctrl) { + free(*ctrl); + *ctrl = NULL; + } +} + +/** + * @brief Sends a generic command on the wpa control interface. + * + * This function should not be used directly. Instead, the + * wpa_controller_send_commandf macro provides syntactic sugar for sending a + * command in a similar way and constructs the 'fmt' argument as it is expected + * here. + * + * This function allows printf style generation of the command payload, but + * expects a very specific encoding of the 'fmt' argument, which must be of the + * form: + * + * " " + * + * The va_args must be provided according to the 'fmt' argument. The behavior is otherwise undefined. + * + * @param ctrl The control interface to send the command on. + * @param name The name of the command to send. + * @param reply The buffer to hold the reply. + * @param reply_length The size of the reply buffer. Will be updated with the actual reply length on success. + * @param fmt The string format of the following arguments constituting the command payload. + * @param ... The arguments constituting the command payload. + * @return int 0 if the command was successfully sent. In this case, 'reply' + * shall contain a *reply_length response from the control interface. -ENOTCON + * is returned if there is no established connection to the control socket. + * Otherwise, a non-zero value is returned. + */ +static int +__wpa_controller_send_commandf(struct wpa_controller *ctrl, const char *name, char *reply, size_t *reply_length, const char *fmt, ...) +{ + int ret; + char cmd[WPA_MAX_MSG_SIZE]; + + if (!ctrl->connected) { + zlog_error_if(ctrl->interface, "no connection to control interface"); + return -ENOTCONN; + } + + va_list args; + va_start(args, fmt); + ret = vsnprintf(cmd, sizeof cmd, fmt, args); + va_end(args); + + if (ret < 0) { + zlog_error_if(ctrl->interface, "failed to encode %s command (%d)", name, ret); + return ret; + } + + size_t cmd_length = (size_t)ret; + zlog_debug_if(ctrl->interface, "wpa -> %.*s", (int)cmd_length, cmd); + + ret = wpa_ctrl_request(ctrl->command, cmd, cmd_length, reply, reply_length, NULL); + if (ret < 0) { + zlog_error_if(ctrl->interface, "failed to send %s command on ctrl interface (%d)", name, ret); + return ret; + } + + zlog_debug_if(ctrl->interface, "wpa <- %.*s", (int)(*reply_length), reply); + + return 0; +} + +/** + * @brief Helper macro to allow encoding the command buffer on the fly using va_args. + * + * The _cmd and _fmt arguments must be string literals. They are combined to + * form the command buffer, with the command name fixed and the payload + * populated by the va_args following the last argument (...). + * + * Eg. + * const char *dpp_uri = "DPP:V:1;K:MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgACE0vdn8KsfXKHusJPcEscx+naQyQJLSob1VjuqPsP6r8=;"; + * wpa_controller_send_commandf(ctrl, "DPP_QRCODE", reply, reply_length, "%s", dpp_uri); + */ +#define wpa_controller_send_commandf(_ctrl, _cmd, _reply, _reply_length, _fmt, ...) \ + __wpa_controller_send_commandf(_ctrl, _cmd, _reply, _reply_length, _cmd " " _fmt, ##__VA_ARGS__) + +/** + * @brief Argument-less version of wpa_controller_send_commandf. + */ +#define wpa_controller_send_command(_ctrl, _cmd, _reply, _reply_length) \ + __wpa_controller_send_commandf(_ctrl, _cmd, _reply, _reply_length, _cmd) + +/** + * @brief Invokes the 'DPP_QR_CODE' command on the hostapd interface. + * + * @param ctrl The wpa controller instance. + * @param dpp_uri The DPP URI to register with the configurator. + * @return int 0 if successful, non-zero otherwise. + */ +int +wpa_controller_qrcode(struct wpa_controller *ctrl, const char *dpp_uri, uint32_t *bootstrap_id) +{ + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + int ret = wpa_controller_send_commandf(ctrl, "DPP_QR_CODE", reply, &reply_length, "%s", dpp_uri); + if (ret < 0) + return ret; + + uint32_t id = (uint32_t)strtoul(reply, NULL, 10); + if (id == 0) { + zlog_error_if(ctrl->interface, "qrcode command failed (response='%.*s')", (int)reply_length, reply); + return -1; + } + + zlog_debug_if(ctrl->interface, "%s <-> id=%" PRIu32 "", dpp_uri, id); + + if (bootstrap_id) + *bootstrap_id = id; + + return 0; +} + +/** + * @brief Invokes the 'SET" command on the control interface. This alllows + * controlling runtime configuration parameters of wpa_supplicant and hostapd. + * + * The caller is responsible for sending appropriately supported key/value + * pairs depending on the daemon respresented by the control interface, and the + * key itself. + * + * @param ctrl The wpa controller instance. + * @param key The name of the configuration key to set. + * @param value The value of the configuration key to set. + * @return int 0 if the command was successfully sent and the value was + * applied. Otherwise a non-zero error value is returned. + */ +int +wpa_controller_set(struct wpa_controller *ctrl, const char *key, const char *value) +{ + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + int ret = wpa_controller_send_commandf(ctrl, "SET", reply, &reply_length, "%s %s", key, value); + if (ret < 0) + return ret; + + return (strncmp(reply, "OK", 2) == 0) ? 0 : -1; +} + +/** + * @brief Invokes the 'DPP_AUTH_INIT' command on the control interface. This + * initiated a DPP authentication with the bootstrapping information identified + * by 'peer_id'. + * + * @param ctrl The wpa controller instance. + * @param peer_id The bootstrapping info identifier for the peer to initiation + * DPP authentication with. + * @param frequency The radio frequency to use to initiation authentication. If + * 0 is supplied, the configurator will attempt to initiate authentication on + * all usable channels (not recommended). + * @return int 0 if the command was + * successfully send, non-zero otherwise. + */ +int +wpa_controller_dpp_auth_init(struct wpa_controller *ctrl, uint32_t peer_id, uint32_t frequency) +{ + int ret; + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + if (frequency > 0) { + ret = wpa_controller_send_commandf(ctrl, "DPP_AUTH_INIT", reply, &reply_length, "peer=%" PRIu32 " neg_freq=%" PRIu32, peer_id, frequency); + } else { + ret = wpa_controller_send_commandf(ctrl, "DPP_AUTH_INIT", reply, &reply_length, "peer=%" PRIu32, peer_id); + } + + if (ret < 0) + return ret; + + return (strncmp(reply, "OK", 2) == 0) ? 0 : -1; +} + +/** + * @brief Invokes the 'DPP_AUTH_INIT' command on the control interface. This + * initiated a DPP authentication with the bootstrapping information identified + * by 'peer_id'. + * + * @param ctrl The wpa controller instance. + * @param peer_id The bootstrapping info identifier for the peer to initiation + * DPP authentication with. + * @param frequency The radio frequency to use to initiation authentication. If + * 0 is supplied, the configurator will attempt to initiate authentication on + * all usable channels (not recommended). + * @param conf The DPP configurator settings for authentication. This can + * include the target network configuration, amongst other things. + * @return int 0 if the command was + * successfully send, non-zero otherwise. + */ +int +wpa_controller_dpp_auth_init_with_conf(struct wpa_controller *ctrl, uint32_t peer_id, uint32_t frequency, const char *conf) +{ + int ret; + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + if (frequency > 0) { + ret = wpa_controller_send_commandf(ctrl, "DPP_AUTH_INIT", reply, &reply_length, "peer=%" PRIu32 " neg_freq=%" PRIu32 " %s", peer_id, frequency, conf); + } else { + ret = wpa_controller_send_commandf(ctrl, "DPP_AUTH_INIT", reply, &reply_length, "peer=%" PRIu32 " %s", peer_id, conf); + } + + if (ret < 0) + return ret; + + return (strncmp(reply, "OK", 2) == 0) ? 0 : -1; +} + +/** + * @brief Invokes the 'DPP_BOOTSTRAP_SET' command on the control interface. + * This associates configurator parameters with the specified peer. Many + * configurator params can be specified, however, the most useful for ztp are + * those related to network provisioning. + * + * @param ctrl The wpa controller instance. + * @param peer_id The bootstrapping info identifier of the peer to set + * bootstrapping information for. + * @param conf The bootstrapping configuration parameters, encoded as a string. + * @return int 0 if the command was successfully sent, non-zero otherwise. + */ +int +wpa_controller_dpp_bootstrap_set(struct wpa_controller *ctrl, uint32_t peer_id, const char *conf) +{ + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + int ret = wpa_controller_send_commandf(ctrl, "DPP_BOOTSTRAP_SET", reply, &reply_length, "%" PRIu32 " %s", peer_id, conf); + if (ret < 0) + return ret; + + return (strncmp(reply, "OK", 2) == 0) ? 0 : -1; +} + +/** + * @brief Invokes the 'DPP_BOOTSTRAP_GEN' command on the control interface. + * This creates a DPP bootstrap key according to the specified parameters in + * 'bi'. + * + * @param ctrl The wpa controller instance. + * @param bi The information describing the bootstrap key. + * @param id The identifier of the bootstrap key, which is used to identify the + * key in subsequent control interface DPP commands. + * @return int 0 if the command completed successfully, non-zero otherwise. + */ +int +wpa_controller_dpp_bootstrap_gen(struct wpa_controller *ctrl, const struct dpp_bootstrap_info *bi, uint32_t *id) +{ + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + int ret = wpa_controller_send_commandf(ctrl, "DPP_BOOTSTRAP_GEN", reply, &reply_length, + "type=%s %s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", + dpp_bootstrap_type_str(bi->type), + bi->channel ? " chan=" : "", bi->channel ? bi->channel : "", + bi->mac ? " mac=" : "", bi->mac ? bi->mac : "", + bi->info ? " info=" : "", bi->info ? bi->info : "", + bi->curve ? " curve=" : "", bi->curve ? bi->curve : "", + bi->key ? " key=" : "", bi->key ? bi->key : "", + bi->key_id ? " key_id=" : "", bi->key_id ? bi->key_id : "", + bi->engine_id ? " engine=" : "", bi->engine_id ? bi->engine_id : "", + bi->engine_path ? " engine_path=" : "", bi->engine_path ? bi->engine_path : ""); + if (ret < 0) + return ret; + + uint32_t id_reply = (uint32_t)strtoul(reply, NULL, 0); + if (id_reply == 0) + return -EINVAL; + + *id = id_reply; + + return 0; +} + +/** + * @brief Requests wpa_supplicant to begin chirping with a specific bootstrap key. + * + * @param ctrl The wpa controller instance. + * @param bootstrap_key_id The identifier of the bootstrapping key to chirp. + * @param iterations The number of chirp iterations to perform. + * @return int The result of the operation, 0 if successful, non-zero otherwise. + */ +int +wpa_controller_dpp_chirp(struct wpa_controller *ctrl, uint32_t bootstrap_key_id, uint32_t iterations) +{ + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + int ret = wpa_controller_send_commandf(ctrl, "DPP_CHIRP", reply, &reply_length, "own=%" PRIu32 " iter=%" PRIu32, bootstrap_key_id, iterations); + if (ret < 0) + return ret; + + return (strncmp(reply, "OK", 2) == 0) ? 0 : -1; +} + +/** + * @brief Requests wpa_supplicant to stop chirping. + * + * @param ctrl The wpa controller instance. + * @return int The result of the operation, 0 if successful, non-zero otherwise. + */ +int +wpa_controller_dpp_chirp_stop(struct wpa_controller *ctrl) +{ + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + int ret = wpa_controller_send_command(ctrl, "DPP_STOP_CHIRP", reply, &reply_length); + if (ret < 0) + return ret; + + return (strncmp(reply, "OK", 2) == 0) ? 0 : -1; +} + +/** + * @brief Requests wpa_supplicant to stop/cancel a DPP exchange. + * @return int The result of the operation, 0 if successful, non-zero otherwise. + */ +int +wpa_controller_dpp_listen_stop(struct wpa_controller *ctrl) +{ + char reply[WPA_MAX_MSG_SIZE]; + size_t reply_length = sizeof reply; + + int ret = wpa_controller_send_command(ctrl, "DPP_STOP_LISTEN", reply, &reply_length); + if (ret < 0) + return ret; + + return (strncmp(reply, "OK", 2) == 0) ? 0 : -1; +} + +/** + * @brief Registers a handler for when chirps are received on the specified interface. + * + * @param ctrl The wpa control instance. + * @param handler The event handler to invoke when an event occurs. + * @param userdata The context to be passed to the event handling functions. + * @return int 0 if successful, non-zero otherwise. + */ +int +wpa_controller_register_event_handler(struct wpa_controller *ctrl, struct wpa_event_handler *handler, void *userdata) +{ + struct wpa_event_handler_instance *instance = (struct wpa_event_handler_instance *)malloc(sizeof *instance); + if (!instance) { + zlog_error_if(ctrl->interface, "failed to allocate memory for chirp received event handler"); + return -ENOMEM; + } + + INIT_LIST_HEAD(&instance->list); + instance->handler = handler; + instance->userdata = userdata; + list_add(&instance->list, &ctrl->event_handlers); + + zlog_debug_if(ctrl->interface, "wpa event handler registered, handler=0x%" PRIx64 " arg=0x%" PRIx64 "", (uint64_t)handler, (uint64_t)userdata); + + return 0; +} + +/** + * @brief Unregisters a handler for chirp received events. + * + * @param ctrl The wpa control instance. + * @param handler The previously registered event handler. + * @param userdata The event handler callback context. + */ +void +wpa_controller_unregister_event_handler(struct wpa_controller *ctrl, struct wpa_event_handler *handler, void *userdata) +{ + struct wpa_event_handler_instance *instance; + list_for_each_entry (instance, &ctrl->event_handlers, list) { + if (instance->handler == handler && instance->userdata == userdata) { + zlog_debug_if(ctrl->interface, "wpa event handler unregistered, handler=0x%" PRIx64 " arg=0x%" PRIx64 "", (uint64_t)handler, (uint64_t)userdata); + list_del(&instance->list); + free(instance); + break; + } + } +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif //__clang_ diff --git a/src/wpas/wpa_controller.h b/src/wpas/wpa_controller.h new file mode 100644 index 0000000..4c035e4 --- /dev/null +++ b/src/wpas/wpa_controller.h @@ -0,0 +1,316 @@ + +#ifndef __WPA_CONTROLLER_H__ +#define __WPA_CONTROLLER_H__ + +#include +#include + +#include + +#include "dpp.h" +#include "wpa_controller_watcher.h" +#include "wpa_core.h" + +struct wpa_ctrl; +struct event_loop; + +struct wpa_event_handler { + /** + * @brief Prototype for the interface presence changed event. + * + * This event fires when the interface presence changes. Specifically, this + * occurs when the interface departs (no longer accessible/usable) or + * arrives (becomes usable). + * + * @param userdata Contextual data that was registered with the handler. + * @param presence The new presence of the interface. + */ + void (*interface_presence_changed)(void *userdata, enum wpa_controller_presence_event presence); + + /** + * @brief Prototype for the dpp frame received event. + * + * This event fires each time a DPP frame is received. + */ + void (*dpp_frame_received)(void *userdata, const char (*mac)[(DPP_MAC_LENGTH * 2) + (DPP_MAC_LENGTH - 1) + 1], enum dpp_public_action_frame_type type, uint32_t frequency); + + /** + * @brief Prototype for the dpp frame transmitted event. + * + * This event fires each time a DPP frame is transmitted. + */ + void (*dpp_frame_transmitted)(void *userdata, const char (*mac)[(DPP_MAC_LENGTH * 2) + (DPP_MAC_LENGTH - 1) + 1], enum dpp_public_action_frame_type type, uint32_t frequency); + + /** + * @brief Prototype for the dpp frame transmitted status event. + * + * This event fires each time a dpp frame transmission completes. This can + * either be due to completion, timeout (no acknowledgement) or some other + * failure. + * + * @param userdata Contextual data that was registered with the handler. + * @param dst The mac address of the frame destination. + * @param frequency The radio frequency the frame was transmitted on. + * @param result The result of the operation, either "SUCCESS", "FAILURE", or "no-ACK". + */ + void (*dpp_frame_transmitted_status)(void *userdata, const char (*dst)[(DPP_MAC_LENGTH * 2) + (DPP_MAC_LENGTH - 1) + 1], uint32_t frequency, const char *result); + + /** + * @brief Prototype for the chirp received event callback. + * + * @param userdata Contextual data that was registered with the handler. + * @param id The bootstrap identifier of the peer. If the peer is not known, -1 will be populated. + * @param mac The mac address of the peer the chirp originated from. + * @param frequency The radio frequency the chirp was received on. + * @param hash The "chirp" hash of the peer's public bootstrapping key. + */ + void (*dpp_chirp_received)(void *userdata, int32_t id, const char (*mac)[(DPP_MAC_LENGTH * 2) + (DPP_MAC_LENGTH - 1) + 1], uint32_t frequency, const struct dpp_bootstrap_publickey_hash *hash); + + /** + * @brief Prototype for the DPP chirp stopped event callback. + * + * This event fires when DPP chirping stops. + * + * @param userdata Contextual data that was registered with the handler. + */ + void (*dpp_chirp_stopped)(void *userdata); + + /** + * @brief Prototype for the DPP failure event callback. + * + * This event fires when a generic DPP failure occurs. Some failures + * including failure details. + * + * @param userdata Contextual data that was registered with the handler. + * @param details A string describing the details of the failure. + * This pointer is always valid. When no failure details were provided, + * this will point to the empty string. + */ + void (*dpp_failure)(void *userdata, const char *details); + + /** + * @brief Prototype for the DPP authentication failure event callback. + * + * This event fires when a DPP exchange fails the authentication stage. + * + * @param userdata Contextual data that was registered with the handler. + */ + void (*dpp_authentication_failure)(void *userdata); + + /** + * @brief Prototype for the DPP authentication success event. + * + * This event fires when DPP authentication completes successfully. + * + * @param initiator Indicates whether this device is the initiator. + */ + void (*dpp_authentication_success)(void *userdata, bool initiator); + + /** + * @brief Prototype for the DPP configuration failed event. + * + * This event fires when a DPP exchange fails the configuration stage. + * + * @param userdata Contextual data that was registered with the handler. + */ + void (*dpp_configuration_failure)(void *userdata); + + /** + * @brief Prototype for the DPP configuration success event. + * + * This event fires when a DPP configuration completes successfully. + * + * @param userdata Contextual data that was registered with the handler. + */ + void (*dpp_configuration_success)(void *userdata); +}; + +struct wpa_controller { + int event_fd; + char *path; + const char *interface; + bool connected; + struct wpa_ctrl *event; + struct wpa_ctrl *command; + struct wpa_controller_watcher *watcher; + struct event_loop *loop; + struct list_head event_handlers; + void *pending_connection_context; +}; + +/** + * @brief Allocates a new wpa controller instance. The returned pointer must be + * initialized with wpa_controller_initialize. + * + * @return struct wpa_controller* + */ +struct wpa_controller * +wpa_controller_alloc(void); + +/** + * @brief Destroys a wpa controller, as allocated by wpa_controller_alloc. + * + * @param ctrl The controller instance to destroy. + */ +void +wpa_controller_destroy(struct wpa_controller **ctrl); + +/** + * @brief Initializes the hostapd control interface. + * + * @param ctrl The controller to initialize. + * @param ctrl_path The full path to the wpa control socket. + * @return int 0 if the control interface was successfully initialized non-zero otherwise. + */ +int +wpa_controller_initialize(struct wpa_controller *ctrl, const char *ctrl_path, struct event_loop *loop); + +/** + * @brief Uninitializes a wpa controller. + * + * @param interface The interface to uninitialize. + */ +void +wpa_controller_uninitialize(struct wpa_controller *ctrl); + +/** + * @brief Invokes the 'DPP_QRCODE' command on the hostapd interface. + * + * @param ctrl The wpa controller instance. + * @param dpp_uri The DPP URI to register with the configurator. + * @param bootstrap_id The unique identifier for the bootstrapping information . + * @return int 0 if successful, non-zero otherwise. + */ +int +wpa_controller_qrcode(struct wpa_controller *ctrl, const char *dpp_uri, uint32_t *bootstrap_id); + +/** + * @brief Invokes the 'SET" command on the control interface. This alllows + * controlling runtime configuration parameters of wpa_supplicant and hostapd. + * + * The caller is responsible for sending appropriately supported key/value + * pairs depending on the daemon respresented by the control interface, and the + * key itself. + * + * @param ctrl The wpa controller instance. + * @param key The name of the configuration key to set. + * @param value The value of the configuration key to set. + * @return int 0 if the command was successfully sent and the value was + * applied. Otherwise a non-zero error value is returned. + */ +int +wpa_controller_set(struct wpa_controller *ctrl, const char *key, const char *value); + +/** + * @brief Invokes the 'DPP_AUTH_INIT' command on the control interface. This + * initiated a DPP authentication with the bootstrapping information identified + * by 'peer_id'. + * + * @param ctrl The wpa controller instance. + * @param peer_id The bootstrapping info identifier for the peer to initiation + * DPP authentication with. + * @param frequency The radio frequency to use to initiation authentication. If + * 0 is supplied, the configurator will attempt to initiate authentication on + * all usable channels (not recommended) + * @return int 0 if the command was successfully send, non-zero otherwise. + */ +int +wpa_controller_dpp_auth_init(struct wpa_controller *ctrl, uint32_t peer_id, uint32_t frequency); + +/** + * @brief Invokes the 'DPP_AUTH_INIT' command on the control interface. This + * initiated a DPP authentication with the bootstrapping information identified + * by 'peer_id'. + * + * @param ctrl The wpa controller instance. + * @param peer_id The bootstrapping info identifier for the peer to initiation + * DPP authentication with. + * @param frequency The radio frequency to use to initiation authentication. If + * 0 is supplied, the configurator will attempt to initiate authentication on + * all usable channels (not recommended). + * @param conf The DPP configurator settings for authentication. This can + * include the target network configuration, amongst other things. + * @return int 0 if the command was + * successfully send, non-zero otherwise. + */ +int +wpa_controller_dpp_auth_init_with_conf(struct wpa_controller *ctrl, uint32_t peer_id, uint32_t frequency, const char *conf); + +/** + * @brief Invokes the 'DPP_BOOTSTRAP_SET' command on the control interface. + * This associates configurator parameters with the specified peer. Many + * configurator params can be specified, however, the most useful for ztp are + * those related to network provisioning. + * + * @param ctrl The wpa controller instance. + * @param peer_id The bootstrapping info identifier of the peer to set + * bootstrapping information for. + * @param conf The bootstrapping configuration parameters, encoded as a string. + * @return int 0 if the command was successfully sent, non-zero otherwise. + */ +int +wpa_controller_dpp_bootstrap_set(struct wpa_controller *ctrl, uint32_t peer_id, const char *conf); + +/** + * @brief Invokes the 'DPP_BOOTSTRAP_GEN' command on the control interface. + * This creates a DPP bootstrap key according to the specified parameters in + * 'bi'. + * + * @param ctrl The wpa controller instance. + * @param bi The information describing the bootstrap key. + * @param id The identifier of the bootstrap key, which is used to identify the + * key in subsequent control interface DPP commands. + * @return int 0 if the command completed successfully, non-zero otherwise. + */ +int +wpa_controller_dpp_bootstrap_gen(struct wpa_controller *ctrl, const struct dpp_bootstrap_info *bi, uint32_t *id); + +/** + * @brief Requests wpa_supplicant to begin chirping with a specific bootstrap key. + * + * @param ctrl The wpa controller instance. + * @param bootstrap_key_id The identifier of the bootstrapping key to chirp. + * @param iterations The number of chirp iterations to perform. + * @return int The result of the operation, 0 if successful, non-zero otherwise. + */ +int +wpa_controller_dpp_chirp(struct wpa_controller *ctrl, uint32_t bootstrap_key_id, uint32_t iterations); + +/** + * @brief Requests wpa_supplicant to stop chirping. + * + * @param ctrl The wpa controller instance. + * @return int The result of the operation, 0 if successful, non-zero otherwise. + */ +int +wpa_controller_dpp_chirp_stop(struct wpa_controller *ctrl); + +/** + * @brief Requests wpa_supplicant to stop/cancel a DPP exchange. + * @return int The result of the operation, 0 if successful, non-zero otherwise. + */ +int +wpa_controller_dpp_listen_stop(struct wpa_controller *ctrl); + +/** + * @brief Registers a handler for when chirps are received on the specified interface. + * + * @param ctrl The wpa control instance. + * @param handler The event handler to invoke when an event occurs. + * @param userdata The context to be passed to the event handling functions. + * @return int 0 if successful, non-zero otherwise. + */ +int +wpa_controller_register_event_handler(struct wpa_controller *ctrl, struct wpa_event_handler *handler, void *userdata); + +/** + * @brief Unregisters a handler for chirp received events. + * + * @param ctrl The wpa control instance. + * @param handler The previously registered event handler. + * @param userdata The event handler callback context. + */ +void +wpa_controller_unregister_event_handler(struct wpa_controller *ctrl, struct wpa_event_handler *handler, void *userdata); + +#endif // __WPA_CONTROLLER_H__ diff --git a/src/wpas/wpa_controller_watcher.c b/src/wpas/wpa_controller_watcher.c new file mode 100644 index 0000000..4c371d9 --- /dev/null +++ b/src/wpas/wpa_controller_watcher.c @@ -0,0 +1,446 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "event_loop.h" +#include "wpa_controller_watcher.h" +#include "ztp_log.h" + +/** + * @brief Controller presence event handler. + */ +struct wpa_controller_presence_event_handler { + struct list_head list; + wpa_controller_presence_event_fn callback; + void *context; +}; + +/** + * @brief Destroys a wpa watch, releasing any resources it owns. + * + * @param pwatch + */ +static void +wpa_inotify_watch_destroy(struct wpa_inotify_watch **pwatch) +{ + struct wpa_inotify_watch *watch = *pwatch; + if (!watch) + return; + + free(watch); + *pwatch = NULL; +} + +/** + * @brief Creates a new wpa watch. + * + * @param path The path of the control interface the watch is associated with. + * @return struct wpa_watch* The newly created instance, if successful. NULL otherwise. + */ +static struct wpa_inotify_watch * +wpa_inotify_watch_create(const char *path) +{ + size_t pathlen = strlen(path) + 1; + struct wpa_inotify_watch *watch = calloc(1, sizeof *watch + pathlen); + if (!watch) { + zlog_error("failed to allocate memory for new wpa controller watch"); + return NULL; + } + + watch->id = -1; + memcpy(watch->path, path, pathlen); + + return watch; +} + +/** + * @brief Registers a new wpa controller presence event handlers. The specified + * handler will be invoked each time the presence of a wpa control socket + * changes (either arrives or departs). + * + * @param watcher The watcher that is configured to watch for control socket presence events. + * @param callback The callback function to invoke. + * @param context The user-supplied context that will be passed to the handler. + * @return int 0 if the handler was successfully registered, non-zero otherwise. + */ +int +wpa_controller_watcher_register_interface_presence_event_handler(struct wpa_controller_watcher *watcher, wpa_controller_presence_event_fn callback, void *context) +{ + struct wpa_controller_presence_event_handler *handler = calloc(1, sizeof *handler); + if (!handler) { + zlog_error("failed to allocate memory for wpa presence event handler"); + return -ENOMEM; + } + + handler->callback = callback; + handler->context = context; + INIT_LIST_HEAD(&handler->list); + list_add_tail(&handler->list, &watcher->handlers); + + return 0; +} + +/** + * @brief Unregisters a wpa controller presence event handler. + * + * @param watcher The watcher that the handler was previously associated with. + * @param callback The callback function that was previously registered. + * @param context The user-supplied context that was previously associated with + * the handler. This is used to uniquely identify the handler registration. + */ +void +wpa_controller_watcher_unregister_interface_presence_event_handler(struct wpa_controller_watcher *watcher, wpa_controller_presence_event_fn callback, void *context) +{ + struct wpa_controller_presence_event_handler *handler; + + list_for_each_entry (handler, &watcher->handlers, list) { + if (handler->callback == callback && handler->context == context) { + list_del(&handler->list); + free(handler); + return; + } + } +} + +/** + * @brief Invokes a single presence event handler for the specified event. + * + * @param handler The handler to invoke. + * @param event The event that occurred. + * @param path The base path (parent directory) of the control socket. + * @param interface The name of the intergace associated with the control socket. + */ +static void +invoke_presence_event_handler(const struct wpa_controller_presence_event_handler *handler, enum wpa_controller_presence_event event, const char *path, const char *interface) +{ + handler->callback(handler->context, event, path, interface); +} + +/** + * @brief Invokes all registered presence event handlers for the specified event. + * + * @param watcher The wpa controller watcher instance. + * @param event The event that occurred. + * @param path The control path the event was signaled on. + * @param interface The name of the interface associated with the control socket. + */ +static void +invoke_presence_event_handlers(struct wpa_controller_watcher *watcher, enum wpa_controller_presence_event event, const char *path, const char *interface) +{ + struct wpa_controller_presence_event_handler *handler; + list_for_each_entry (handler, &watcher->handlers, list) { + invoke_presence_event_handler(handler, event, path, interface); + } +} + +/** + * @brief Manually discover and fire presence events for all existing control + * interfaces. + * + * @param watcher The watcher instance to discover control entries for. + */ +static void +prime_event_presence_arrivals(struct wpa_controller_watcher *watcher) +{ + DIR *dir = opendir(watcher->watch_ctrl->path); + if (!dir) { + int ret = errno; + zlog_error("failed to open wpa control path (%d)", ret); + return; + } + + for (;;) { + struct dirent *entry = readdir(dir); + if (!entry) + break; + if (entry->d_type != DT_SOCK) + continue; + invoke_presence_event_handlers(watcher, WPA_CONTROLLER_ARRIVED, watcher->watch_ctrl->path, entry->d_name); + } + + closedir(dir); +} + +/** + * @brief Handles when the control path arrives or is known to be present. This + * primarily setups up a new watch on the control directory, and primes the + * existing handlers with presence information by checking if there are any + * existing control interface sockets. + * + * @param watcher The watcher instance the event occurred for. + */ +static void +on_ctrl_path_arrived(struct wpa_controller_watcher *watcher) +{ + int id = inotify_add_watch(watcher->inotify_fd, watcher->watch_ctrl->path, IN_CREATE | IN_DELETE); + if (id < 0) { + zlog_error("failed to add inotify watch for wpa control path %s (%d)", watcher->watch_ctrl->path, errno); + return; + } + + watcher->watch_ctrl->id = id; + zlog_debug("wpa control path %s arrived", watcher->watch_ctrl->path); + + prime_event_presence_arrivals(watcher); +} + +/** + * @brief Handles when the control path departs or is known to be absent. This + * primary removes the watch on the control directory. + * + * @param watcher The watcher instance the event occurred for. + */ +static void +on_ctrl_path_departed(struct wpa_controller_watcher *watcher) +{ + inotify_rm_watch(watcher->inotify_fd, watcher->watch_ctrl->id); + watcher->watch_ctrl->id = -1; + zlog_debug("wpa control path %s departed", watcher->watch_ctrl->path); +} + +/** + * @brief Handler for inotify events related to the parent control directory. + * + * @param watcher The watcher instance associated with the event. + * @param event The event that occurred. + */ +static void +handle_inotify_event_parent(struct wpa_controller_watcher *watcher, const struct inotify_event *event) +{ + if ((event->mask & IN_ISDIR) == 0 || strncmp(event->name, watcher->ctrl_component, event->len) != 0) + return; + + if (event->mask & IN_CREATE) { + on_ctrl_path_arrived(watcher); + } else if (event->mask & IN_DELETE) { + on_ctrl_path_departed(watcher); + } else { + zlog_debug("unexpected mask=0x%08x for inotify event watch id=%d, for path %s", event->mask, event->wd, watcher->watch_ctrl->path); + } +} + +/** + * @brief Handler for inotify events related to the control socket directory. + * + * @param watcher The watcher instance associated with the event. + * @param event The event that occurred. + */ +static void +handle_inotify_event_ctrl(struct wpa_controller_watcher *watcher, const struct inotify_event *event) +{ + enum wpa_controller_presence_event presence; + if (event->mask & IN_CREATE) { + presence = WPA_CONTROLLER_ARRIVED; + } else if (event->mask & IN_DELETE) { + presence = WPA_CONTROLLER_DEPARTED; + } else { + zlog_warning("unexpected mask=0x%08x for inotify event watch id=%d for path %s", event->mask, event->wd, watcher->watch_ctrl->path); + return; + } + + invoke_presence_event_handlers(watcher, presence, watcher->watch_ctrl->path, event->name); +} + +/** + * @brief First-level handler for inotify events. + * + * This primarily determines if the event is related to the parent control + * directory, or the control directory itself and dispatches the event + * appropriately. + * + * @param watcher The watcher instance associated with the event. + * @param event The event that occurred. + */ +static void +handle_inotify_event(struct wpa_controller_watcher *watcher, const struct inotify_event *event) +{ + const char *name = event->len ? event->name : ""; + zlog_debug("inotify path=%s target=%s mask=0x%08x", watcher->watch_ctrl->path, name, event->mask); + + if (event->wd == watcher->watch_parent->id) { + handle_inotify_event_parent(watcher, event); + } else if (event->wd == watcher->watch_ctrl->id) { + handle_inotify_event_ctrl(watcher, event); + } else { + zlog_warning("no watch found for inotify event id=%d name=%s", event->wd, name); + return; + } +} + +/** + * @brief Handler for all watches. + * + * @param fd The inotify file decriptor. + * @param context The wpa controller watcher instance. + */ +static void +on_inotify_signaled(int fd, void *context) +{ + struct wpa_controller_watcher *watcher = (struct wpa_controller_watcher *)context; + char eventbuf[sizeof(struct inotify_event) + NAME_MAX + 1]; + + for (;;) { + ssize_t n = read(fd, eventbuf, sizeof eventbuf); + if (n < 0) { + int ret = errno; + if (ret != EAGAIN) + zlog_error("error reading inotify event (%d)", ret); + return; + } + + char *p = eventbuf; + do { + const struct inotify_event *event = (struct inotify_event *)p; + p += (ssize_t)((sizeof *event) + event->len); + n -= (ssize_t)((sizeof *event) + event->len); + handle_inotify_event(watcher, event); + } while (n > 0); + + assert(n == 0); + } +} + +/** + * @brief Initializes the inotify watches. The path should be the base + * directory that hostapd or wpa_supplicant was configured to write its control + * socket files to. Eg. /var/run/hostapd or /var/run/wpa_supplicant. + * + * @param watcher The watcher instance to initialize watches for. + * @param path The base path where the target process writes its control socket information. + * @return int 0 if the watches were successfully initialized, non-zero otherwise. + */ +static int +wpa_controller_watcher_configure_watches(struct wpa_controller_watcher *watcher, const char *path) +{ + int ret; + struct wpa_inotify_watch *watch_ctrl = NULL; + struct wpa_inotify_watch *watch_parent = NULL; + + char *separator = strrchr(path, '/'); + if (!separator) { + zlog_error("wpa control path %s malformed", path); + return -EINVAL; + } + + watch_ctrl = wpa_inotify_watch_create(path); + if (!watch_ctrl) { + zlog_error("failed to create watch for wpa control path %s", path); + return -ENOMEM; + } + + size_t leaf_index = (size_t)(separator - path); + watch_ctrl->path[leaf_index] = '\0'; + watch_parent = wpa_inotify_watch_create(watch_ctrl->path); + watch_ctrl->path[leaf_index] = '/'; + + if (!watch_parent) { + zlog_error("failed to create watch for wpa control path parent %s", path); + ret = -ENOMEM; + goto fail; + } + + int id = inotify_add_watch(watcher->inotify_fd, watch_parent->path, IN_CREATE | IN_DELETE); + if (id == -1) { + ret = errno; + zlog_error("failed to add inotify watch for wpa control path parent %s (%d)", watch_parent->path, ret); + goto fail; + } + + watcher->watch_parent = watch_parent; + watcher->watch_parent->id = id; + watcher->watch_ctrl = watch_ctrl; + watcher->ctrl_component = watcher->watch_ctrl->path + leaf_index + 1; + + zlog_debug("monitoring started for wpa control path %s", path); + + if (access(path, F_OK) == 0) + on_ctrl_path_arrived(watcher); + + ret = 0; +out: + return ret; +fail: + if (watch_ctrl) + free(watch_ctrl); + if (watch_parent) + free(watch_parent); + goto out; +} + +/** + * @brief Destroys a wpa controller watcher that was previously created with + * 'wpa_controller_watcher_create. + * + * @param pwatcher Pointer to the watcher to destroy. + */ +void +wpa_controller_watcher_destroy(struct wpa_controller_watcher **pwatcher) +{ + struct wpa_controller_watcher *watcher = *pwatcher; + + event_loop_unregister_event(watcher->loop, watcher->inotify_fd); + wpa_inotify_watch_destroy(&watcher->watch_ctrl); + wpa_inotify_watch_destroy(&watcher->watch_parent); + + struct wpa_controller_presence_event_handler *handler; + struct wpa_controller_presence_event_handler *tmp; + list_for_each_entry_safe (handler, tmp, &watcher->handlers, list) { + list_del(&handler->list); + free(handler); + } + + free(watcher); + *pwatcher = NULL; +} + +/** + * @brief Creates and initializes a new instance of a wpa controller watcher. + * + * @param loop The ztp event loop to be used to monitor for control socket presence. + * @param path The base control socket path used by wpa_supplicant or hostapd. + * + * @return struct wpa_controller_watcher* A new instance of a wpa controller watcher. + */ +struct wpa_controller_watcher * +wpa_controller_watcher_create(struct event_loop *loop, const char *path) +{ + int inotify_fd = inotify_init1(IN_NONBLOCK); + if (inotify_fd == -1) { + zlog_error("failed to initialize inotify for wpa controller watcher (%d)", errno); + return NULL; + } + + struct wpa_controller_watcher *watcher = calloc(1, sizeof *watcher); + if (!watcher) { + zlog_error("failed to allocate memory for new wpa controller watcher"); + return NULL; + } + + INIT_LIST_HEAD(&watcher->handlers); + + watcher->loop = loop; + watcher->inotify_fd = inotify_fd; + + int ret = event_loop_register_event(loop, EPOLLIN, inotify_fd, on_inotify_signaled, watcher); + if (ret < 0) { + zlog_error("failed to register inotify event handler for new wpa controller watcher (%d)", ret); + wpa_controller_watcher_destroy(&watcher); + return NULL; + } + + ret = wpa_controller_watcher_configure_watches(watcher, path); + if (ret < 0) { + zlog_error("failed to configure inotify watches for wpa controller watcher (%d)", ret); + wpa_controller_watcher_destroy(&watcher); + return NULL; + } + + return watcher; +} diff --git a/src/wpas/wpa_controller_watcher.h b/src/wpas/wpa_controller_watcher.h new file mode 100644 index 0000000..e47d38b --- /dev/null +++ b/src/wpas/wpa_controller_watcher.h @@ -0,0 +1,94 @@ + +#ifndef __WPA_CONTROLLER_WATCHER_H__ +#define __WPA_CONTROLLER_WATCHER_H__ + +#include + +struct event_loop; + +/** + * @brief Directory wpa control socket watch. One watch exists for each control + * socket information directory being monitored. + */ +struct wpa_inotify_watch { + int id; + char path[]; +}; + +/** + * @brief WPA controller watcher. This is the primary structure containing + * state information needed to track the presence of wpa controllers on the + * system. + */ +struct wpa_controller_watcher { + struct wpa_inotify_watch *watch_parent; + struct wpa_inotify_watch *watch_ctrl; + struct list_head handlers; + struct event_loop *loop; + const char *ctrl_component; + int inotify_fd; +}; + +/** + * @brief Creates and initializes a new instance of a wpa controller watcher. + * + * @param loop The ztp event loop to be used to monitor for control socket presence. + * @param path The base control socket path used by wpa_supplicant or hostapd. + * + * @return struct wpa_controller_watcher* A new instance of a wpa controller watcher. + */ +struct wpa_controller_watcher * +wpa_controller_watcher_create(struct event_loop *loop, const char *path); + +/** + * @brief Destroys a wpa controller watcher that was previously created with + * 'wpa_controller_watcher_create. + * + * @param pwatcher Pointer to the watcher to destroy. + */ +void +wpa_controller_watcher_destroy(struct wpa_controller_watcher **pwatcher); + +/** + * @brief Describes a wpa controller presence event. + */ +enum wpa_controller_presence_event { + WPA_CONTROLLER_ARRIVED, + WPA_CONTROLLER_DEPARTED +}; + +/** + * @brief Prototype for wpa controller presence event handlers. + * + * @param context The user-supplied context. + * @param event The event that occurred. + * @param path The full path of the control socket to the interface. + * @param interface The interface for which the event occurred. + */ +typedef void (*wpa_controller_presence_event_fn)(void *context, enum wpa_controller_presence_event event, const char *path, const char *interface); + +/** + * @brief Registers a new wpa controller presence event handlers. The specified + * handler will be invoked each time the presence of a wpa control socket + * changes (either arrives or departs). + * + * @param watcher The watcher that is configured to watch for control socket presence events. + * @param callback The callback function to invoke. + * @param context The user-supplied context that will be passed to the handler. + * @return int 0 if the handler was successfully registered, non-zero otherwise. + */ +int +wpa_controller_watcher_register_interface_presence_event_handler(struct wpa_controller_watcher *watcher, wpa_controller_presence_event_fn callback, void *context); + +/** + * @brief Unregisters a wpa controller presence event handler. + * + * @param watcher The watcher that the handler was previously associated with. + * @param callback The callback function that was previously registered. + * @param context The user-supplied context that was previously associated with + * the handler. This is used to uniquely identify the handler registration. + */ +void +wpa_controller_watcher_unregister_interface_presence_event_handler(struct wpa_controller_watcher *watcher, wpa_controller_presence_event_fn callback, void *context); + +#endif //__WPA_CONTROLLER_WATCHER_H__ diff --git a/src/wpas/wpa_core.c b/src/wpas/wpa_core.c new file mode 100644 index 0000000..b14b46b --- /dev/null +++ b/src/wpas/wpa_core.c @@ -0,0 +1,74 @@ + +#include +#include + +#include "string_utils.h" +#include "wpa_core.h" +#include "ztp_log.h" + +/** + * @brief Translates a dpp_network to a wpa_supplicant and/or hostapd DPP + * configurator params string. The configurator params string is used to + * associated provisioning information with peers/enrollees. + * + * Currently, this function only supports PSK-based network credentials. + * + * @param network The network to convert. + * @param params A string buffer to write the configurator params to. + * @param params_length The size of the 'params' buffer, in bytes. + * @return int 0 If the network was successfully converted. In this case, + * params will contain the properly encoded configurator params and + * params_length will indicate the length. Otherwise a non-zero value is + * returned. + */ +int +dpp_network_to_wpa_configurator_params(struct dpp_network *network, char *params, size_t *params_length, enum dpp_network_role netrole) +{ + char ssid_hex[(DPP_SSID_LENGTH_MAX * 2) + 1]; + hex_encode(network->discovery.ssid, network->discovery.ssid_length, ssid_hex, sizeof ssid_hex); + + if (!dpp_network_is_valid(network)) { + zlog_error("dpp network is invalid or unsupported"); + return -EINVAL; + } + + // This is guaranteed to be non-NULL by dpp_network_is_valid() above. + struct dpp_network_credential *credential = list_first_entry(&network->credentials, struct dpp_network_credential, list); + const char *akm = dpp_akm_str(credential->akm); + const char *netrolestr = dpp_network_role_str(netrole); + + const char *cred_type; + const char *cred_value; + + if (credential->akm == DPP_AKM_PSK) { + switch (credential->psk.type) { + case PSK_CREDENTIAL_TYPE_PASSPHRASE: + cred_type = WPA_DPP_CONF_CRED_PSK_TYPE_PASSPHRASE; + cred_value = credential->psk.passphrase.hex; + break; + case PSK_CREDENTIAL_TYPE_PSK: + cred_type = WPA_DPP_CONF_CRED_PSK_TYPE_PSK; + cred_value = credential->psk.key.hex; + break; + default: + zlog_error("dpp network has unsupported psk credential type=%u", (uint32_t)credential->psk.type); + return -ENOTSUP; + } + } else if (credential->akm == DPP_AKM_SAE) { + cred_type = WPA_DPP_CONF_CRED_SAE_TYPE_PASSPHRASE; + cred_value = credential->sae.passphrase_hex; + } else { + zlog_error("dpp network has unsupported akm '%s'", akm); + return -ENOTSUP; + } + + int ret = snprintf(params, *params_length, "conf=%s-%s ssid=%s %s=%s", netrolestr, akm, ssid_hex, cred_type, cred_value); + if (ret < 0) { + zlog_error("failed to translate dpp network to configurator params (%d)", ret); + return ret; + } + + *params_length = (size_t)ret; + + return 0; +} diff --git a/src/wpas/wpa_core.h b/src/wpas/wpa_core.h new file mode 100644 index 0000000..ea2b52f --- /dev/null +++ b/src/wpas/wpa_core.h @@ -0,0 +1,61 @@ + +#ifndef __WPA_CORE_H__ +#define __WPA_CORE_H__ + +#include "dpp.h" + +/** + * @brief Prototype for the chirp received event callback. + * + * @param userdata Contextual data that was registered with the handler. + * @param id The bootstrap identifier of the peer. If the peer is not known, -1 will be populated. + * @param mac The mac address of the peer the chirp originated from. + * @param frequency The radio frequency the chirp was received on. + * @param hash The "chirp" hash of the peer's public bootstrapping key. + */ +typedef void (*dpp_chirp_received_fn)(void *userdata, int32_t id, const char (*mac)[(DPP_MAC_LENGTH * 2) + 1], uint32_t frequency, const struct dpp_bootstrap_publickey_hash *hash); + +/** + * @brief The maximum length of a configurator params string. Currently, this + * is defined to allow enough space for an SSID and PSK encoded as hex. + */ +#define WPA_CONFIGURATOR_PARAMS_MAX_LENGTH 512 + +/** + * @brief wpa config option name for setting global DPP configurator parameters. + */ +#define WPA_CFG_PROPERTY_DPP_CONFIGURATOR_PARAMS "dpp_configurator_params" + +/** + * @brief DPP network role strings as used in dpp_configurator_params. + */ +#define WPA_DPP_CONF_NETROLE_STA "sta" +#define WPA_DPP_CONF_NETROLE_AP "ap" +#define WPA_DPP_CONF_NETWORK_CONFIGURATOR "configurator" + +/** + * @brief DPP network credential type names as used in dpp_configurator_params. + */ +#define WPA_DPP_CONF_CRED_PSK_TYPE_PSK "psk" +#define WPA_DPP_CONF_CRED_PSK_TYPE_PASSPHRASE "pass" +#define WPA_DPP_CONF_CRED_SAE_TYPE_PASSPHRASE "pass" + +/** + * @brief Translates a dpp_network to a wpa_supplicant and/or hostapd DPP + * configurator params string. The configurator params string is used to + * associated provisioning information with peers/enrollees. + * + * Currently, this function only supports PSK-based network credentials. + * + * @param network The network to convert. + * @param params A string buffer to write the configurator params to. + * @param params_length The size of the 'params' buffer, in bytes. + * @return int 0 If the network was successfully converted. In this case, + * params will contain the properly encoded configurator params and + * params_length will indicate the length. Otherwise a non-zero value is + * returned. + */ +int +dpp_network_to_wpa_configurator_params(struct dpp_network *network, char *params, size_t *params_length, enum dpp_network_role netrole); + +#endif //__WPA_CORE_H__ diff --git a/src/wpas/wpa_supplicant.c b/src/wpas/wpa_supplicant.c new file mode 100644 index 0000000..1a716c5 --- /dev/null +++ b/src/wpas/wpa_supplicant.c @@ -0,0 +1,57 @@ + +#include +#include + +#include + +#include "wpa_supplicant.h" + +/** + * @brief Table mapping enum wpas_interface_state values to strings. + */ +static const char *wpas_interface_state_strs[WPAS_INTERFACE_STATE_INVALID + 1] = { + "disconnected", + "inactive", + "disabled", + "scanning", + "authenticating", + "associating", + "associated", + "4way_handshake", + "group_handshake", + "completed", + "unknown", + "invalid", +}; + +/** + * @brief Parses a string and converts it to a wpa supplicant interface state + * enumeration value. + * + * If the string does not correspond to a valid enumeration value, WPAS_INTERFACE_STATE_INVALID is returned. + * @param state The string to parse and convert. + * @return enum wpas_interface_state The enumeration value corresponding to the + * string, oo WPAS_INTERFACE_STATE_INVALID if no such correspondence exists. + */ +enum wpas_interface_state +parse_wpas_interface_state(const char *state) +{ + for (size_t i = 0; i < ARRAY_SIZE(wpas_interface_state_strs); i++) { + if (strcmp(state, wpas_interface_state_strs[i]) == 0) + return (enum wpas_interface_state)i; + } + + return WPAS_INTERFACE_STATE_INVALID; +} + +/** + * @brief Converts a wpa supplicant interface state to a string. + * + * @param state The state to convert. + * @return const char* The string representation of the state. + */ +const char * +wpas_interface_state_str(enum wpas_interface_state state) +{ + return wpas_interface_state_strs[state]; +} diff --git a/src/wpas/wpa_supplicant.h b/src/wpas/wpa_supplicant.h new file mode 100644 index 0000000..15df443 --- /dev/null +++ b/src/wpas/wpa_supplicant.h @@ -0,0 +1,50 @@ + +#ifndef __WPA_SUPPLICANT_H__ +#define __WPA_SUPPLICANT_H__ + +#define WPAS_DBUS_SERVICE_PATH "/fi/w1/wpa_supplicant1" +#define WPAS_DBUS_SERVICE "fi.w1.wpa_supplicant1" +#define WPAS_DBUS_INTERFACE "fi.w1.wpa_supplicant1.Interface" +#define WPAS_DBUS_NETWORK "fi.w1.wpa_supplicant1.Network" +#define WPAS_DBUS_BSS "fi.w1.wpa_supplicant1.BSS" + +/** + * @brief wpa supplicant interface state. + */ +enum wpas_interface_state { + WPAS_INTERFACE_STATE_DISCONNECTED = 0, + WPAS_INTERFACE_STATE_INACTIVE, + WPAS_INTERFACE_STATE_DISABLED, + WPAS_INTERFACE_STATE_SCANNING, + WPAS_INTERFACE_STATE_AUTHENTICATING, + WPAS_INTERFACE_STATE_ASSOCIATING, + WPAS_INTERFACE_STATE_ASSOCIATED, + WPAS_INTERFACE_STATE_4WAY_HANDSHAKE, + WPAS_INTERFACE_STATE_GROUP_HANDSHAKE, + WPAS_INTERFACE_STATE_COMPLETED, + WPAS_INTERFACE_STATE_UNKNOWN, + WPAS_INTERFACE_STATE_INVALID +}; + +/** + * @brief Parses a string and converts it to a wpa supplicant interface state + * enumeration value. + * + * If the string does not correspond to a valid enumeration value, WPAS_INTERFACE_STATE_INVALID is returned. + * @param state The string to parse and convert. + * @return enum wpas_interface_state The enumeration value corresponding to the + * string, oo WPAS_INTERFACE_STATE_INVALID if no such correspondence exists. + */ +enum wpas_interface_state +parse_wpas_interface_state(const char *state); + +/** + * @brief Converts a wpa supplicant interface state to a string. + * + * @param state The state to convert. + * @return const char* The string representation of the state. + */ +const char * +wpas_interface_state_str(enum wpas_interface_state state); + +#endif //__WPA_SUPPLICANT_H__ diff --git a/src/ztpd/CMakeLists.txt b/src/ztpd/CMakeLists.txt new file mode 100644 index 0000000..0dd1942 --- /dev/null +++ b/src/ztpd/CMakeLists.txt @@ -0,0 +1,40 @@ + +project(ztpd) + +add_executable(ztpd + "" +) + +install( + TARGETS ztpd + DESTINATION ${CMAKE_INSTALL_SBINDIR} +) + +target_sources(ztpd + PRIVATE + main.c + ztpd_ui.c + ztpd.c +) + +target_include_directories(ztpd + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../core + ${CMAKE_CURRENT_SOURCE_DIR}/../core/biproviders + ${CMAKE_CURRENT_SOURCE_DIR}/../dbus + ${CMAKE_CURRENT_SOURCE_DIR}/../systemd + ${CMAKE_CURRENT_SOURCE_DIR}/../utils + ${CMAKE_CURRENT_SOURCE_DIR}/../wpas + ${CMAKE_CURRENT_SOURCE_DIR}/../wifi +) + +target_link_libraries(ztpd + ztpcore + ztpdbus + ztpsystemd + ztputils + ${LIBGPIOD_TARGET} +) + +add_subdirectory(systemd) diff --git a/src/ztpd/main.c b/src/ztpd/main.c new file mode 100644 index 0000000..72be261 --- /dev/null +++ b/src/ztpd/main.c @@ -0,0 +1,320 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "event_loop.h" +#include "ztp_dbus_client.h" +#include "ztp_dbus_network_configuration.h" +#include "ztp_dbus_server.h" +#include "ztp_log.h" +#include "ztp_settings.h" +#include "ztp_wpa_supplicant.h" +#include "ztpd.h" + +/** + * @brief Returns a pointer to the first non-whitespace character in a string. + + * @param s The string to trim. + * @return char* The first non-whitespace character in the string, or '\0' if + * the string is entirely whitespce. + */ +static char * +triml(char *s) +{ + while (isspace(*s)) + s++; + return s; +} + +/** + * @brief Arguments that are used to initialize a ztpd instance. + */ +struct ztpd_args { + struct ztp_settings *settings; + struct ztp_dbus_client *dbus_client; + struct ztp_dbus_server *dbus_server; + struct event_loop *loop; + struct ztp_wpa_supplicant *wpas; +}; + +/** + * @brief Settings-changed event handler context. This is used to pass the ztpd + * service pointer to the event handler such that it can be used to provide it + * new settings. + */ +struct ztp_settings_changed_event_context { + struct ztpd *ztpd; + struct ztpd_args *ztpd_args; +}; + +/** + * @brief Cycle initialization of ztpd. + * + * @param ztpd The instance to cycle. + * @param args The arguments to initialize ztpd with. + * @return int 0 if re-initialization succeeded, non-zero otherwise. + */ +static int +ztpd_reinitialize(struct ztpd *ztpd, struct ztpd_args *args) +{ + ztpd_uninitialize(ztpd); + + int ret = ztpd_initialize(ztpd, args->settings, args->loop, args->dbus_client, args->dbus_server, args->wpas); + if (ret < 0) { + zlog_error("failed to reinitialize ztpd (%d)", ret); + return ret; + } + + return 0; +} + +/** + * @brief ztp settings changed handler. This function will be invoked each time + * the ztp settings are changed on-disk. It will determine how to handle the + * change and supply new settings to ztpd. + * + * @param settings_old The old settings that have changed. + * @param changed_event The settings-changed event payload. + * @param context The context previously associated with the settings-changed handler. + */ +static void +on_settings_changed(struct ztp_settings *settings_old, struct ztp_settings_changed_event *changed_event, void *context) +{ + __unused(changed_event); + + struct ztp_settings_changed_event_context *changed_event_context = (struct ztp_settings_changed_event_context *)context; + const char *config_file = settings_old->config_file; + + struct ztp_settings *settings = NULL; + int ret = ztp_settings_parse(config_file, &settings); + if (ret < 0) { + zlog_error("failed to parse updated ztp settings file %s (%d)", config_file, ret); + return; + } + + ret = ztp_settings_register_change_handler(settings, on_settings_changed, context); + if (ret < 0) { + zlog_warning("failed to register settings changed handler (%d); future settings changes reflected on process restart", ret); + } + + changed_event_context->ztpd_args->settings = settings; + ztp_dbus_server_update_settings(changed_event_context->ztpd_args->dbus_server, settings); + ztpd_reinitialize(changed_event_context->ztpd, changed_event_context->ztpd_args); + + ztp_settings_destroy(&settings_old); +} + +/** + * @brief Processes an update for a file descriptor used for monitoring + * traditional process signals. + * + * @param fd The file descriptor which signaled an update. + * @param context The ztpd event loop instance. + */ +static void +on_signal_received(int fd, void *context) +{ + struct signalfd_siginfo siginfo; + ssize_t bytes = read(fd, &siginfo, sizeof siginfo); + if (bytes < (ssize_t)(sizeof siginfo)) { + zlog_warning("siginfo read returned too little data"); + return; + } + + switch (siginfo.ssi_signo) { + case SIGINT: + case SIGTERM: + event_loop_stop((struct event_loop *)context); + break; + default: + break; + } +} + +/** + * @brief Register for file descriptor signal updates. + * + * This function registers a file descriptor to receive updates for a set of + * signals. Normally, signals must be handled by dedicated signal handlers + * which have severe restrictions on the code they can execute (since a signal + * halts all execution of a process). This normally results in adding a new + * thread and using a self-pipe trick to communicate the intent of the signal, + * which is messy. + * + * We are only interested in the signals used to stop the service: SIGTERM (eg. + * kill) and SIGINT (eg. Ctrl+C). These signals are masked so that the + * regular/default signal handlers will be disabled, then hook these up to a + * file descriptor. + * + * @param loop The event loop to register the signal handler with. + * @param pfd_signals The output pointer for the signal handling descriptor. + * @return int The status of the operation; 0 if successful, -1 otherwise. + */ +static int +register_terminate_signals(struct event_loop *loop, int *pfd_signals) +{ + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGTERM); + sigaddset(&mask, SIGINT); + + if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) { + zlog_error("failed to block terminate signals"); + return -1; + } + + int fd_signals = signalfd(-1, &mask, 0); + if (fd_signals < 0) { + zlog_error("failed to allocate signal fd"); + return -1; + } + + int ret = event_loop_register_event(loop, EPOLLIN, fd_signals, on_signal_received, loop); + if (ret < 0) { + zlog_error("failed to register event for monitoring termination signals (%d)", ret); + return ret; + } + + *pfd_signals = fd_signals; + return 0; +} + +/** + * @brief Main program entrypoint. + * + * @param argc + * @param argv + * @return int + */ +int +main(int argc, char *argv[]) +{ + int ret, signalfd = -1; + bool daemonize = false; + char *config_file = ZTP_DEFAULT_CONFIG_PATH; + struct ztp_settings *settings; + struct ztpd ztpd; + struct ztp_dbus_client dbus_client; + struct ztp_dbus_server dbus_server; + struct event_loop loop; + struct ztp_wpa_supplicant wpas; + struct ztp_dbus_network_configuration_manager *network_configuration_manager; + + // Process command line options. + for (;;) { + int opt = getopt(argc, argv, "bc:"); + if (opt < 0) { + break; + } + + switch (opt) { + case 'b': + daemonize = true; + break; + case 'c': + config_file = triml(optarg); + break; + default: + break; + } + } + + ret = ztp_settings_parse(config_file, &settings); + if (ret < 0) { + zlog_panic("failed to parse settings from %s (%d)", config_file, ret); + return -1; + } + + ret = event_loop_initialize(&loop); + if (ret < 0) { + zlog_panic("failed to initialize event loop (%d)", ret); + return -1; + } + + ret = ztp_dbus_initialize(&dbus_client, &loop); + if (ret < 0) { + zlog_panic("failed to initialize dbus connector (%d)", ret); + return -1; + } + + ret = ztp_dbus_network_configuration_manager_create(&network_configuration_manager, ZTP_DBUS_SERVER_PATH); + if (ret < 0) { + zlog_error("failed to instantiate d-bus network configuration manager (%d)", ret); + return ret; + } + + ret = ztp_dbus_server_initialize(&dbus_server, settings, network_configuration_manager, ZTP_DBUS_SERVER_PATH); + if (ret < 0) { + zlog_panic("failed to initialize dbus server (%d)", ret); + return -1; + } + + ret = ztp_wpa_supplicant_initialize(&wpas); + if (ret < 0) { + zlog_panic("failed to initialize wpa supplicant connector (%d)", ret); + return -1; + } + + ret = register_terminate_signals(&loop, &signalfd); + if (ret < 0) { + zlog_panic("failed to register termination signals (%d)", ret); + return -1; + } + + ret = ztpd_initialize(&ztpd, settings, &loop, &dbus_client, &dbus_server, &wpas); + if (ret < 0) { + zlog_panic("failed to initialize ztpd (%d)", ret); + return -1; + } + + struct ztpd_args ztpd_args = { + .settings = settings, + .dbus_client = &dbus_client, + .dbus_server = &dbus_server, + .loop = &loop, + .wpas = &wpas, + }; + + struct ztp_settings_changed_event_context changed_event_context = { + .ztpd = &ztpd, + .ztpd_args = &ztpd_args, + }; + + ret = ztp_settings_register_change_handler(settings, on_settings_changed, &changed_event_context); + if (ret < 0) { + zlog_panic("failed to register settings changed event handler (%d)", ret); + return -1; + } + + // Run the process in the background if requested. + if (daemonize) { + if (daemon(0, 0)) { + zlog_panic("failed to daemonize"); + return -1; + } + } + + ztpd_run(&ztpd); + + zlog_debug("event loop completed"); + + ztpd_uninitialize(&ztpd); + ztp_dbus_uninitialize(&dbus_client); + ztp_dbus_server_uninitialize(&dbus_server); + ztp_settings_destroy(&ztpd_args.settings); + event_loop_uninitialize(&loop); + + close(signalfd); + + zlog_debug("exiting"); + + return 0; +} diff --git a/src/ztpd/systemd/CMakeLists.txt b/src/ztpd/systemd/CMakeLists.txt new file mode 100644 index 0000000..a4574ca --- /dev/null +++ b/src/ztpd/systemd/CMakeLists.txt @@ -0,0 +1,15 @@ + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/ztpd.service.in + ${CMAKE_CURRENT_BINARY_DIR}/ztpd.service +) +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/ztpd.service + DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/systemd/system +) +install( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/com.microsoft.ztp1.service + DESTINATION ${CMAKE_INSTALL_FULL_DATADIR}/dbus-1/system-services +) diff --git a/src/ztpd/systemd/com.microsoft.ztp1.service b/src/ztpd/systemd/com.microsoft.ztp1.service new file mode 100644 index 0000000..4c27d63 --- /dev/null +++ b/src/ztpd/systemd/com.microsoft.ztp1.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=com.microsoft.ztp1 +Exec=/bin/false +User=root +SystemdService=ztpd.service \ No newline at end of file diff --git a/src/ztpd/systemd/ztpd.service.in b/src/ztpd/systemd/ztpd.service.in new file mode 100644 index 0000000..97bff75 --- /dev/null +++ b/src/ztpd/systemd/ztpd.service.in @@ -0,0 +1,13 @@ +[Unit] +Description=Wi-Fi Zero Touch Provisioning Daemon +Before=network.target +Wants=network.target + +[Service] +Type=dbus +BusName=com.microsoft.ztp1 +Restart=on-failure +ExecStart=${CMAKE_INSTALL_FULL_SBINDIR}/ztpd -c ${CMAKE_INSTALL_FULL_SYSCONFDIR}/ztpd/config.json + +[Install] +Alias=dbus-com.microsoft.ztp1.service diff --git a/src/ztpd/ztpd.c b/src/ztpd/ztpd.c new file mode 100644 index 0000000..96e63a0 --- /dev/null +++ b/src/ztpd/ztpd.c @@ -0,0 +1,519 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "dpp.h" +#include "event_loop.h" +#include "time_utils.h" +#include "ztp_configurator.h" +#include "ztp_dbus_client.h" +#include "ztp_dbus_configurator.h" +#include "ztp_dbus_server.h" +#include "ztp_enrollee.h" +#include "ztp_log.h" +#include "ztp_settings.h" +#include "ztp_systemd.h" +#include "ztp_wpa_supplicant.h" +#include "ztpd.h" +#include "ztpd_ui.h" + +/** + * @brief Deactivate the enrollee device role. + * + * @param ztpd The ztpd global instance. + * @param enrollee The enrollee to deactivate. + */ +static void +ztpd_device_role_deactivate_enrollee(struct ztpd *ztpd, struct ztp_enrollee **enrollee) +{ + __unused(ztpd); + + ztp_enrollee_destroy(enrollee); +} + +/** + * @brief Activate the enrollee device role. + * + * @param interface The interface to activate the enrollee role on. + * @return int 0 if the enrollee role was successfully activated, non-zero otherwise. + */ +static int +ztpd_device_role_activate_enrollee(struct ztpd *ztpd, const char *interface, struct ztp_enrollee_settings *settings, struct ztp_enrollee **penrollee) +{ + struct ztp_enrollee *enrollee; + int ret = ztp_enrollee_create(interface, settings, ztpd->wpas, ztpd->dbus, ztpd->loop, &enrollee); + if (ret < 0) { + zlog_error_if(interface, "failed to create enrollee (%d)", ret); + return ret; + } + + *penrollee = enrollee; + + return 0; +} + +/** + * @brief Activate the configurator device role. + * + * @param interface The interface to activate the configurator role on. + * @return int 0 if the configurator role was successfully activated, non-zero otherwise. + */ +int +ztpd_device_role_activate_configurator(struct ztpd *ztpd, const char *interface, struct ztp_configurator_settings *settings, struct ztp_configurator **pconfigurator) +{ + struct ztp_configurator *configurator; + int ret = ztp_configurator_create(interface, settings, ztpd->loop, &configurator); + if (ret < 0) { + zlog_error_if(interface, "failed to create configurator (%d)", ret); + return ret; + } + + ret = ztp_dbus_configurator_register(ztpd->dbus_srv, configurator); + if (ret < 0) { + zlog_error_if(interface, "failed to register configurator with d-bus (%d)", ret); + ztp_configurator_destroy(&configurator); + return ret; + } + + *pconfigurator = configurator; + + return 0; +} + +/** + * @brief Deactivate the configurator device role. + * + * @param ztpd The global ztpd instance. + * @param pconfigurator The configurator to deactivate. + */ +static void +ztpd_device_role_deactivate_configurator(struct ztpd *ztpd, struct ztp_configurator **pconfigurator) +{ + if (!pconfigurator || !*pconfigurator) + return; + + int ret = ztp_dbus_configurator_unregister(ztpd->dbus_srv, *pconfigurator); + if (ret < 0) + zlog_warning_if((*pconfigurator)->interface, "failed to unregister configurator d-bus object (%d)", ret); + + ztp_configurator_destroy(pconfigurator); +} + +/** + * @brief Unenforce any dependencies of the dpp role function. This includes any + * configured, dependent systemd activation unit. + * + * @param ztpd The global ztpd instance. + * @param settings The device role settings to use for deactivation. + */ +static void +ztpd_device_role_deactivate_unenforce_dependencies(struct ztpd *ztpd, struct ztp_device_role_settings *settings) +{ + if (settings->activation_unit) { + int ret = ztp_systemd_unit_stop(ztpd->dbus->bus, settings->activation_unit); + if (ret < 0) { + zlog_error_if(settings->interface, "failed to stop deactivation unit %s dependency (%d)", settings->activation_unit, ret); + return; + } + } +} + +/** + * @brief Deactivate a device role. + * + * @param ztpd The global ztpd instance. + * @param instance The device role instance to deaactivate. + */ +static void +ztpd_device_role_deactivate(struct ztpd *ztpd, struct ztpd_device_role_instance *instance) +{ + ztpd_device_role_deactivate_unenforce_dependencies(ztpd, instance->settings); + + switch (instance->settings->role) { + case DPP_DEVICE_ROLE_ENROLLEE: + ztpd_device_role_deactivate_enrollee(ztpd, &instance->enrollee); + break; + case DPP_DEVICE_ROLE_CONFIGURATOR: + ztpd_device_role_deactivate_configurator(ztpd, &instance->configurator); + break; + default: + break; + } + + list_del(&instance->list); + free(instance); +} + +/** + * @brief Deactivates all enabled device roles. + * + * @param ztpd The global ztpd instance. +*/ +static void +ztpd_device_role_deactivate_all(struct ztpd *ztpd) +{ + struct ztpd_device_role_instance *instance; + struct ztpd_device_role_instance *instancetmp; + list_for_each_entry_safe (instance, instancetmp, &ztpd->instances, list) { + ztpd_device_role_deactivate(ztpd, instance); + } +} + +/** + * @brief Enforce any dependencies of the dpp role function. This includes any + * configured, dependent systemd activation unit. + * + * @param ztpd The global ztpd instance. + * @param settings The device role settings to use for activation. + * @return int 0 if all dependencies were enforced, non-zero otherwise. + */ +static int +ztpd_device_role_activate_enforce_dependencies(struct ztpd *ztpd, struct ztp_device_role_settings *settings) +{ + if (settings->activation_unit) { + int ret = ztp_systemd_unit_start(ztpd->dbus->bus, settings->activation_unit); + if (ret < 0) { + zlog_error_if(settings->interface, "failed to start activation unit %s dependency (%d)", settings->activation_unit, ret); + return ret; + } + } + + return 0; +} + +/** + * @brief Activate a device role. + * + * This creates a new instance of the device role using the specified settings. + * + * @param ztpd The global ztpd instance. + * @param settings The device role settings to use for activation. + * @return int 0 if the role was activated and added to ztpds tracking list, non-zero otherwise. + */ +static int +ztpd_device_role_activate(struct ztpd *ztpd, struct ztp_device_role_settings *settings) +{ + int ret; + const char *role = dpp_device_role_str(settings->role); + if (!ztpd->settings->dpp_roles_activated[settings->role]) { + zlog_info_if(settings->interface, "dpp device role '%s' not activated; ignoring", role); + return -ENODEV; + } + + struct ztpd_device_role_instance *instance = calloc(1, sizeof *instance); + if (!instance) { + zlog_error_if(settings->interface, "failed to allocate memory for device role instance"); + return -ENOMEM; + } + + ret = ztpd_device_role_activate_enforce_dependencies(ztpd, settings); + if (ret < 0) { + zlog_error_if(settings->interface, "failed to enforce dependencies (%d)", ret); + free(instance); + return ret; + } + + switch (settings->role) { + case DPP_DEVICE_ROLE_ENROLLEE: + ret = ztpd_device_role_activate_enrollee(ztpd, settings->interface, &settings->enrollee, &instance->enrollee); + break; + case DPP_DEVICE_ROLE_CONFIGURATOR: + ret = ztpd_device_role_activate_configurator(ztpd, settings->interface, &settings->configurator, &instance->configurator); + break; + default: + ret = -EINVAL; + break; + } + + if (ret < 0) { + zlog_error_if(settings->interface, "failed to activate dpp device role '%s' (%d)", role, ret); + return ret; + } + + instance->settings = settings; + + zlog_info_if(settings->interface, "dpp device role '%s' activated", role); + list_add(&instance->list, &ztpd->instances); + + return 0; +} + +/** + * @brief Activates all enabled device roles. + * + * @param ztpd The global ztpd instance. +*/ +static void +ztpd_device_role_activate_all(struct ztpd *ztpd) +{ + struct ztp_device_role_settings_entry *entry; + list_for_each_entry (entry, &ztpd->settings->role_settings, list) { + int ret = ztpd_device_role_activate(ztpd, &entry->settings); + if (ret < 0 && ret != -ENODEV) { + zlog_warning_if(entry->settings.interface, "failed to process device role (%d)", ret); + continue; + } + } +} + +/** + * @brief Handler function invoked when ui activation state changes. + * + * @param ztpd The global ztpd instance. + * @param activated Describes the current (desired) activation state. + */ +static void +on_ui_activation_changed(struct ztpd *ztpd, bool activated) +{ + static const char *state[] = { + "deactivated", + "activated", + }; + zlog_info("Δstate[ui] %s -> %s", state[!activated], state[activated]); + + int ret = activated ? ztpd_ui_activate(ztpd) : ztpd_ui_deactivate(ztpd); + if (ret < 0) + zlog_warning("failed to enforce ui mode change to %s (%d)", state[activated], ret); +} + +/** + * @brief Processes an update of the ui activation file descriptor used for + * monitoring when the ui should be activated and de-activated. + * + * @param fd The file descriptor that has an update. + * @param context The global ztpd instance. + */ +static void +process_fd_update_ui_activation(int fd, void *context) +{ + struct ztpd *ztpd = (struct ztpd *)context; + + struct gpiod_line_event event; + if (gpiod_line_event_read_fd(fd, &event) < 0) { + zlog_warning("gpio line singaled, but failed to read event(s)"); + return; + } + + struct timespec now; + int ret = clock_gettime(CLOCK_BOOTTIME, &now); + if (ret < 0) { + ret = errno; + zlog_warning("failed to retrieve current time (%d); unable to debounce button press", ret); + return; + } + + switch (event.event_type) { + case GPIOD_LINE_EVENT_RISING_EDGE: { + if (ztpd->ui_activation_gpio_line_last_rising_edge.tv_sec == 0 && + ztpd->ui_activation_gpio_line_last_rising_edge.tv_nsec == 0) { + ztpd->ui_activation_gpio_line_last_rising_edge = now; + } + return; + } + case GPIOD_LINE_EVENT_FALLING_EDGE: { + struct timespec elapsed = timespec_diff(&now, &ztpd->ui_activation_gpio_line_last_rising_edge); + if (timespeccmp(&elapsed, &ztpd->ui_activation_gpio_debounce, <)) { + zlog_debug("debounce time not met; ignoring spurious falling edge"); + return; + } + break; + } + default: + zlog_warning("unexpected gpio line signal (%d) received; ignoring", event.event_type); + return; + } + + ztpd->ui_activation_gpio_line_last_rising_edge.tv_sec = 0; + ztpd->ui_activation_gpio_line_last_rising_edge.tv_nsec = 0; + + // refresh unit active state so it can be accurately toggled + char *active_state; + ret = ztpd_systemd_unit_get_activestate(ztpd->dbus->bus, ztpd->settings->ui_activation_unit, &active_state); + if (ret < 0) { + zlog_warning("failed to determine ui activation unit state (%d); using cached value", ret); + } else { + ztpd->ui_activated = strcmp(active_state, "active") == 0; + free(active_state); + } + + ztpd->ui_activated = !ztpd->ui_activated; + on_ui_activation_changed(ztpd, ztpd->ui_activated); +} + +/** + * @brief Runs the main ztpd event loop. + * + * This is the main event loop and primary thread of execution for the daemon. + * epoll is used to wait for changes on a set of monitored file descriptors. + * + * @param ztpd The global ztpd instance. + */ +void +ztpd_run(struct ztpd *ztpd) +{ + event_loop_run(ztpd->loop); +} + +/** + * @brief Uninitialize the ui activation context. + * + * @param ztpd The global ztpd instance. + */ +static void +ztpd_uninitialize_ui_activation(struct ztpd *ztpd) +{ + if (ztpd->fd_uiactivation != -1) { + event_loop_unregister_event(ztpd->loop, ztpd->fd_uiactivation); + ztpd->fd_uiactivation = -1; + } + + if (ztpd->ui_activation_gpio_chip) { + gpiod_chip_close(ztpd->ui_activation_gpio_chip); + ztpd->ui_activation_gpio_chip = NULL; + } +} + +/** + * @brief Default delay value, in milliseconds, to use to debounce a button + * press. This will only be used if no debounce value is specified in settings. + */ +#define DEFAULT_GPIO_BUTTON_DEBOUNCE_DELAY_MS 50 + +/** + * @brief Initializes the ui activation trigger(s). + + * @param ztpd The global ztpd instance. + * @return int The status of initialization; 0 if successful, -1 otherwise. + */ +static int +ztpd_initialize_ui_activation(struct ztpd *ztpd) +{ + int ret; + + if (!ztpd->settings->ui_activation_gpio) + return 0; + + struct gpiod_chip *chip = gpiod_chip_open_lookup(ztpd->settings->ui_activation_gpio_chip); + if (!chip) { + zlog_error("failed to open gpio chip '%s'", ztpd->settings->ui_activation_gpio_chip); + return -1; + } + + struct gpiod_line *line = NULL; + if (ztpd->settings->ui_activation_gpio_line_name) + line = gpiod_chip_find_line(chip, ztpd->settings->ui_activation_gpio_line_name); + if (!line && ztpd->settings->ui_activation_gpio_line >= 0) + line = gpiod_chip_get_line(chip, (unsigned)ztpd->settings->ui_activation_gpio_line); + if (!line) { + zlog_error("failed to open gpio line"); + goto fail; + } + + if (gpiod_line_request_both_edges_events(line, "ztpd") < 0) { + zlog_error("failed to configure gpio line for both edge events"); + goto fail; + } + + int fd = gpiod_line_event_get_fd(line); + if (fd < 0) { + zlog_error("failed to obtain gpio event fd"); + goto fail; + } + + ret = event_loop_register_event(ztpd->loop, EPOLLIN, fd, process_fd_update_ui_activation, ztpd); + if (ret < 0) { + zlog_error("failed to register event for monitoring gpio events (%d)", ret); + goto fail; + } + + if (ztpd->settings->ui_activation_unit) { + char *active_state; + ret = ztpd_systemd_unit_get_activestate(ztpd->dbus->bus, ztpd->settings->ui_activation_unit, &active_state); + if (ret < 0) { + zlog_warning("failed to determine ui activation unit state (%d); assuming inactive", ret); + ztpd->ui_activated = false; + } else { + ztpd->ui_activated = strcmp(active_state, "active") == 0; + free(active_state); + } + + zlog_debug("ui activation unit (%s) state %s", ztpd->settings->ui_activation_unit, ztpd->ui_activated ? "active" : "inactive"); + } + + ztpd->fd_uiactivation = fd; + ztpd->ui_activation_gpio_chip = chip; + ztpd->ui_activation_gpio_line = line; + ztpd->ui_activation_gpio_line_last_rising_edge.tv_nsec = 0; + ztpd->ui_activation_gpio_line_last_rising_edge.tv_sec = 0; + ztpd->ui_activation_gpio_debounce.tv_sec = 0; + ztpd->ui_activation_gpio_debounce.tv_nsec = ztpd->settings->ui_activation_gpio_delay; + + if (ztpd->ui_activation_gpio_debounce.tv_nsec <= 0) + ztpd->ui_activation_gpio_debounce.tv_nsec = DEFAULT_GPIO_BUTTON_DEBOUNCE_DELAY_MS; + ztpd->ui_activation_gpio_debounce.tv_nsec *= NSEC_PER_MSEC; + + ret = 0; +out: + return ret; +fail: + ztpd_uninitialize_ui_activation(ztpd); + ret = -1; + goto out; +} + +/** + * @brief Uninitialize the ztpd daemon. + * + * @param ztpd The global daemon instance to uninitialize. + */ +void +ztpd_uninitialize(struct ztpd *ztpd) +{ + ztpd_device_role_deactivate_all(ztpd); + ztpd_uninitialize_ui_activation(ztpd); +} + +/** + * @brief Initialize the ztpd daemon. + * + * @param ztpd The instance to initialize. + * @param settings The options with which to initialize the daemon. + * @param loop The event loop to run the daemon with. + * @param dbus A dbus connector instance. + * @param dbus_srv A dbus server instance. + * @param wpas A wpa supplicant connector instance. + * @return int 0 if the instance was successfully initialized, non-zero otherwise. + */ +int +ztpd_initialize(struct ztpd *ztpd, struct ztp_settings *settings, struct event_loop *loop, struct ztp_dbus_client *dbus, struct ztp_dbus_server *dbus_srv, struct ztp_wpa_supplicant *wpas) +{ + explicit_bzero(ztpd, sizeof *ztpd); + + INIT_LIST_HEAD(&ztpd->instances); + ztpd->wpas = wpas; + ztpd->dbus = dbus; + ztpd->loop = loop; + ztpd->settings = settings; + ztpd->dbus_srv = dbus_srv; + ztpd->fd_uiactivation = -1; + + int ret = ztpd_initialize_ui_activation(ztpd); + if (ret < 0) { + zlog_error("failed to initialize ztpd ui activation (%d)", ret); + ztpd_uninitialize(ztpd); + return ret; + } + + ztpd_device_role_activate_all(ztpd); + return 0; +} diff --git a/src/ztpd/ztpd.h b/src/ztpd/ztpd.h new file mode 100644 index 0000000..acd2877 --- /dev/null +++ b/src/ztpd/ztpd.h @@ -0,0 +1,86 @@ + +#ifndef __ZTPD_H__ +#define __ZTPD_H__ + +#include + +#include +#include + +struct ztp_dbus_client; +struct ztp_dbus_server; +struct ztp_wpa_supplicant; +struct ztp_settings; + +struct ztpd_device_role_instance { + struct list_head list; + struct ztp_device_role_settings *settings; + union { + struct ztp_enrollee *enrollee; + struct ztp_configurator *configurator; + }; +}; + +/** + * @brief The global daemon instance structure. + */ +struct ztpd { + // generic bits + struct list_head interfaces; + struct ztp_settings *settings; + int terminate_pending; + + // epoll + int fd_uiactivation; + struct event_loop *loop; + + // child event control interfaces + struct ztp_dbus_client *dbus; + struct ztp_dbus_server *dbus_srv; + struct ztp_wpa_supplicant *wpas; + + // role instance pointers + struct list_head instances; + + // ui activation + bool ui_activated; + struct gpiod_chip *ui_activation_gpio_chip; + struct gpiod_line *ui_activation_gpio_line; + struct timespec ui_activation_gpio_debounce; + struct timespec ui_activation_gpio_line_last_rising_edge; +}; + +/** + * @brief Initialize the ztpd daemon. + * + * @param ztpd The instance to initialize. + * @param settings The options with which to initialize the daemon. + * @param loop The event loop to run the daemon with. + * @param dbus A dbus connector instance. + * @param dbus_srv A dbus server instance. + * @param wpas A wpa supplicant connector instance. + * @return int 0 if the instance was successfully initialized, non-zero otherwise. + */ +int +ztpd_initialize(struct ztpd *ztpd, struct ztp_settings *settings, struct event_loop *loop, struct ztp_dbus_client *dbus, struct ztp_dbus_server *dbus_srv, struct ztp_wpa_supplicant *wpas); + +/** + * @brief Uninitialize the ztpd daemon. + * + * @param ztpd The global daemon instance to uninitialize. + */ +void +ztpd_uninitialize(struct ztpd *ztpd); + +/** + * @brief Runs the main ztpd event loop. + * + * This is the main event loop and primary thread of execution for the daemon. + * epoll is used to wait for changes on a set of monitored file descriptors. + * + * @param ztpd The global ztpd instance. + */ +void +ztpd_run(struct ztpd *ztpd); + +#endif diff --git a/src/ztpd/ztpd_ui.c b/src/ztpd/ztpd_ui.c new file mode 100644 index 0000000..212875a --- /dev/null +++ b/src/ztpd/ztpd_ui.c @@ -0,0 +1,55 @@ + +#include "ztpd_ui.h" +#include "ztp_dbus_client.h" +#include "ztp_log.h" +#include "ztp_settings.h" +#include "ztp_systemd.h" +#include "ztpd.h" + +/** + * @brief Activates the ui service. + * + * @param ztpd The global ztpd instance. + * @return int + */ +int +ztpd_ui_activate(struct ztpd *ztpd) +{ + int ret; + if (ztpd->settings->ui_activation_unit) { + ret = ztp_systemd_unit_start(ztpd->dbus->bus, ztpd->settings->ui_activation_unit); + if (ret < 0) { + zlog_warning("failed to start unit %s", ztpd->settings->ui_activation_unit); + } else { + zlog_info("started ui service unit %s", ztpd->settings->ui_activation_unit); + } + } else { + ret = 0; + } + + return ret; +} + +/** + * @brief Deactivates the ui service. + * + * @param ztpd The global ztpd instance. + * @return int + */ +int +ztpd_ui_deactivate(struct ztpd *ztpd) +{ + int ret; + if (ztpd->settings->ui_activation_unit) { + ret = ztp_systemd_unit_stop(ztpd->dbus->bus, ztpd->settings->ui_activation_unit); + if (ret < 0) { + zlog_warning("failed to stop unit %s", ztpd->settings->ui_activation_unit); + } else { + zlog_info("stopped ui service unit %s", ztpd->settings->ui_activation_unit); + } + } else { + ret = 0; + } + + return ret; +} diff --git a/src/ztpd/ztpd_ui.h b/src/ztpd/ztpd_ui.h new file mode 100644 index 0000000..b7b7faf --- /dev/null +++ b/src/ztpd/ztpd_ui.h @@ -0,0 +1,25 @@ + +#ifndef __ZTPD_UI_H__ +#define __ZTPD_UI_H__ + +struct ztpd; + +/** + * @brief Activates the ui service. + * + * @param ztpd The global ztpd instance. + * @return int + */ +int +ztpd_ui_activate(struct ztpd *ztpd); + +/** + * @brief Deactivates the ui service. + * + * @param ztpd The global ztpd instance. + * @return int + */ +int +ztpd_ui_deactivate(struct ztpd *ztpd); + +#endif //__ZTPD_UI_H__