diff --git a/examples/provisioning/ble_prov/README.md b/examples/provisioning/ble_prov/README.md index d6d5223a9c..fa49ba8e4d 100644 --- a/examples/provisioning/ble_prov/README.md +++ b/examples/provisioning/ble_prov/README.md @@ -80,7 +80,7 @@ Make sure to note down the BLE device name (starting with PROV_) displayed in th In a separate terminal run the `esp_prov.py` script under `$IDP_PATH/tools/esp_prov` directory (please replace `myssid` and `mypassword` with the credentials of the AP to which the device is supposed to connect to after provisioning). Assuming default example configuration : ``` -python esp_prov.py --ssid myssid --passphrase mypassword --sec_ver 1 --pop abcd1234 --transport ble --ble_devname PROV_261FCC +python esp_prov.py --transport ble --service_name PROV_261FCC --sec_ver 1 --pop abcd1234 --ssid myssid --passphrase mypassword ``` Above command will perform the provisioning steps, and the monitor log should display something like this : @@ -150,7 +150,6 @@ Or, enable `Reset Provisioning` option under `Example Configuration` under menuc If the platform requirement, for running `esp_prov` is not satisfied, then the script execution will fallback to console mode, in which case the full process (involving user inputs) will look like this : ``` -==== Esp_Prov Version: V0.1 ==== BLE client is running in console mode This could be due to your platform not being supported or dependencies not being met Please ensure all pre-requisites are met to run the full fledged client diff --git a/examples/provisioning/ble_prov/ble_prov_test.py b/examples/provisioning/ble_prov/ble_prov_test.py index f379e7d3ac..01277117ed 100644 --- a/examples/provisioning/ble_prov/ble_prov_test.py +++ b/examples/provisioning/ble_prov/ble_prov_test.py @@ -76,7 +76,7 @@ def test_examples_provisioning_ble(env, extra_data): raise RuntimeError("Failed to get security") print("Getting transport") - transport = esp_prov.get_transport(provmode, None, devname) + transport = esp_prov.get_transport(provmode, devname) if transport is None: raise RuntimeError("Failed to get transport") diff --git a/examples/provisioning/console_prov/README.md b/examples/provisioning/console_prov/README.md index cb7551c614..0732c54505 100644 --- a/examples/provisioning/console_prov/README.md +++ b/examples/provisioning/console_prov/README.md @@ -65,14 +65,12 @@ I (398) app_prov: Console provisioning started In a separate terminal run the `esp_prov.py` script under `$IDP_PATH/tools/esp_prov` directory (please replace `myssid` and `mypassword` with the credentials of the AP to which the device is supposed to connect to after provisioning). Assuming default example configuration, the script should be run as follows : ``` -python esp_prov.py --ssid myssid --passphrase mypassword --sec_ver 1 --pop abcd1234 --transport console +python esp_prov.py --transport console --proto_ver "V0.1" --sec_ver 1 --pop abcd1234 --ssid myssid --passphrase mypassword ``` A console will open up and the `Client->Device` commands have to be copied manually to the serial monitor console prompt : ``` -==== Esp_Prov Version: V0.1 ==== - ==== Verifying protocol version ==== Client->Device msg : proto-ver 0 56302e31 Enter device->client msg : @@ -111,8 +109,6 @@ This is helpful in understanding the provisioning process and the order in which The full execution sequence of `esp_prov`, as seen on the console, is shown here : ``` -==== Esp_Prov Version: V0.1 ==== - ==== Verifying protocol version ==== Client->Device msg : proto-ver 0 56302e31 Enter device->client msg : 53554343455353 diff --git a/examples/provisioning/custom_config/README.md b/examples/provisioning/custom_config/README.md index 6a07e69f60..82ed6ffe31 100644 --- a/examples/provisioning/custom_config/README.md +++ b/examples/provisioning/custom_config/README.md @@ -68,7 +68,7 @@ I (519482) tcpip_adapter: softAP assign IP to station,IP is: 192.168.4.2 In a separate terminal run the `esp_prov.py` script under `$IDP_PATH/tools/esp_prov` directory (please replace the values corresponding to the parameters `--custom_info` and `--custom_ver` with your desired values for the custom configuration). Assuming default example configuration, the script should be run as follows : ``` -python esp_prov.py --ssid myssid --passphrase mypassword --sec_ver 0 --transport softap --softap_endpoint 192.168.4.1:80 --custom_config --custom_info "some string" --custom_ver 4321 +python esp_prov.py --transport softap --service_name "192.168.4.1:80" --sec_ver 0 --ssid myssid --passphrase mypassword --custom_config --custom_info "some string" --custom_ver 4321 ``` Above command will perform the provisioning steps, and the monitor log should display something like this : diff --git a/examples/provisioning/manager/README.md b/examples/provisioning/manager/README.md index 5bba6aad4c..b59f83710b 100644 --- a/examples/provisioning/manager/README.md +++ b/examples/provisioning/manager/README.md @@ -74,10 +74,10 @@ I (1045) wifi_prov_mgr: Provisioning started with service name : PROV_261FCC Make sure to note down the BLE device name (starting with `PROV_`) displayed in the serial monitor log (eg. PROV_261FCC). This will depend on the MAC ID and will be unique for every device. -In a separate terminal run the `esp_prov.py` script under `$IDP_PATH/tools/esp_prov` directory (please replace `myssid` and `mypassword` with the credentials of the AP to which the device is supposed to connect to after provisioning). Assuming default example configuration : +In a separate terminal run the `esp_prov.py` script under `$IDP_PATH/tools/esp_prov` directory (make sure to replace `myssid` and `mypassword` with the credentials of the AP to which the device is supposed to connect to after provisioning). Assuming default example configuration, which uses protocomm security scheme 1 and proof of possession PoP based authentication : ``` -python esp_prov.py --ssid myssid --passphrase mypassword --sec_ver 1 --pop abcd1234 --transport ble --ble_devname PROV_261FCC +python esp_prov.py --transport ble --service_name PROV_261FCC --sec_ver 1 --pop abcd1234 --ssid myssid --passphrase mypassword ``` Above command will perform the provisioning steps, and the monitor log should display something like this : @@ -109,6 +109,54 @@ I (54355) app: Hello World! I (55355) app: Hello World! ``` +### Wi-Fi Scanning + +Provisioning manager also supports providing real-time Wi-Fi scan results (performed on the device) during provisioning. This allows the client side applications to choose the AP for which the device Wi-Fi station is to be configured. Various information about the visible APs is available, like signal strength (RSSI) and security type, etc. Also, the manager now provides capabilities information which can be used by client applications to determine the security type and availability of specific features (like `wifi_scan`). + +When using the scan based provisioning, we don't need to specify the `--ssid` and `--passphrase` fields explicitly: + +``` +python esp_prov.py --transport ble --service_name PROV_261FCC --pop abcd1234 +``` + +See below the sample output from `esp_prov` tool on running above command: + +``` +Connecting... +Connected +Getting Services... +Security scheme determined to be : 1 + +==== Starting Session ==== +==== Session Established ==== + +==== Scanning Wi-Fi APs ==== +++++ Scan process executed in 1.9967520237 sec +++++ Scan results : 5 + +++++ Scan finished in 2.7374596596 sec +==== Wi-Fi Scan results ==== +S.N. SSID BSSID CHN RSSI AUTH +[ 1] MyHomeWiFiAP 788a20841996 1 -45 WPA2_PSK +[ 2] MobileHotspot 7a8a20841996 11 -46 WPA2_PSK +[ 3] MyHomeWiFiAP 788a208daa26 11 -54 WPA2_PSK +[ 4] NeighborsWiFiAP 8a8a20841996 6 -61 WPA2_PSK +[ 5] InsecureWiFiAP dca4caf1227c 7 -74 Open + +Select AP by number (0 to rescan) : 1 +Enter passphrase for MyHomeWiFiAP : + +==== Sending Wi-Fi credential to esp32 ==== +==== Wi-Fi Credentials sent successfully ==== + +==== Applying config to esp32 ==== +==== Apply config sent successfully ==== + +==== Wi-Fi connection state ==== +++++ WiFi state: connected ++++ +==== Provisioning was successful ==== +``` + ## Troubleshooting ### Provisioning failed diff --git a/examples/provisioning/manager/wifi_prov_mgr_test.py b/examples/provisioning/manager/wifi_prov_mgr_test.py index 25bb463d2e..ea7172dc4d 100644 --- a/examples/provisioning/manager/wifi_prov_mgr_test.py +++ b/examples/provisioning/manager/wifi_prov_mgr_test.py @@ -76,7 +76,7 @@ def test_examples_wifi_prov_mgr(env, extra_data): raise RuntimeError("Failed to get security") print("Getting transport") - transport = esp_prov.get_transport(provmode, None, devname) + transport = esp_prov.get_transport(provmode, devname) if transport is None: raise RuntimeError("Failed to get transport") diff --git a/examples/provisioning/softap_prov/README.md b/examples/provisioning/softap_prov/README.md index e5445b80c8..56d092dbfa 100644 --- a/examples/provisioning/softap_prov/README.md +++ b/examples/provisioning/softap_prov/README.md @@ -81,7 +81,7 @@ I (519482) tcpip_adapter: softAP assign IP to station,IP is: 192.168.4.2 In a separate terminal run the `esp_prov.py` script under `$IDP_PATH/tools/esp_prov` directory (please replace `myssid` and `mypassword` with the credentials of the AP to which the device is supposed to connect to after provisioning). The SoftAP endpoint corresponds to the IP and port of the device on the SoftAP network, but this is usually same as the default value and may be left out. Assuming default example configuration, the script should be run as follows : ``` -python esp_prov.py --ssid myssid --passphrase mypassword --sec_ver 1 --pop abcd1234 --transport softap --softap_endpoint 192.168.4.1:80 +python esp_prov.py --transport softap --service_name "192.168.4.1:80" --sec_ver 1 --pop abcd1234 --ssid myssid --passphrase mypassword ``` Above command will perform the provisioning steps, and the monitor log should display something like this : diff --git a/examples/provisioning/softap_prov/softap_prov_test.py b/examples/provisioning/softap_prov/softap_prov_test.py index b8d8d9c7f1..bbe5289877 100644 --- a/examples/provisioning/softap_prov/softap_prov_test.py +++ b/examples/provisioning/softap_prov/softap_prov_test.py @@ -98,7 +98,7 @@ def test_examples_provisioning_softap(env, extra_data): raise RuntimeError("Failed to get security") print("Getting transport") - transport = esp_prov.get_transport(provmode, softap_endpoint, None) + transport = esp_prov.get_transport(provmode, softap_endpoint) if transport is None: raise RuntimeError("Failed to get transport") diff --git a/tools/esp_prov/README.md b/tools/esp_prov/README.md index c4fef6938c..7f8a443b00 100644 --- a/tools/esp_prov/README.md +++ b/tools/esp_prov/README.md @@ -6,7 +6,7 @@ # SYNOPSIS ``` -python esp_prov.py --transport < mode of provisioning : softap \ ble \ console > --sec_ver < Security version 0 / 1 > [ Optional parameters... ] +python esp_prov.py --transport < mode of provisioning : softap \ ble \ console > [ Optional parameters... ] ``` # DESCRIPTION @@ -53,11 +53,9 @@ Usage of `esp-prov` assumes that the provisioning app has specific protocomm end * `--pop ` (Optional) For specifying optional Proof of Possession string to use for protocomm endpoint security version 1. This option is ignored when security version 0 is in use -* `--softap_endpoint ` (Optional) (Default `192.168.4.1:80`) - For specifying the IP and port of the HTTP server on which provisioning app is running. The client must connect to the device SoftAP prior to running `esp_prov` - -* `--ble_devname ` (Optional) - For specifying name of the BLE device to which connection is to be established prior to starting provisioning process. This is only used when `--transport ble` is specified, else it is ignored. Since connection with BLE is supported only on Linux, so this option is again ignored for other platforms +* `--service_name (Optional) + When transport mode is ble, this specifies the BLE device name to which connection is to be established for provisioned. + When transport mode is softap, this specifies the HTTP server hostname / IP which is running the provisioning service, on the SoftAP network of the device which is to be provisioned. This defaults to `192.168.4.1:80` if not specified * `--custom_config` (Optional) This flag assumes the provisioning app has an endpoint called `custom-config`. Use `--custom_info` and `--custom_ver` options to specify the fields accepted by this endpoint diff --git a/tools/esp_prov/esp_prov.py b/tools/esp_prov/esp_prov.py index ad199143ae..52fb394f26 100644 --- a/tools/esp_prov/esp_prov.py +++ b/tools/esp_prov/esp_prov.py @@ -18,6 +18,7 @@ from __future__ import print_function from builtins import input import argparse +import textwrap import time import os import sys @@ -57,12 +58,16 @@ def get_security(secver, pop=None, verbose=False): return None -def get_transport(sel_transport, softap_endpoint=None, ble_devname=None): +def get_transport(sel_transport, service_name): try: tp = None if (sel_transport == 'softap'): - tp = transport.Transport_Softap(softap_endpoint) + if service_name is None: + service_name = '192.168.4.1:80' + tp = transport.Transport_HTTP(service_name) elif (sel_transport == 'ble'): + if service_name is None: + raise RuntimeError('"--service_name" must be specified for ble transport') # BLE client is now capable of automatically figuring out # the primary service from the advertisement data and the # characteristics corresponding to each endpoint. @@ -71,7 +76,7 @@ def get_transport(sel_transport, softap_endpoint=None, ble_devname=None): # in which case, the automated discovery will fail and the client # will fallback to using the provided UUIDs instead nu_lookup = {'prov-session': 'ff51', 'prov-config': 'ff52', 'proto-ver': 'ff53'} - tp = transport.Transport_BLE(devname=ble_devname, + tp = transport.Transport_BLE(devname=service_name, service_uuid='0000ffff-0000-1000-8000-00805f9b34fb', nu_lookup=nu_lookup) elif (sel_transport == 'console'): @@ -93,31 +98,53 @@ def version_match(tp, protover, verbose=False): if response.lower() == protover.lower(): return True - # Else interpret this as JSON structure containing - # information with versions and capabilities of both - # provisioning service and application - info = json.loads(response) - if info['prov']['ver'].lower() == protover.lower(): - return True + try: + # Else interpret this as JSON structure containing + # information with versions and capabilities of both + # provisioning service and application + info = json.loads(response) + if info['prov']['ver'].lower() == protover.lower(): + return True + + except ValueError: + # If decoding as JSON fails, it means that capabilities + # are not supported + return False - return False except Exception as e: on_except(e) return None -def has_capability(tp, capability, verbose=False): +def has_capability(tp, capability='none', verbose=False): + # Note : default value of `capability` argument cannot be empty string + # because protocomm_httpd expects non zero content lengths try: response = tp.send_data('proto-ver', capability) if verbose: print("proto-ver response : ", response) - info = json.loads(response) - if capability in info['prov']['cap']: - return True + try: + # Interpret this as JSON structure containing + # information with versions and capabilities of both + # provisioning service and application + info = json.loads(response) + supported_capabilities = info['prov']['cap'] + if capability.lower() == 'none': + # No specific capability to check, but capabilities + # feature is present so return True + return True + elif capability in supported_capabilities: + return True + return False - except Exception as e: + except ValueError: + # If decoding as JSON fails, it means that capabilities + # are not supported + return False + + except RuntimeError as e: on_except(e) return False @@ -239,75 +266,123 @@ def get_wifi_config(tp, sec): return None +def desc_format(*args): + desc = '' + for arg in args: + desc += textwrap.fill(replace_whitespace=False, text=arg) + "\n" + return desc + + if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Generate ESP prov payload") + parser = argparse.ArgumentParser(description=desc_format( + 'ESP Provisioning tool for configuring devices ' + 'running protocomm based provisioning service.', + 'See esp-idf/examples/provisioning for sample applications'), + formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument("--ssid", dest='ssid', type=str, - help="SSID of Wi-Fi Network", default='') - parser.add_argument("--passphrase", dest='passphrase', type=str, - help="Passphrase of Wi-Fi network", default='') + parser.add_argument("--transport", required=True, dest='mode', type=str, + help=desc_format( + 'Mode of transport over which provisioning is to be performed.', + 'This should be one of "softap", "ble" or "console"')) - parser.add_argument("--sec_ver", dest='secver', type=int, - help="Security scheme version", default=None) - parser.add_argument("--proto_ver", dest='protover', type=str, - help="Protocol version", default='') - parser.add_argument("--pop", dest='pop', type=str, - help="Proof of possession", default='') + parser.add_argument("--service_name", dest='name', type=str, + help=desc_format( + 'This specifies the name of the provisioning service to connect to, ' + 'depending upon the mode of transport :', + '\t- transport "ble" : The BLE Device Name', + '\t- transport "softap" : HTTP Server hostname or IP', + '\t (default "192.168.4.1:80")')) - parser.add_argument("--softap_endpoint", dest='softap_endpoint', type=str, - help=", http(s):// shouldn't be included", default='192.168.4.1:80') + parser.add_argument("--proto_ver", dest='version', type=str, default='', + help=desc_format( + 'This checks the protocol version of the provisioning service running ' + 'on the device before initiating Wi-Fi configuration')) - parser.add_argument("--ble_devname", dest='ble_devname', type=str, - help="BLE Device Name", default='') + parser.add_argument("--sec_ver", dest='secver', type=int, default=None, + help=desc_format( + 'Protocomm security scheme used by the provisioning service for secure ' + 'session establishment. Accepted values are :', + '\t- 0 : No security', + '\t- 1 : X25519 key exchange + AES-CTR encryption', + '\t + Authentication using Proof of Possession (PoP)', + 'In case device side application uses IDF\'s provisioning manager, ' + 'the compatible security version is automatically determined from ' + 'capabilities retrieved via the version endpoint')) - parser.add_argument("--transport", dest='provmode', type=str, - help="provisioning mode i.e console or softap or ble", default='softap') + parser.add_argument("--pop", dest='pop', type=str, default='', + help=desc_format( + 'This specifies the Proof of possession (PoP) when security scheme 1 ' + 'is used')) - parser.add_argument("--custom_config", help="Provision Custom Configuration", - action="store_true") - parser.add_argument("--custom_info", dest='custom_info', type=str, - help="Custom Config Info String", default='') - parser.add_argument("--custom_ver", dest='custom_ver', type=int, - help="Custom Config Version Number", default=2) + parser.add_argument("--ssid", dest='ssid', type=str, default='', + help=desc_format( + 'This configures the device to use SSID of the Wi-Fi network to which ' + 'we would like it to connect to permanently, once provisioning is complete. ' + 'If Wi-Fi scanning is supported by the provisioning service, this need not ' + 'be specified')) + + parser.add_argument("--passphrase", dest='passphrase', type=str, default='', + help=desc_format( + 'This configures the device to use Passphrase for the Wi-Fi network to which ' + 'we would like it to connect to permanently, once provisioning is complete. ' + 'If Wi-Fi scanning is supported by the provisioning service, this need not ' + 'be specified')) + + parser.add_argument("--custom_config", action="store_true", + help=desc_format( + 'This is an optional parameter, only intended for use with ' + '"examples/provisioning/custom_config"')) + parser.add_argument("--custom_info", dest='custom_info', type=str, default='', + help=desc_format( + 'Custom Config Info String. "--custom_config" must be specified for using this')) + parser.add_argument("--custom_ver", dest='custom_ver', type=int, default=2, + help=desc_format( + 'Custom Config Version Number. "--custom_config" must be specified for using this')) + + parser.add_argument("-v","--verbose", help="Increase output verbosity", action="store_true") - parser.add_argument("-v","--verbose", help="increase output verbosity", action="store_true") args = parser.parse_args() - if args.protover != '': - print("==== Esp_Prov Version: " + args.protover + " ====") - - obj_transport = get_transport(args.provmode, args.softap_endpoint, args.ble_devname) + obj_transport = get_transport(args.mode.lower(), args.name) if obj_transport is None: - print("---- Invalid provisioning mode ----") + print("---- Failed to establish connection ----") exit(1) # If security version not specified check in capabilities if args.secver is None: + # First check if capabilities are supported or not + if not has_capability(obj_transport): + print('Security capabilities could not be determined. Please specify "--sec_ver" explicitly') + print("---- Invalid Security Version ----") + exit(2) + # When no_sec is present, use security 0, else security 1 args.secver = int(not has_capability(obj_transport, 'no_sec')) + print("Security scheme determined to be :", args.secver) - if (args.secver != 0) and not has_capability(obj_transport, 'no_pop'): - if len(args.pop) == 0: - print("---- Proof of Possession argument not provided ----") - exit(2) - elif len(args.pop) != 0: - print("---- Proof of Possession will be ignored ----") - args.pop = '' + if (args.secver != 0) and not has_capability(obj_transport, 'no_pop'): + if len(args.pop) == 0: + print("---- Proof of Possession argument not provided ----") + exit(2) + elif len(args.pop) != 0: + print("---- Proof of Possession will be ignored ----") + args.pop = '' obj_security = get_security(args.secver, args.pop, args.verbose) if obj_security is None: print("---- Invalid Security Version ----") exit(2) - if args.protover != '': + if args.version != '': print("\n==== Verifying protocol version ====") - if not version_match(obj_transport, args.protover, args.verbose): + if not version_match(obj_transport, args.version, args.verbose): print("---- Error in protocol version matching ----") exit(3) print("==== Verified protocol version successfully ====") print("\n==== Starting Session ====") if not establish_session(obj_transport, obj_security): + print("Failed to establish session. Ensure that security scheme and proof of possession are correct") print("---- Error in establishing session ----") exit(4) print("==== Session Established ====") @@ -328,7 +403,7 @@ if __name__ == '__main__': while True: print("\n==== Scanning Wi-Fi APs ====") start_time = time.time() - APs = scan_wifi_APs(args.provmode, obj_transport, obj_security) + APs = scan_wifi_APs(args.mode.lower(), obj_transport, obj_security) end_time = time.time() print("\n++++ Scan finished in " + str(end_time - start_time) + " sec") if APs is None: diff --git a/tools/esp_prov/transport/__init__.py b/tools/esp_prov/transport/__init__.py index 809d1335d3..907df1f3ca 100644 --- a/tools/esp_prov/transport/__init__.py +++ b/tools/esp_prov/transport/__init__.py @@ -14,5 +14,5 @@ # from .transport_console import * # noqa: F403, F401 -from .transport_softap import * # noqa: F403, F401 +from .transport_http import * # noqa: F403, F401 from .transport_ble import * # noqa: F403, F401 diff --git a/tools/esp_prov/transport/transport_softap.py b/tools/esp_prov/transport/transport_http.py similarity index 65% rename from tools/esp_prov/transport/transport_softap.py rename to tools/esp_prov/transport/transport_http.py index 5b0d2908fd..3c7aed013c 100644 --- a/tools/esp_prov/transport/transport_softap.py +++ b/tools/esp_prov/transport/transport_http.py @@ -16,14 +16,30 @@ from __future__ import print_function from future.utils import tobytes +import socket import http.client +import ssl from .transport import Transport -class Transport_Softap(Transport): - def __init__(self, url): - self.conn = http.client.HTTPConnection(url, timeout=30) +class Transport_HTTP(Transport): + def __init__(self, hostname, certfile=None): + try: + socket.gethostbyname(hostname.split(':')[0]) + except socket.gaierror: + raise RuntimeError("Unable to resolve hostname :" + hostname) + + if certfile is None: + self.conn = http.client.HTTPConnection(hostname, timeout=30) + else: + ssl_ctx = ssl.create_default_context(cafile=certfile) + self.conn = http.client.HTTPSConnection(hostname, context=ssl_ctx, timeout=30) + try: + print("Connecting to " + hostname) + self.conn.connect() + except Exception as err: + raise RuntimeError("Connection Failure : " + str(err)) self.headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"} def _send_post_request(self, path, data):