diff --git a/components/esp_http_client/esp_http_client.c b/components/esp_http_client/esp_http_client.c index a27cf9455e..ade9fb1c24 100644 --- a/components/esp_http_client/esp_http_client.c +++ b/components/esp_http_client/esp_http_client.c @@ -812,12 +812,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 fc855e00f9..6a78ed3f66 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 @@ -69,15 +70,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; @@ -214,19 +227,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; } @@ -264,10 +295,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) { - ESP_LOGI(TAG, "Connection closed, all data received"); - } else if (data_read < 0) { - ESP_LOGE(TAG, "Error: SSL data read error"); - return ESP_FAIL; + /* + * 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) { 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 0000000000..e397c82e62 --- /dev/null +++ b/examples/system/ota/advanced_https_ota/example_test.py @@ -0,0 +1,299 @@ +import re +import os +import sys +import socket +import BaseHTTPServer +import SimpleHTTPServer +from threading import Thread +import ssl + +try: + import IDF +except ImportError: + # this is a test case write with tiny-test-fw. + # to run test cases outside tiny-test-fw, + # we need to set environment variable `TEST_FW_PATH`, + # then get and insert `TEST_FW_PATH` to sys path before import FW module + test_fw_path = os.getenv("TEST_FW_PATH") + if test_fw_path and test_fw_path not in sys.path: + sys.path.insert(0, test_fw_path) + import IDF + +import DUT +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): + # parser = argparse.ArgumentParser() + # parser.add_argument('-p', '--port', dest='port', type= int, + # help= "Server Port", default= 8000) + # args = parser.parse_args() + 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() + + +@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") + # 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) + IDF.log_performance("advanced_https_ota_bin_size", "{}KB".format(bin_size // 1024)) + 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("Connected to WiFi network! Attempting to connect to server...", 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("Connected to WiFi network! Attempting to connect to server...", timeout=30) + dut1.reset() + + +@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") + # 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) + IDF.log_performance("advanced_https_ota_bin_size", "{}KB".format(bin_size // 1024)) + 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, 8002)) + thread1.daemon = True + thread1.start() + 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("Connected to WiFi network! Attempting to connect to server...", 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("Image validation failed, image is corrupted", timeout=30) + + +@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") + # 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) + IDF.log_performance("advanced_https_ota_bin_size", "{}KB".format(bin_size // 1024)) + 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, 8003)) + thread1.daemon = True + thread1.start() + 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("Connected to WiFi network! Attempting to connect to server...", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8003/" + truncated_bin_name)) + dut1.write("https://" + host_ip + ":8003/" + truncated_bin_name) + dut1.expect("advanced_https_ota_example: esp_https_ota_read_img_desc failed", timeout=30) + + +@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") + # 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+") + for i in range(random_bin_size): + fo.write(str(random.randrange(0,255,1))) + fo.close() + bin_size = os.path.getsize(binary_file) + IDF.log_performance("advanced_https_ota_bin_size", "{}KB".format(bin_size // 1024)) + 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, 8004)) + thread1.daemon = True + thread1.start() + 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("Connected to WiFi network! Attempting to connect to server...", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8004/" + random_bin_name)) + dut1.write("https://" + host_ip + ":8004/" + 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 f3f71e3a84..47c1591d11 100644 --- a/examples/system/ota/advanced_https_ota/main/Kconfig.projbuild +++ b/examples/system/ota/advanced_https_ota/main/Kconfig.projbuild @@ -18,4 +18,26 @@ menu "Example Configuration" help URL of server which hosts the firmware image. + config EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN + bool + default y if 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 241495be39..3ec256a4d7 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 @@ -22,10 +22,21 @@ #include "nvs.h" #include "nvs_flash.h" +#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN +#include "esp_vfs_dev.h" +#include "driver/uart.h" +#endif + +#if CONFIG_EXAMPLE_CONNECT_WIFI +#include "esp_wifi.h" +#endif + 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 + /* FreeRTOS event group to signal when we are connected & ready to make a request */ static EventGroupHandle_t wifi_event_group; @@ -34,6 +45,24 @@ static EventGroupHandle_t wifi_event_group; to the AP with an IP? */ const int CONNECTED_BIT = BIT0; +#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN +static esp_err_t example_configure_stdin_stdout(void) +{ + // Initialize VFS & UART so we can use std::cout/cin + setvbuf(stdin, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); + /* Install UART driver for interrupt-driven reads and writes */ + ESP_ERROR_CHECK( uart_driver_install( (uart_port_t)CONFIG_CONSOLE_UART_NUM, + 256, 0, 0, NULL, 0) ); + /* Tell VFS to use UART driver */ + esp_vfs_dev_uart_use_driver(CONFIG_CONSOLE_UART_NUM); + esp_vfs_dev_uart_set_rx_line_endings(ESP_LINE_ENDINGS_CR); + /* Move the caret to the beginning of the next line on '\n' */ + esp_vfs_dev_uart_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); + return ESP_OK; +} +#endif + static esp_err_t event_handler(void *ctx, system_event_t *event) { switch (event->event_id) { @@ -87,10 +116,12 @@ 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; } @@ -106,8 +137,27 @@ void advanced_ota_example_task(void * pvParameter) esp_http_client_config_t config = { .url = CONFIG_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, }; @@ -142,7 +192,7 @@ void advanced_ota_example_task(void * pvParameter) ESP_LOGD(TAG, "Image bytes read: %d", esp_https_ota_get_image_len_read(https_ota_handle)); } - if (esp_https_ota_is_complete_data_received(&https_ota_handle) != true) { + if (esp_https_ota_is_complete_data_received(https_ota_handle) != true) { // the OTA image was not completely received and user can customise the response to this situation. ESP_LOGE(TAG, "Complete data was not received."); } @@ -154,11 +204,11 @@ ota_end: vTaskDelay(1000 / portTICK_PERIOD_MS); esp_restart(); } else { - ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed..."); - } - - while (1) { - vTaskDelay(1000 / portTICK_PERIOD_MS); + 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); + vTaskDelete(NULL); } } @@ -177,6 +227,14 @@ void app_main() ESP_ERROR_CHECK( err ); initialise_wifi(); + +#if CONFIG_EXAMPLE_CONNECT_WIFI + /* Ensure to disable any WiFi power save mode, this allows best throughput + * and hence timings for overall OTA operation. + */ + esp_wifi_set_ps(WIFI_PS_NONE); +#endif // CONFIG_EXAMPLE_CONNECT_WIFI + xTaskCreate(&advanced_ota_example_task, "advanced_ota_example_task", 1024 * 8, NULL, 5, 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 0000000000..75b1f21a9d --- /dev/null +++ b/examples/system/ota/advanced_https_ota/sdkconfig.ci @@ -0,0 +1,4 @@ +CONFIG_FIRMWARE_UPGRADE_URL="FROM_STDIN" +CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK=y +CONFIG_EXAMPLE_SKIP_VERSION_CHECK=y +CONFIG_EXAMPLE_OTA_RECV_TIMEOUT=300 \ No newline at end of file diff --git a/examples/system/ota/advanced_https_ota/sdkconfig.defaults b/examples/system/ota/advanced_https_ota/sdkconfig.defaults new file mode 100644 index 0000000000..2289a82300 --- /dev/null +++ b/examples/system/ota/advanced_https_ota/sdkconfig.defaults @@ -0,0 +1,4 @@ +# Default sdkconfig parameters to use the OTA +# partition table layout, with a 4MB flash size +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_TWO_OTA=y 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 e69de29bb2..5b21a5c2ad 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 0000000000..444efcbed7 --- /dev/null +++ b/examples/system/ota/native_ota_example/example_test.py @@ -0,0 +1,299 @@ +import re +import os +import sys +import socket +import BaseHTTPServer +import SimpleHTTPServer +from threading import Thread +import ssl + +try: + import IDF +except ImportError: + # this is a test case write with tiny-test-fw. + # to run test cases outside tiny-test-fw, + # we need to set environment variable `TEST_FW_PATH`, + # then get and insert `TEST_FW_PATH` to sys path before import FW module + test_fw_path = os.getenv("TEST_FW_PATH") + if test_fw_path and test_fw_path not in sys.path: + sys.path.insert(0, test_fw_path) + import IDF + +import DUT +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): + # parser = argparse.ArgumentParser() + # parser.add_argument('-p', '--port', dest='port', type= int, + # help= "Server Port", default= 8000) + # args = parser.parse_args() + 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() + + +@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") + # 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) + IDF.log_performance("native_ota_bin_size", "{}KB".format(bin_size // 1024)) + 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, 8005)) + 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("Connect to Wifi ! Start to Connect to Server....", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8005/" + bin_name)) + dut1.write("https://" + host_ip + ":8005/" + bin_name) + dut1.expect("Loaded app from partition at offset", timeout=60) + dut1.expect("Starting OTA example", timeout=30) + dut1.reset() + + +@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") + # 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) + IDF.log_performance("native_ota_bin_size", "{}KB".format(bin_size // 1024)) + 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, 8006)) + thread1.daemon = True + thread1.start() + 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("Connect to Wifi ! Start to Connect to Server....", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8006/" + truncated_bin_name)) + dut1.write("https://" + host_ip + ":8006/" + truncated_bin_name) + dut1.expect("native_ota_example: Image validation failed, image is corrupted", timeout=20) + + +@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") + # 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) + IDF.log_performance("native_ota_bin_size", "{}KB".format(bin_size // 1024)) + 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, 8007)) + thread1.daemon = True + thread1.start() + 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("Connect to Wifi ! Start to Connect to Server....", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8007/" + truncated_bin_name)) + dut1.write("https://" + host_ip + ":8007/" + truncated_bin_name) + dut1.expect("native_ota_example: received package is not fit len", timeout=20) + + +@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") + # 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+") + for i in range(random_bin_size): + fo.write(str(random.randrange(0,255,1))) + fo.close() + bin_size = os.path.getsize(binary_file) + IDF.log_performance("native_ota_bin_size", "{}KB".format(bin_size // 1024)) + 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, 8008)) + thread1.daemon = True + thread1.start() + 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("Connect to Wifi ! Start to Connect to Server....", timeout=30) + + print("writing to device: {}".format("https://" + host_ip + ":8008/" + random_bin_name)) + dut1.write("https://" + host_ip + ":8008/" + 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 ebb9995729..b153e3a4d4 100644 --- a/examples/system/ota/native_ota_example/main/Kconfig.projbuild +++ b/examples/system/ota/native_ota_example/main/Kconfig.projbuild @@ -22,6 +22,22 @@ menu "Example Configuration" See example README.md for details. + config EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN + bool + default y if 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 GPIO_DIAGNOSTIC int "Number of the GPIO input for diagnostic" range 0 39 @@ -33,4 +49,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 b3b34d57d5..744e4ef21c 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 @@ -23,10 +23,20 @@ #include "nvs.h" #include "nvs_flash.h" #include "driver/gpio.h" +#include "errno.h" + +#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN +#include "esp_vfs_dev.h" +#include "driver/uart.h" +#endif #define EXAMPLE_WIFI_SSID CONFIG_WIFI_SSID #define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD #define EXAMPLE_SERVER_URL CONFIG_FIRMWARE_UPG_URL +#if CONFIG_EXAMPLE_CONNECT_WIFI +#include "esp_wifi.h" +#endif + #define BUFFSIZE 1024 #define HASH_LEN 32 /* SHA-256 digest length */ @@ -36,6 +46,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 + /* FreeRTOS event group to signal when we are connected & ready to make a request */ static EventGroupHandle_t wifi_event_group; @@ -44,6 +56,24 @@ static EventGroupHandle_t wifi_event_group; to the AP with an IP? */ const int CONNECTED_BIT = BIT0; +#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDIN +static esp_err_t example_configure_stdin_stdout(void) +{ + // Initialize VFS & UART so we can use std::cout/cin + setvbuf(stdin, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); + /* Install UART driver for interrupt-driven reads and writes */ + ESP_ERROR_CHECK( uart_driver_install( (uart_port_t)CONFIG_CONSOLE_UART_NUM, + 256, 0, 0, NULL, 0) ); + /* Tell VFS to use UART driver */ + esp_vfs_dev_uart_use_driver(CONFIG_CONSOLE_UART_NUM); + esp_vfs_dev_uart_set_rx_line_endings(ESP_LINE_ENDINGS_CR); + /* Move the caret to the beginning of the next line on '\n' */ + esp_vfs_dev_uart_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); + return ESP_OK; +} +#endif + static esp_err_t event_handler(void *ctx, system_event_t *event) { switch (event->event_id) { @@ -151,7 +181,27 @@ static void ota_example_task(void *pvParameter) esp_http_client_config_t config = { .url = EXAMPLE_SERVER_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"); @@ -208,12 +258,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; @@ -238,19 +289,33 @@ 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,all data received"); - 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); + ESP_LOGI(TAG, "Total Write binary data length: %d", binary_file_length); if (esp_http_client_is_complete_data_received(client) != true) { ESP_LOGE(TAG, "Error in receiving complete file"); http_cleanup(client); task_fatal_error(); } - if (esp_ota_end(update_handle) != ESP_OK) { - ESP_LOGE(TAG, "esp_ota_end failed!"); + 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(); } @@ -336,5 +401,13 @@ void app_main() ESP_ERROR_CHECK( err ); initialise_wifi(); + +#if CONFIG_EXAMPLE_CONNECT_WIFI + /* Ensure to disable any WiFi power save mode, this allows best throughput + * and hence timings for overall OTA operation. + */ + esp_wifi_set_ps(WIFI_PS_NONE); +#endif // CONFIG_EXAMPLE_CONNECT_WIFI + xTaskCreate(&ota_example_task, "ota_example_task", 8192, NULL, 5, NULL); } 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 0000000000..f2cfb16670 --- /dev/null +++ b/examples/system/ota/native_ota_example/sdkconfig.ci @@ -0,0 +1,4 @@ +CONFIG_FIRMWARE_UPG_URL="FROM_STDIN" +CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECK=y +CONFIG_EXAMPLE_SKIP_VERSION_CHECK=y +CONFIG_EXAMPLE_OTA_RECV_TIMEOUT=300 \ No newline at end of file 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 e69de29bb2..5b21a5c2ad 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/examples/system/ota/simple_ota_example/main/simple_ota_example.c b/examples/system/ota/simple_ota_example/main/simple_ota_example.c index 139e83747d..c06d6297c0 100644 --- a/examples/system/ota/simple_ota_example/main/simple_ota_example.c +++ b/examples/system/ota/simple_ota_example/main/simple_ota_example.c @@ -27,6 +27,10 @@ #include "driver/uart.h" #endif +#if CONFIG_EXAMPLE_CONNECT_WIFI +#include "esp_wifi.h" +#endif + static const char *TAG = "simple_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"); @@ -187,5 +191,13 @@ void app_main() ESP_ERROR_CHECK( err ); initialise_wifi(); + +#if CONFIG_EXAMPLE_CONNECT_WIFI + /* Ensure to disable any WiFi power save mode, this allows best throughput + * and hence timings for overall OTA operation. + */ + esp_wifi_set_ps(WIFI_PS_NONE); +#endif // CONFIG_EXAMPLE_CONNECT_WIFI + xTaskCreate(&simple_ota_example_task, "ota_example_task", 8192, NULL, 5, NULL); }