From 032a04139518f25e424aff1177d60d3afacfd2f3 Mon Sep 17 00:00:00 2001 From: Shubham Kulkarni Date: Mon, 9 Dec 2019 19:53:05 +0530 Subject: [PATCH] Bugfix for failing OTA example example_test.py is added to test advanced_https_ota_example and native ota_example. Closes https://github.com/espressif/esp-idf/issues/4394 --- components/esp_http_client/esp_http_client.c | 4 +- components/esp_https_ota/src/esp_https_ota.c | 82 ++++-- .../ota/advanced_https_ota/example_test.py | 277 ++++++++++++++++++ .../advanced_https_ota/main/Kconfig.projbuild | 22 ++ .../main/advanced_https_ota_example.c | 32 +- .../ota/advanced_https_ota/sdkconfig.ci | 4 + .../server_certs/ca_cert.pem | 21 ++ .../ota/native_ota_example/example_test.py | 277 ++++++++++++++++++ .../native_ota_example/main/Kconfig.projbuild | 22 ++ .../main/native_ota_example.c | 43 ++- .../ota/native_ota_example/sdkconfig.ci | 4 + .../server_certs/ca_cert.pem | 21 ++ tools/ci/config/target-test.yml | 2 +- 13 files changed, 781 insertions(+), 30 deletions(-) create mode 100644 examples/system/ota/advanced_https_ota/example_test.py create mode 100644 examples/system/ota/advanced_https_ota/sdkconfig.ci create mode 100644 examples/system/ota/native_ota_example/example_test.py create mode 100644 examples/system/ota/native_ota_example/sdkconfig.ci diff --git a/components/esp_http_client/esp_http_client.c b/components/esp_http_client/esp_http_client.c index 3c6b08c64..69ced3406 100644 --- a/components/esp_http_client/esp_http_client.c +++ b/components/esp_http_client/esp_http_client.c @@ -807,12 +807,12 @@ bool esp_http_client_is_complete_data_received(esp_http_client_handle_t client) { if (client->response->is_chunked) { if (!client->is_chunk_complete) { - ESP_LOGI(TAG, "Chunks were not completely read"); + ESP_LOGD(TAG, "Chunks were not completely read"); return false; } } else { if (client->response->data_process != client->response->content_length) { - ESP_LOGI(TAG, "Data processed %d != Data specified in content length %d", client->response->data_process, client->response->content_length); + ESP_LOGD(TAG, "Data processed %d != Data specified in content length %d", client->response->data_process, client->response->content_length); return false; } } diff --git a/components/esp_https_ota/src/esp_https_ota.c b/components/esp_https_ota/src/esp_https_ota.c index c5c80c551..0cda4439d 100644 --- a/components/esp_https_ota/src/esp_https_ota.c +++ b/components/esp_https_ota/src/esp_https_ota.c @@ -18,6 +18,7 @@ #include #include #include +#include #define IMAGE_HEADER_SIZE sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t) + 1 #define DEFAULT_OTA_BUF_SIZE IMAGE_HEADER_SIZE @@ -70,15 +71,27 @@ static esp_err_t _http_handle_response_code(esp_http_client_handle_t http_client } char upgrade_data_buf[DEFAULT_OTA_BUF_SIZE]; + /* + * `data_read_size` holds number of bytes to be read. + * `bytes_read` holds number of bytes read. + */ + int bytes_read = 0, data_read_size = DEFAULT_OTA_BUF_SIZE; if (process_again(status_code)) { - while (1) { - int data_read = esp_http_client_read(http_client, upgrade_data_buf, DEFAULT_OTA_BUF_SIZE); - if (data_read < 0) { - ESP_LOGE(TAG, "Error: SSL data read error"); - return ESP_FAIL; - } else if (data_read == 0) { - return ESP_OK; + while (data_read_size > 0) { + int data_read = esp_http_client_read(http_client, (upgrade_data_buf + bytes_read), data_read_size); + /* + * As esp_http_client_read never returns negative error code, we rely on + * `errno` to check for underlying transport connectivity closure if any + */ + if (errno == ENOTCONN || errno == ECONNRESET) { + ESP_LOGE(TAG, "Connection closed, errno = %d", errno); + break; } + bytes_read += data_read; + data_read_size -= data_read; + } + if (data_read_size > 0) { + return ESP_FAIL; } } return ESP_OK; @@ -215,19 +228,37 @@ esp_err_t esp_https_ota_get_img_desc(esp_https_ota_handle_t https_ota_handle, es ESP_LOGE(TAG, "esp_https_ota_read_img_desc: Invalid state"); return ESP_FAIL; } + /* + * `data_read_size` holds number of bytes needed to read complete header. + * `bytes_read` holds number of bytes read. + */ int data_read_size = IMAGE_HEADER_SIZE; - int data_read = esp_http_client_read(handle->http_client, - handle->ota_upgrade_buf, - data_read_size); - if (data_read < 0) { - return ESP_FAIL; - } - if (data_read >= sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) { - memcpy(new_app_info, &handle->ota_upgrade_buf[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t)); - handle->binary_file_len += data_read; - } else { + int data_read = 0, bytes_read = 0; + /* + * while loop is added to download complete image headers, even if the headers + * are not sent in a single packet. + */ + while (data_read_size > 0 && !esp_https_ota_is_complete_data_received(https_ota_handle)) { + data_read = esp_http_client_read(handle->http_client, + (handle->ota_upgrade_buf + bytes_read), + data_read_size); + /* + * As esp_http_client_read never returns negative error code, we rely on + * `errno` to check for underlying transport connectivity closure if any + */ + if (errno == ENOTCONN || errno == ECONNRESET) { + ESP_LOGE(TAG, "Connection closed, errno = %d", errno); + break; + } + data_read_size -= data_read; + bytes_read += data_read; + } + if (data_read_size > 0) { + ESP_LOGE(TAG, "Complete headers were not received"); return ESP_FAIL; } + handle->binary_file_len = bytes_read; + memcpy(new_app_info, &handle->ota_upgrade_buf[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t)); return ESP_OK; } @@ -265,10 +296,21 @@ esp_err_t esp_https_ota_perform(esp_https_ota_handle_t https_ota_handle) handle->ota_upgrade_buf, handle->ota_upgrade_buf_size); if (data_read == 0) { + /* + * As esp_http_client_read never returns negative error code, we rely on + * `errno` to check for underlying transport connectivity closure if any + */ + if (errno == ENOTCONN || errno == ECONNRESET) { + ESP_LOGE(TAG, "Connection closed, errno = %d", errno); + return ESP_FAIL; + } + /* esp_https_ota_is_complete_data_received is added to check whether + complete image is received. + */ + if (!esp_https_ota_is_complete_data_received(https_ota_handle)) { + return ESP_ERR_HTTPS_OTA_IN_PROGRESS; + } ESP_LOGI(TAG, "Connection closed"); - } else if (data_read < 0) { - ESP_LOGE(TAG, "Error: SSL data read error"); - return ESP_FAIL; } else if (data_read > 0) { return _ota_write(handle, (const void *)handle->ota_upgrade_buf, data_read); } diff --git a/examples/system/ota/advanced_https_ota/example_test.py b/examples/system/ota/advanced_https_ota/example_test.py new file mode 100644 index 000000000..5dcdf054d --- /dev/null +++ b/examples/system/ota/advanced_https_ota/example_test.py @@ -0,0 +1,277 @@ +import re +import os +import socket +import BaseHTTPServer +import SimpleHTTPServer +from threading import Thread +import ssl + +from tiny_test_fw import DUT +import ttfw_idf +import random + +server_cert = "-----BEGIN CERTIFICATE-----\n" \ + "MIIDXTCCAkWgAwIBAgIJAP4LF7E72HakMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n"\ + "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n"\ + "aWRnaXRzIFB0eSBMdGQwHhcNMTkwNjA3MDk1OTE2WhcNMjAwNjA2MDk1OTE2WjBF\n"\ + "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n"\ + "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"\ + "CgKCAQEAlzfCyv3mIv7TlLkObxunKfCdrJ/zgdANrsx0RBtpEPhV560hWJ0fEin0\n"\ + "nIOMpJSiF9E6QsPdr6Q+eogH4XnOMU9JE+iG743N1dPfGEzJvRlyct/Ck8SswKPC\n"\ + "9+VXsnOdZmUw9y/xtANbURA/TspvPzz3Avv382ffffrJGh7ooOmaZSCZFlSYHLZA\n"\ + "w/XlRr0sSRbLpFGY0gXjaAV8iHHiPDYLy4kZOepjV9U51xi+IGsL4w75zuMgsHyF\n"\ + "3nJeGYHgtGVBrkL0ZKG5udY0wcBjysjubDJC4iSlNiq2HD3fhs7j6CZddV2v845M\n"\ + "lVKNxP0kO4Uj4D8r+5USWC8JKfAwxQIDAQABo1AwTjAdBgNVHQ4EFgQU6OE7ssfY\n"\ + "IIPTDThiUoofUpsD5NwwHwYDVR0jBBgwFoAU6OE7ssfYIIPTDThiUoofUpsD5Nww\n"\ + "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAXIlHS/FJWfmcinUAxyBd\n"\ + "/xd5Lu8ykeru6oaUCci+Vk9lyoMMES7lQ+b/00d5x7AcTawkTil9EWpBTPTOTraA\n"\ + "lzJMQhNKmSLk0iIoTtAJtSZgUSpIIozqK6lenxQQDsHbXKU6h+u9H6KZE8YcjsFl\n"\ + "6vL7sw9BVotw/VxfgjQ5OSGLgoLrdVT0z5C2qOuwOgz1c7jNiJhtMdwN+cOtnJp2\n"\ + "fuBgEYyE3eeuWogvkWoDcIA8r17Ixzkpq2oJsdvZcHZPIZShPKW2SHUsl98KDemu\n"\ + "y0pQyExmQUbwKE4vbFb9XuWCcL9XaOHQytyszt2DeD67AipvoBwVU7/LBOvqnsmy\n"\ + "hA==\n"\ + "-----END CERTIFICATE-----\n" + +server_key = "-----BEGIN PRIVATE KEY-----\n"\ + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCXN8LK/eYi/tOU\n"\ + "uQ5vG6cp8J2sn/OB0A2uzHREG2kQ+FXnrSFYnR8SKfScg4yklKIX0TpCw92vpD56\n"\ + "iAfhec4xT0kT6Ibvjc3V098YTMm9GXJy38KTxKzAo8L35Veyc51mZTD3L/G0A1tR\n"\ + "ED9Oym8/PPcC+/fzZ999+skaHuig6ZplIJkWVJgctkDD9eVGvSxJFsukUZjSBeNo\n"\ + "BXyIceI8NgvLiRk56mNX1TnXGL4gawvjDvnO4yCwfIXecl4ZgeC0ZUGuQvRkobm5\n"\ + "1jTBwGPKyO5sMkLiJKU2KrYcPd+GzuPoJl11Xa/zjkyVUo3E/SQ7hSPgPyv7lRJY\n"\ + "Lwkp8DDFAgMBAAECggEAfBhAfQE7mUByNbxgAgI5fot9eaqR1Nf+QpJ6X2H3KPwC\n"\ + "02sa0HOwieFwYfj6tB1doBoNq7i89mTc+QUlIn4pHgIowHO0OGawomeKz5BEhjCZ\n"\ + "4XeLYGSoODary2+kNkf2xY8JTfFEcyvGBpJEwc4S2VyYgRRx+IgnumTSH+N5mIKZ\n"\ + "SXWNdZIuHEmkwod+rPRXs6/r+PH0eVW6WfpINEbr4zVAGXJx2zXQwd2cuV1GTJWh\n"\ + "cPVOXLu+XJ9im9B370cYN6GqUnR3fui13urYbnWnEf3syvoH/zuZkyrVChauoFf8\n"\ + "8EGb74/HhXK7Q2s8NRakx2c7OxQifCbcy03liUMmyQKBgQDFAob5B/66N4Q2cq/N\n"\ + "MWPf98kYBYoLaeEOhEJhLQlKk0pIFCTmtpmUbpoEes2kCUbH7RwczpYko8tlKyoB\n"\ + "6Fn6RY4zQQ64KZJI6kQVsjkYpcP/ihnOY6rbds+3yyv+4uPX7Eh9sYZwZMggE19M\n"\ + "CkFHkwAjiwqhiiSlUxe20sWmowKBgQDEfx4lxuFzA1PBPeZKGVBTxYPQf+DSLCre\n"\ + "ZFg3ZmrxbCjRq1O7Lra4FXWD3dmRq7NDk79JofoW50yD8wD7I0B7opdDfXD2idO8\n"\ + "0dBnWUKDr2CAXyoLEINce9kJPbx4kFBQRN9PiGF7VkDQxeQ3kfS8CvcErpTKCOdy\n"\ + "5wOwBTwJdwKBgDiTFTeGeDv5nVoVbS67tDao7XKchJvqd9q3WGiXikeELJyuTDqE\n"\ + "zW22pTwMF+m3UEAxcxVCrhMvhkUzNAkANHaOatuFHzj7lyqhO5QPbh4J3FMR0X9X\n"\ + "V8VWRSg+jA/SECP9koOl6zlzd5Tee0tW1pA7QpryXscs6IEhb3ns5R2JAoGAIkzO\n"\ + "RmnhEOKTzDex611f2D+yMsMfy5BKK2f4vjLymBH5TiBKDXKqEpgsW0huoi8Gq9Uu\n"\ + "nvvXXAgkIyRYF36f0vUe0nkjLuYAQAWgC2pZYgNLJR13iVbol0xHJoXQUHtgiaJ8\n"\ + "GLYFzjHQPqFMpSalQe3oELko39uOC1CoJCHFySECgYBeycUnRBikCO2n8DNhY4Eg\n"\ + "9Y3oxcssRt6ea5BZwgW2eAYi7/XqKkmxoSoOykUt3MJx9+EkkrL17bxFSpkj1tvL\n"\ + "qvxn7egtsKjjgGNAxwXC4MwCvhveyUQQxtQb8AqGrGqo4jEEN0L15cnP38i2x1Uo\n"\ + "muhfskWf4MABV0yTUaKcGg==\n"\ + "-----END PRIVATE KEY-----\n" + + +def get_my_ip(): + s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s1.connect(("8.8.8.8", 80)) + my_ip = s1.getsockname()[0] + s1.close() + return my_ip + + +def start_https_server(ota_image_dir, server_ip, server_port): + os.chdir(ota_image_dir) + + server_file = os.path.join(ota_image_dir, "server_cert.pem") + cert_file_handle = open(server_file, "w+") + cert_file_handle.write(server_cert) + cert_file_handle.close() + + key_file = os.path.join(ota_image_dir, "server_key.pem") + key_file_handle = open("server_key.pem", "w+") + key_file_handle.write(server_key) + key_file_handle.close() + + httpd = BaseHTTPServer.HTTPServer((server_ip, server_port), + SimpleHTTPServer.SimpleHTTPRequestHandler) + + httpd.socket = ssl.wrap_socket(httpd.socket, + keyfile=key_file, + certfile=server_file, server_side=True) + httpd.serve_forever() + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_advanced_https_ota_example(env, extra_data): + """ + This is a positive test case, which downloads complete binary file multiple number of times. + Number of iterations can be specified in variable iterations. + steps: | + 1. join AP + 2. Fetch OTA image over HTTPS + 3. Reboot with the new OTA image + """ + dut1 = env.get_dut("advanced_https_ota_example", "examples/system/ota/advanced_https_ota", dut_class=ttfw_idf.ESP32DUT) + # Number of iterations to validate OTA + iterations = 3 + # File to be downloaded. This file is generated after compilation + bin_name = "advanced_https_ota.bin" + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, bin_name) + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("advanced_https_ota_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("advanced_https_ota_bin_size", bin_size // 1024) + # start test + host_ip = get_my_ip() + thread1 = Thread(target=start_https_server, args=(dut1.app.binary_path, host_ip, 8001)) + thread1.daemon = True + thread1.start() + dut1.start_app() + for i in range(iterations): + dut1.expect("Loaded app from partition at offset", timeout=30) + try: + ip_address = dut1.expect(re.compile(r" sta ip: ([^,]+),"), timeout=30) + print("Connected to AP with IP: {}".format(ip_address)) + except DUT.ExpectTimeout: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP') + thread1.close() + dut1.expect("Starting Advanced OTA example", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8001/" + bin_name)) + dut1.write("https://" + host_ip + ":8001/" + bin_name) + dut1.expect("Loaded app from partition at offset", timeout=60) + dut1.expect("Starting Advanced OTA example", timeout=30) + dut1.reset() + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_advanced_https_ota_example_truncated_bin(env, extra_data): + """ + Working of OTA if binary file is truncated is validated in this test case. + Application should return with error message in this case. + steps: | + 1. join AP + 2. Generate truncated binary file + 3. Fetch OTA image over HTTPS + 4. Check working of code if bin is truncated + """ + dut1 = env.get_dut("advanced_https_ota_example", "examples/system/ota/advanced_https_ota", dut_class=ttfw_idf.ESP32DUT) + # Original binary file generated after compilation + bin_name = "advanced_https_ota.bin" + # Truncated binary file to be generated from original binary file + truncated_bin_name = "truncated.bin" + # Size of truncated file to be grnerated. This value can range from 288 bytes (Image header size) to size of original binary file + # truncated_bin_size is set to 64000 to reduce consumed by the test case + truncated_bin_size = 64000 + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, bin_name) + f = open(binary_file, "r+") + fo = open(os.path.join(dut1.app.binary_path, truncated_bin_name), "w+") + fo.write(f.read(truncated_bin_size)) + fo.close() + f.close() + binary_file = os.path.join(dut1.app.binary_path, truncated_bin_name) + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("advanced_https_ota_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("advanced_https_ota_bin_size", bin_size // 1024) + # start test + host_ip = get_my_ip() + dut1.start_app() + dut1.expect("Loaded app from partition at offset", timeout=30) + try: + ip_address = dut1.expect(re.compile(r" sta ip: ([^,]+),"), timeout=30) + print("Connected to AP with IP: {}".format(ip_address)) + except DUT.ExpectTimeout: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP') + dut1.expect("Starting Advanced OTA example", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8001/" + truncated_bin_name)) + dut1.write("https://" + host_ip + ":8001/" + truncated_bin_name) + dut1.expect("Image validation failed, image is corrupted", timeout=30) + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_advanced_https_ota_example_truncated_header(env, extra_data): + """ + Working of OTA if headers of binary file are truncated is vaildated in this test case. + Application should return with error message in this case. + steps: | + 1. join AP + 2. Generate binary file with truncated headers + 3. Fetch OTA image over HTTPS + 4. Check working of code if headers are not sent completely + """ + dut1 = env.get_dut("advanced_https_ota_example", "examples/system/ota/advanced_https_ota", dut_class=ttfw_idf.ESP32DUT) + # Original binary file generated after compilation + bin_name = "advanced_https_ota.bin" + # Truncated binary file to be generated from original binary file + truncated_bin_name = "truncated_header.bin" + # Size of truncated file to be grnerated. This value should be less than 288 bytes (Image header size) + truncated_bin_size = 180 + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, bin_name) + f = open(binary_file, "r+") + fo = open(os.path.join(dut1.app.binary_path, truncated_bin_name), "w+") + fo.write(f.read(truncated_bin_size)) + fo.close() + f.close() + binary_file = os.path.join(dut1.app.binary_path, truncated_bin_name) + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("advanced_https_ota_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("advanced_https_ota_bin_size", bin_size // 1024) + # start test + host_ip = get_my_ip() + dut1.start_app() + dut1.expect("Loaded app from partition at offset", timeout=30) + try: + ip_address = dut1.expect(re.compile(r" sta ip: ([^,]+),"), timeout=30) + print("Connected to AP with IP: {}".format(ip_address)) + except DUT.ExpectTimeout: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP') + dut1.expect("Starting Advanced OTA example", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8001/" + truncated_bin_name)) + dut1.write("https://" + host_ip + ":8001/" + truncated_bin_name) + dut1.expect("advanced_https_ota_example: esp_https_ota_read_img_desc failed", timeout=30) + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_advanced_https_ota_example_random(env, extra_data): + """ + Working of OTA if random data is added in binary file are validated in this test case. + Magic byte verification should fail in this case. + steps: | + 1. join AP + 2. Generate random binary image + 3. Fetch OTA image over HTTPS + 4. Check working of code for random binary file + """ + dut1 = env.get_dut("advanced_https_ota_example", "examples/system/ota/advanced_https_ota", dut_class=ttfw_idf.ESP32DUT) + # Random binary file to be generated + random_bin_name = "random.bin" + # Size of random binary file. 32000 is choosen, to reduce the time required to run the test-case + random_bin_size = 32000 + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, random_bin_name) + fo = open(binary_file, "w+") + # First byte of binary file is always set to zero. If first byte is generated randomly, + # in some cases it may generate 0xE9 which will result in failure of testcase. + fo.write(str(0)) + for i in range(random_bin_size - 1): + fo.write(str(random.randrange(0,255,1))) + fo.close() + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("advanced_https_ota_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("advanced_https_ota_bin_size", bin_size // 1024) + # start test + host_ip = get_my_ip() + dut1.start_app() + dut1.expect("Loaded app from partition at offset", timeout=30) + try: + ip_address = dut1.expect(re.compile(r" sta ip: ([^,]+),"), timeout=30) + print("Connected to AP with IP: {}".format(ip_address)) + except DUT.ExpectTimeout: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP') + dut1.expect("Starting Advanced OTA example", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8001/" + random_bin_name)) + dut1.write("https://" + host_ip + ":8001/" + random_bin_name) + dut1.expect("esp_ota_ops: OTA image has invalid magic byte", timeout=10) + + +if __name__ == '__main__': + test_examples_protocol_advanced_https_ota_example() + test_examples_protocol_advanced_https_ota_example_truncated_bin() + test_examples_protocol_advanced_https_ota_example_truncated_header() + test_examples_protocol_advanced_https_ota_example_random() diff --git a/examples/system/ota/advanced_https_ota/main/Kconfig.projbuild b/examples/system/ota/advanced_https_ota/main/Kconfig.projbuild index 22b772a05..0c6d32510 100644 --- a/examples/system/ota/advanced_https_ota/main/Kconfig.projbuild +++ b/examples/system/ota/advanced_https_ota/main/Kconfig.projbuild @@ -6,4 +6,26 @@ menu "Example Configuration" help URL of server which hosts the firmware image. + config EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN + bool + default y if EXAMPLE_FIRMWARE_UPGRADE_URL = "FROM_STDIN" + + config EXAMPLE_SKIP_COMMON_NAME_CHECK + bool "Skip server certificate CN fieldcheck" + default n + help + This allows you to skip the validation of OTA server certificate CN field. + + config EXAMPLE_SKIP_VERSION_CHECK + bool "Skip firmware version check" + default n + help + This allows you to skip the firmware version check. + + config EXAMPLE_OTA_RECV_TIMEOUT + int "OTA Receive Timeout" + default 5000 + help + Maximum time for reception + endmenu diff --git a/examples/system/ota/advanced_https_ota/main/advanced_https_ota_example.c b/examples/system/ota/advanced_https_ota/main/advanced_https_ota_example.c index f9407582c..b164760e7 100644 --- a/examples/system/ota/advanced_https_ota/main/advanced_https_ota_example.c +++ b/examples/system/ota/advanced_https_ota/main/advanced_https_ota_example.c @@ -28,6 +28,8 @@ static const char *TAG = "advanced_https_ota_example"; extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start"); extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end"); +#define OTA_URL_SIZE 256 + static esp_err_t validate_image_header(esp_app_desc_t *new_app_info) { if (new_app_info == NULL) { @@ -40,10 +42,13 @@ static esp_err_t validate_image_header(esp_app_desc_t *new_app_info) ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version); } +#ifndef CONFIG_EXAMPLE_SKIP_VERSION_CHECK if (memcmp(new_app_info->version, running_app_info.version, sizeof(new_app_info->version)) == 0) { ESP_LOGW(TAG, "Current running version is the same as a new. We will not continue the update."); return ESP_FAIL; } +#endif + return ESP_OK; } @@ -55,8 +60,27 @@ void advanced_ota_example_task(void *pvParameter) esp_http_client_config_t config = { .url = CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL, .cert_pem = (char *)server_cert_pem_start, + .timeout_ms = CONFIG_EXAMPLE_OTA_RECV_TIMEOUT, }; +#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN + char url_buf[OTA_URL_SIZE]; + if (strcmp(config.url, "FROM_STDIN") == 0) { + example_configure_stdin_stdout(); + fgets(url_buf, OTA_URL_SIZE, stdin); + int len = strlen(url_buf); + url_buf[len - 1] = '\0'; + config.url = url_buf; + } else { + ESP_LOGE(TAG, "Configuration mismatch: wrong firmware upgrade image url"); + abort(); + } +#endif + +#ifdef CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK + config.skip_cert_common_name_check = true; +#endif + esp_https_ota_config_t ota_config = { .http_config = &config, }; @@ -103,11 +127,11 @@ ota_end: vTaskDelay(1000 / portTICK_PERIOD_MS); esp_restart(); } else { + if (ota_finish_err == ESP_ERR_OTA_VALIDATE_FAILED) { + ESP_LOGE(TAG, "Image validation failed, image is corrupted"); + } ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed %d", ota_finish_err); - } - - while (1) { - vTaskDelay(1000 / portTICK_PERIOD_MS); + vTaskDelete(NULL); } } diff --git a/examples/system/ota/advanced_https_ota/sdkconfig.ci b/examples/system/ota/advanced_https_ota/sdkconfig.ci new file mode 100644 index 000000000..c4df1c1b1 --- /dev/null +++ b/examples/system/ota/advanced_https_ota/sdkconfig.ci @@ -0,0 +1,4 @@ +CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL="FROM_STDIN" +CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK=y +CONFIG_EXAMPLE_SKIP_VERSION_CHECK=y +CONFIG_EXAMPLE_OTA_RECV_TIMEOUT=300 diff --git a/examples/system/ota/advanced_https_ota/server_certs/ca_cert.pem b/examples/system/ota/advanced_https_ota/server_certs/ca_cert.pem index e69de29bb..5b21a5c2a 100644 --- a/examples/system/ota/advanced_https_ota/server_certs/ca_cert.pem +++ b/examples/system/ota/advanced_https_ota/server_certs/ca_cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAP4LF7E72HakMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTkwNjA3MDk1OTE2WhcNMjAwNjA2MDk1OTE2WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAlzfCyv3mIv7TlLkObxunKfCdrJ/zgdANrsx0RBtpEPhV560hWJ0fEin0 +nIOMpJSiF9E6QsPdr6Q+eogH4XnOMU9JE+iG743N1dPfGEzJvRlyct/Ck8SswKPC +9+VXsnOdZmUw9y/xtANbURA/TspvPzz3Avv382ffffrJGh7ooOmaZSCZFlSYHLZA +w/XlRr0sSRbLpFGY0gXjaAV8iHHiPDYLy4kZOepjV9U51xi+IGsL4w75zuMgsHyF +3nJeGYHgtGVBrkL0ZKG5udY0wcBjysjubDJC4iSlNiq2HD3fhs7j6CZddV2v845M +lVKNxP0kO4Uj4D8r+5USWC8JKfAwxQIDAQABo1AwTjAdBgNVHQ4EFgQU6OE7ssfY +IIPTDThiUoofUpsD5NwwHwYDVR0jBBgwFoAU6OE7ssfYIIPTDThiUoofUpsD5Nww +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAXIlHS/FJWfmcinUAxyBd +/xd5Lu8ykeru6oaUCci+Vk9lyoMMES7lQ+b/00d5x7AcTawkTil9EWpBTPTOTraA +lzJMQhNKmSLk0iIoTtAJtSZgUSpIIozqK6lenxQQDsHbXKU6h+u9H6KZE8YcjsFl +6vL7sw9BVotw/VxfgjQ5OSGLgoLrdVT0z5C2qOuwOgz1c7jNiJhtMdwN+cOtnJp2 +fuBgEYyE3eeuWogvkWoDcIA8r17Ixzkpq2oJsdvZcHZPIZShPKW2SHUsl98KDemu +y0pQyExmQUbwKE4vbFb9XuWCcL9XaOHQytyszt2DeD67AipvoBwVU7/LBOvqnsmy +hA== +-----END CERTIFICATE----- diff --git a/examples/system/ota/native_ota_example/example_test.py b/examples/system/ota/native_ota_example/example_test.py new file mode 100644 index 000000000..0bce3cffd --- /dev/null +++ b/examples/system/ota/native_ota_example/example_test.py @@ -0,0 +1,277 @@ +import re +import os +import socket +import BaseHTTPServer +import SimpleHTTPServer +from threading import Thread +import ssl + +from tiny_test_fw import DUT +import ttfw_idf +import random + +server_cert = "-----BEGIN CERTIFICATE-----\n" \ + "MIIDXTCCAkWgAwIBAgIJAP4LF7E72HakMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n"\ + "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n"\ + "aWRnaXRzIFB0eSBMdGQwHhcNMTkwNjA3MDk1OTE2WhcNMjAwNjA2MDk1OTE2WjBF\n"\ + "MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\n"\ + "ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"\ + "CgKCAQEAlzfCyv3mIv7TlLkObxunKfCdrJ/zgdANrsx0RBtpEPhV560hWJ0fEin0\n"\ + "nIOMpJSiF9E6QsPdr6Q+eogH4XnOMU9JE+iG743N1dPfGEzJvRlyct/Ck8SswKPC\n"\ + "9+VXsnOdZmUw9y/xtANbURA/TspvPzz3Avv382ffffrJGh7ooOmaZSCZFlSYHLZA\n"\ + "w/XlRr0sSRbLpFGY0gXjaAV8iHHiPDYLy4kZOepjV9U51xi+IGsL4w75zuMgsHyF\n"\ + "3nJeGYHgtGVBrkL0ZKG5udY0wcBjysjubDJC4iSlNiq2HD3fhs7j6CZddV2v845M\n"\ + "lVKNxP0kO4Uj4D8r+5USWC8JKfAwxQIDAQABo1AwTjAdBgNVHQ4EFgQU6OE7ssfY\n"\ + "IIPTDThiUoofUpsD5NwwHwYDVR0jBBgwFoAU6OE7ssfYIIPTDThiUoofUpsD5Nww\n"\ + "DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAXIlHS/FJWfmcinUAxyBd\n"\ + "/xd5Lu8ykeru6oaUCci+Vk9lyoMMES7lQ+b/00d5x7AcTawkTil9EWpBTPTOTraA\n"\ + "lzJMQhNKmSLk0iIoTtAJtSZgUSpIIozqK6lenxQQDsHbXKU6h+u9H6KZE8YcjsFl\n"\ + "6vL7sw9BVotw/VxfgjQ5OSGLgoLrdVT0z5C2qOuwOgz1c7jNiJhtMdwN+cOtnJp2\n"\ + "fuBgEYyE3eeuWogvkWoDcIA8r17Ixzkpq2oJsdvZcHZPIZShPKW2SHUsl98KDemu\n"\ + "y0pQyExmQUbwKE4vbFb9XuWCcL9XaOHQytyszt2DeD67AipvoBwVU7/LBOvqnsmy\n"\ + "hA==\n"\ + "-----END CERTIFICATE-----\n" + +server_key = "-----BEGIN PRIVATE KEY-----\n"\ + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCXN8LK/eYi/tOU\n"\ + "uQ5vG6cp8J2sn/OB0A2uzHREG2kQ+FXnrSFYnR8SKfScg4yklKIX0TpCw92vpD56\n"\ + "iAfhec4xT0kT6Ibvjc3V098YTMm9GXJy38KTxKzAo8L35Veyc51mZTD3L/G0A1tR\n"\ + "ED9Oym8/PPcC+/fzZ999+skaHuig6ZplIJkWVJgctkDD9eVGvSxJFsukUZjSBeNo\n"\ + "BXyIceI8NgvLiRk56mNX1TnXGL4gawvjDvnO4yCwfIXecl4ZgeC0ZUGuQvRkobm5\n"\ + "1jTBwGPKyO5sMkLiJKU2KrYcPd+GzuPoJl11Xa/zjkyVUo3E/SQ7hSPgPyv7lRJY\n"\ + "Lwkp8DDFAgMBAAECggEAfBhAfQE7mUByNbxgAgI5fot9eaqR1Nf+QpJ6X2H3KPwC\n"\ + "02sa0HOwieFwYfj6tB1doBoNq7i89mTc+QUlIn4pHgIowHO0OGawomeKz5BEhjCZ\n"\ + "4XeLYGSoODary2+kNkf2xY8JTfFEcyvGBpJEwc4S2VyYgRRx+IgnumTSH+N5mIKZ\n"\ + "SXWNdZIuHEmkwod+rPRXs6/r+PH0eVW6WfpINEbr4zVAGXJx2zXQwd2cuV1GTJWh\n"\ + "cPVOXLu+XJ9im9B370cYN6GqUnR3fui13urYbnWnEf3syvoH/zuZkyrVChauoFf8\n"\ + "8EGb74/HhXK7Q2s8NRakx2c7OxQifCbcy03liUMmyQKBgQDFAob5B/66N4Q2cq/N\n"\ + "MWPf98kYBYoLaeEOhEJhLQlKk0pIFCTmtpmUbpoEes2kCUbH7RwczpYko8tlKyoB\n"\ + "6Fn6RY4zQQ64KZJI6kQVsjkYpcP/ihnOY6rbds+3yyv+4uPX7Eh9sYZwZMggE19M\n"\ + "CkFHkwAjiwqhiiSlUxe20sWmowKBgQDEfx4lxuFzA1PBPeZKGVBTxYPQf+DSLCre\n"\ + "ZFg3ZmrxbCjRq1O7Lra4FXWD3dmRq7NDk79JofoW50yD8wD7I0B7opdDfXD2idO8\n"\ + "0dBnWUKDr2CAXyoLEINce9kJPbx4kFBQRN9PiGF7VkDQxeQ3kfS8CvcErpTKCOdy\n"\ + "5wOwBTwJdwKBgDiTFTeGeDv5nVoVbS67tDao7XKchJvqd9q3WGiXikeELJyuTDqE\n"\ + "zW22pTwMF+m3UEAxcxVCrhMvhkUzNAkANHaOatuFHzj7lyqhO5QPbh4J3FMR0X9X\n"\ + "V8VWRSg+jA/SECP9koOl6zlzd5Tee0tW1pA7QpryXscs6IEhb3ns5R2JAoGAIkzO\n"\ + "RmnhEOKTzDex611f2D+yMsMfy5BKK2f4vjLymBH5TiBKDXKqEpgsW0huoi8Gq9Uu\n"\ + "nvvXXAgkIyRYF36f0vUe0nkjLuYAQAWgC2pZYgNLJR13iVbol0xHJoXQUHtgiaJ8\n"\ + "GLYFzjHQPqFMpSalQe3oELko39uOC1CoJCHFySECgYBeycUnRBikCO2n8DNhY4Eg\n"\ + "9Y3oxcssRt6ea5BZwgW2eAYi7/XqKkmxoSoOykUt3MJx9+EkkrL17bxFSpkj1tvL\n"\ + "qvxn7egtsKjjgGNAxwXC4MwCvhveyUQQxtQb8AqGrGqo4jEEN0L15cnP38i2x1Uo\n"\ + "muhfskWf4MABV0yTUaKcGg==\n"\ + "-----END PRIVATE KEY-----\n" + + +def get_my_ip(): + s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s1.connect(("8.8.8.8", 80)) + my_ip = s1.getsockname()[0] + s1.close() + return my_ip + + +def start_https_server(ota_image_dir, server_ip, server_port): + os.chdir(ota_image_dir) + + server_file = os.path.join(ota_image_dir, "server_cert.pem") + cert_file_handle = open(server_file, "w+") + cert_file_handle.write(server_cert) + cert_file_handle.close() + + key_file = os.path.join(ota_image_dir, "server_key.pem") + key_file_handle = open("server_key.pem", "w+") + key_file_handle.write(server_key) + key_file_handle.close() + + httpd = BaseHTTPServer.HTTPServer((server_ip, server_port), + SimpleHTTPServer.SimpleHTTPRequestHandler) + + httpd.socket = ssl.wrap_socket(httpd.socket, + keyfile=key_file, + certfile=server_file, server_side=True) + httpd.serve_forever() + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_native_ota_example(env, extra_data): + """ + This is a positive test case, which downloads complete binary file multiple number of times. + Number of iterations can be specified in variable iterations. + steps: | + 1. join AP + 2. Fetch OTA image over HTTPS + 3. Reboot with the new OTA image + """ + dut1 = env.get_dut("native_ota_example", "examples/system/ota/native_ota_example", dut_class=ttfw_idf.ESP32DUT) + # No. of times working of application to be validated + iterations = 3 + # File to be downloaded. This file is generated after compilation + bin_name = "native_ota.bin" + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, bin_name) + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("native_ota_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("native_ota_bin_size", bin_size // 1024) + # start test + host_ip = get_my_ip() + thread1 = Thread(target=start_https_server, args=(dut1.app.binary_path, host_ip, 8002)) + thread1.daemon = True + thread1.start() + dut1.start_app() + for i in range(iterations): + dut1.expect("Loaded app from partition at offset", timeout=30) + try: + ip_address = dut1.expect(re.compile(r" sta ip: ([^,]+),"), timeout=30) + print("Connected to AP with IP: {}".format(ip_address)) + except DUT.ExpectTimeout: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP') + thread1.close() + dut1.expect("Starting OTA example", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8002/" + bin_name)) + dut1.write("https://" + host_ip + ":8002/" + bin_name) + dut1.expect("Loaded app from partition at offset", timeout=60) + dut1.expect("Starting OTA example", timeout=30) + dut1.reset() + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_native_ota_example_truncated_bin(env, extra_data): + """ + Working of OTA if binary file is truncated is validated in this test case. + Application should return with error message in this case. + steps: | + 1. join AP + 2. Generate truncated binary file + 3. Fetch OTA image over HTTPS + 4. Check working of code if bin is truncated + """ + dut1 = env.get_dut("native_ota_example", "examples/system/ota/native_ota_example", dut_class=ttfw_idf.ESP32DUT) + # Original binary file generated after compilation + bin_name = "native_ota.bin" + # Truncated binary file to be generated from original binary file + truncated_bin_name = "truncated.bin" + # Size of truncated file to be grnerated. This value can range from 288 bytes (Image header size) to size of original binary file + # truncated_bin_size is set to 64000 to reduce consumed by the test case + truncated_bin_size = 64000 + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, bin_name) + f = open(binary_file, "r+") + fo = open(os.path.join(dut1.app.binary_path, truncated_bin_name), "w+") + fo.write(f.read(truncated_bin_size)) + fo.close() + f.close() + binary_file = os.path.join(dut1.app.binary_path, truncated_bin_name) + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("native_ota_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("native_ota_bin_size", bin_size // 1024) + # start test + host_ip = get_my_ip() + dut1.start_app() + dut1.expect("Loaded app from partition at offset", timeout=30) + try: + ip_address = dut1.expect(re.compile(r" sta ip: ([^,]+),"), timeout=60) + print("Connected to AP with IP: {}".format(ip_address)) + except DUT.ExpectTimeout: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP') + dut1.expect("Starting OTA example", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8002/" + truncated_bin_name)) + dut1.write("https://" + host_ip + ":8002/" + truncated_bin_name) + dut1.expect("native_ota_example: Image validation failed, image is corrupted", timeout=20) + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_native_ota_example_truncated_header(env, extra_data): + """ + Working of OTA if headers of binary file are truncated is vaildated in this test case. + Application should return with error message in this case. + steps: | + 1. join AP + 2. Generate binary file with truncated headers + 3. Fetch OTA image over HTTPS + 4. Check working of code if headers are not sent completely + """ + dut1 = env.get_dut("native_ota_example", "examples/system/ota/native_ota_example", dut_class=ttfw_idf.ESP32DUT) + # Original binary file generated after compilation + bin_name = "native_ota.bin" + # Truncated binary file to be generated from original binary file + truncated_bin_name = "truncated_header.bin" + # Size of truncated file to be grnerated. This value should be less than 288 bytes (Image header size) + truncated_bin_size = 180 + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, bin_name) + f = open(binary_file, "r+") + fo = open(os.path.join(dut1.app.binary_path, truncated_bin_name), "w+") + fo.write(f.read(truncated_bin_size)) + fo.close() + f.close() + binary_file = os.path.join(dut1.app.binary_path, truncated_bin_name) + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("native_ota_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("native_ota_bin_size", bin_size // 1024) + # start test + host_ip = get_my_ip() + dut1.start_app() + dut1.expect("Loaded app from partition at offset", timeout=30) + try: + ip_address = dut1.expect(re.compile(r" sta ip: ([^,]+),"), timeout=60) + print("Connected to AP with IP: {}".format(ip_address)) + except DUT.ExpectTimeout: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP') + dut1.expect("Starting OTA example", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8002/" + truncated_bin_name)) + dut1.write("https://" + host_ip + ":8002/" + truncated_bin_name) + dut1.expect("native_ota_example: received package is not fit len", timeout=20) + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_native_ota_example_random(env, extra_data): + """ + Working of OTA if random data is added in binary file are validated in this test case. + Magic byte verification should fail in this case. + steps: | + 1. join AP + 2. Generate random binary image + 3. Fetch OTA image over HTTPS + 4. Check working of code for random binary file + """ + dut1 = env.get_dut("native_ota_example", "examples/system/ota/native_ota_example", dut_class=ttfw_idf.ESP32DUT) + # Random binary file to be generated + random_bin_name = "random.bin" + # Size of random binary file. 32000 is choosen, to reduce the time required to run the test-case + random_bin_size = 32000 + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, random_bin_name) + fo = open(binary_file, "w+") + # First byte of binary file is always set to zero. If first byte is generated randomly, + # in some cases it may generate 0xE9 which will result in failure of testcase. + fo.write(str(0)) + for i in range(random_bin_size - 1): + fo.write(str(random.randrange(0,255,1))) + fo.close() + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("native_ota_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("native_ota_bin_size", bin_size // 1024) + # start test + host_ip = get_my_ip() + dut1.start_app() + dut1.expect("Loaded app from partition at offset", timeout=30) + try: + ip_address = dut1.expect(re.compile(r" sta ip: ([^,]+),"), timeout=60) + print("Connected to AP with IP: {}".format(ip_address)) + except DUT.ExpectTimeout: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP') + dut1.expect("Starting OTA example", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8002/" + random_bin_name)) + dut1.write("https://" + host_ip + ":8002/" + random_bin_name) + dut1.expect("esp_ota_ops: OTA image has invalid magic byte", timeout=20) + + +if __name__ == '__main__': + test_examples_protocol_native_ota_example() + test_examples_protocol_native_ota_example_truncated_bin() + test_examples_protocol_native_ota_example_truncated_header() + test_examples_protocol_native_ota_example_random() diff --git a/examples/system/ota/native_ota_example/main/Kconfig.projbuild b/examples/system/ota/native_ota_example/main/Kconfig.projbuild index 15c4390b8..192dd0886 100644 --- a/examples/system/ota/native_ota_example/main/Kconfig.projbuild +++ b/examples/system/ota/native_ota_example/main/Kconfig.projbuild @@ -6,6 +6,22 @@ menu "Example Configuration" help URL of server which hosts the firmware image. + config EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN + bool + default y if EXAMPLE_FIRMWARE_UPG_URL = "FROM_STDIN" + + config EXAMPLE_SKIP_COMMON_NAME_CHECK + bool "Skip server certificate CN fieldcheck" + default n + help + This allows you to skip the validation of OTA server certificate CN field. + + config EXAMPLE_SKIP_VERSION_CHECK + bool "Skip firmware version check" + default n + help + This allows you to skip the firmware version check. + config EXAMPLE_GPIO_DIAGNOSTIC int "Number of the GPIO input for diagnostic" range 0 39 @@ -17,4 +33,10 @@ menu "Example Configuration" `Diagnostics (5 sec)...` which will be on first boot. If GPIO is not pulled low then the operable of the app will be confirmed. + config EXAMPLE_OTA_RECV_TIMEOUT + int "OTA Receive Timeout" + default 5000 + help + Maximum time for reception + endmenu diff --git a/examples/system/ota/native_ota_example/main/native_ota_example.c b/examples/system/ota/native_ota_example/main/native_ota_example.c index 69e216019..5b4abdf10 100644 --- a/examples/system/ota/native_ota_example/main/native_ota_example.c +++ b/examples/system/ota/native_ota_example/main/native_ota_example.c @@ -20,6 +20,7 @@ #include "nvs_flash.h" #include "driver/gpio.h" #include "protocol_examples_common.h" +#include "errno.h" #if CONFIG_EXAMPLE_CONNECT_WIFI #include "esp_wifi.h" @@ -34,6 +35,8 @@ static char ota_write_data[BUFFSIZE + 1] = { 0 }; extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start"); extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end"); +#define OTA_URL_SIZE 256 + static void http_cleanup(esp_http_client_handle_t client) { esp_http_client_close(client); @@ -93,7 +96,27 @@ static void ota_example_task(void *pvParameter) esp_http_client_config_t config = { .url = CONFIG_EXAMPLE_FIRMWARE_UPG_URL, .cert_pem = (char *)server_cert_pem_start, + .timeout_ms = CONFIG_EXAMPLE_OTA_RECV_TIMEOUT, }; + +#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN + char url_buf[OTA_URL_SIZE]; + if (strcmp(config.url, "FROM_STDIN") == 0) { + example_configure_stdin_stdout(); + fgets(url_buf, OTA_URL_SIZE, stdin); + int len = strlen(url_buf); + url_buf[len - 1] = '\0'; + config.url = url_buf; + } else { + ESP_LOGE(TAG, "Configuration mismatch: wrong firmware upgrade image url"); + abort(); + } +#endif + +#ifdef CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK + config.skip_cert_common_name_check = true; +#endif + esp_http_client_handle_t client = esp_http_client_init(&config); if (client == NULL) { ESP_LOGE(TAG, "Failed to initialise HTTP connection"); @@ -150,12 +173,13 @@ static void ota_example_task(void *pvParameter) infinite_loop(); } } - +#ifndef CONFIG_EXAMPLE_SKIP_VERSION_CHECK if (memcmp(new_app_info.version, running_app_info.version, sizeof(new_app_info.version)) == 0) { ESP_LOGW(TAG, "Current running version is the same as a new. We will not continue the update."); http_cleanup(client); infinite_loop(); } +#endif image_header_was_checked = true; @@ -180,8 +204,18 @@ static void ota_example_task(void *pvParameter) binary_file_length += data_read; ESP_LOGD(TAG, "Written image length %d", binary_file_length); } else if (data_read == 0) { - ESP_LOGI(TAG, "Connection closed"); - break; + /* + * As esp_http_client_read never returns negative error code, we rely on + * `errno` to check for underlying transport connectivity closure if any + */ + if (errno == ECONNRESET || errno == ENOTCONN) { + ESP_LOGE(TAG, "Connection closed, errno = %d", errno); + break; + } + if (esp_http_client_is_complete_data_received(client) == true) { + ESP_LOGI(TAG, "Connection closed"); + break; + } } } ESP_LOGI(TAG, "Total Write binary data length: %d", binary_file_length); @@ -193,6 +227,9 @@ static void ota_example_task(void *pvParameter) err = esp_ota_end(update_handle); if (err != ESP_OK) { + if (err == ESP_ERR_OTA_VALIDATE_FAILED) { + ESP_LOGE(TAG, "Image validation failed, image is corrupted"); + } ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err)); http_cleanup(client); task_fatal_error(); diff --git a/examples/system/ota/native_ota_example/sdkconfig.ci b/examples/system/ota/native_ota_example/sdkconfig.ci new file mode 100644 index 000000000..8a61d2a32 --- /dev/null +++ b/examples/system/ota/native_ota_example/sdkconfig.ci @@ -0,0 +1,4 @@ +CONFIG_EXAMPLE_FIRMWARE_UPG_URL="FROM_STDIN" +CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK=y +CONFIG_EXAMPLE_SKIP_VERSION_CHECK=y +CONFIG_EXAMPLE_OTA_RECV_TIMEOUT=300 diff --git a/examples/system/ota/native_ota_example/server_certs/ca_cert.pem b/examples/system/ota/native_ota_example/server_certs/ca_cert.pem index e69de29bb..5b21a5c2a 100644 --- a/examples/system/ota/native_ota_example/server_certs/ca_cert.pem +++ b/examples/system/ota/native_ota_example/server_certs/ca_cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAP4LF7E72HakMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTkwNjA3MDk1OTE2WhcNMjAwNjA2MDk1OTE2WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAlzfCyv3mIv7TlLkObxunKfCdrJ/zgdANrsx0RBtpEPhV560hWJ0fEin0 +nIOMpJSiF9E6QsPdr6Q+eogH4XnOMU9JE+iG743N1dPfGEzJvRlyct/Ck8SswKPC +9+VXsnOdZmUw9y/xtANbURA/TspvPzz3Avv382ffffrJGh7ooOmaZSCZFlSYHLZA +w/XlRr0sSRbLpFGY0gXjaAV8iHHiPDYLy4kZOepjV9U51xi+IGsL4w75zuMgsHyF +3nJeGYHgtGVBrkL0ZKG5udY0wcBjysjubDJC4iSlNiq2HD3fhs7j6CZddV2v845M +lVKNxP0kO4Uj4D8r+5USWC8JKfAwxQIDAQABo1AwTjAdBgNVHQ4EFgQU6OE7ssfY +IIPTDThiUoofUpsD5NwwHwYDVR0jBBgwFoAU6OE7ssfYIIPTDThiUoofUpsD5Nww +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAXIlHS/FJWfmcinUAxyBd +/xd5Lu8ykeru6oaUCci+Vk9lyoMMES7lQ+b/00d5x7AcTawkTil9EWpBTPTOTraA +lzJMQhNKmSLk0iIoTtAJtSZgUSpIIozqK6lenxQQDsHbXKU6h+u9H6KZE8YcjsFl +6vL7sw9BVotw/VxfgjQ5OSGLgoLrdVT0z5C2qOuwOgz1c7jNiJhtMdwN+cOtnJp2 +fuBgEYyE3eeuWogvkWoDcIA8r17Ixzkpq2oJsdvZcHZPIZShPKW2SHUsl98KDemu +y0pQyExmQUbwKE4vbFb9XuWCcL9XaOHQytyszt2DeD67AipvoBwVU7/LBOvqnsmy +hA== +-----END CERTIFICATE----- diff --git a/tools/ci/config/target-test.yml b/tools/ci/config/target-test.yml index 827e31858..6fe33ab95 100644 --- a/tools/ci/config/target-test.yml +++ b/tools/ci/config/target-test.yml @@ -184,7 +184,7 @@ test_weekend_network: example_test_001: extends: .example_test_template - parallel: 2 + parallel: 3 tags: - ESP32 - Example_WIFI