From b26f6662f195f1cf02b0648c7638b232276c8a7d Mon Sep 17 00:00:00 2001 From: Anurag Kar Date: Fri, 1 Feb 2019 18:41:46 +0530 Subject: [PATCH] http_server examples : Updated tests and examples to demonstrate usage of `httpd_register_err_handler()` and accommodate for changes in default error handler behavior --- .../http_server/advanced_tests/main/tests.c | 14 +- .../advanced_tests/scripts/test.py | 12 +- .../file_serving/main/file_server.c | 80 ++++---- .../protocols/http_server/simple/main/main.c | 33 ++- .../http_server/simple/scripts/client.py | 189 ++++++++++++++---- 5 files changed, 237 insertions(+), 91 deletions(-) diff --git a/examples/protocols/http_server/advanced_tests/main/tests.c b/examples/protocols/http_server/advanced_tests/main/tests.c index 8845585be..589bf9646 100644 --- a/examples/protocols/http_server/advanced_tests/main/tests.c +++ b/examples/protocols/http_server/advanced_tests/main/tests.c @@ -55,6 +55,7 @@ esp_err_t echo_post_handler(httpd_req_t *req) int ret; if (!buf) { + ESP_LOGE(TAG, "Failed to allocate memory of %d bytes!", req->content_len + 1); httpd_resp_send_500(req); return ESP_FAIL; } @@ -84,12 +85,15 @@ esp_err_t echo_post_handler(httpd_req_t *req) if (hdr_len) { /* Read Custom header value */ req_hdr = malloc(hdr_len + 1); - if (req_hdr) { - httpd_req_get_hdr_value_str(req, "Custom", req_hdr, hdr_len + 1); - - /* Set as additional header for response packet */ - httpd_resp_set_hdr(req, "Custom", req_hdr); + if (!req_hdr) { + ESP_LOGE(TAG, "Failed to allocate memory of %d bytes!", hdr_len + 1); + httpd_resp_send_500(req); + return ESP_FAIL; } + httpd_req_get_hdr_value_str(req, "Custom", req_hdr, hdr_len + 1); + + /* Set as additional header for response packet */ + httpd_resp_set_hdr(req, "Custom", req_hdr); } httpd_resp_send(req, buf, req->content_len); free (req_hdr); diff --git a/examples/protocols/http_server/advanced_tests/scripts/test.py b/examples/protocols/http_server/advanced_tests/scripts/test.py index 14371abd8..ceddc1934 100644 --- a/examples/protocols/http_server/advanced_tests/scripts/test.py +++ b/examples/protocols/http_server/advanced_tests/scripts/test.py @@ -367,7 +367,7 @@ def put_hello(dut, port): def post_hello(dut, port): # POST /hello returns 405' - Utility.console_log("[test] POST /hello returns 404 =>", end=' ') + Utility.console_log("[test] POST /hello returns 405 =>", end=' ') conn = http.client.HTTPConnection(dut, int(port), timeout=15) conn.request("POST", "/hello", "Hello") resp = conn.getresponse() @@ -541,8 +541,10 @@ def leftover_data_test(dut, port): if not test_val("False URI Status", str(404), str(resp.status)): s.close() return False - resp.read() + # socket would have been closed by server due to error + s.close() + s = http.client.HTTPConnection(dut + ":" + port, timeout=15) s.request("GET", url='/hello') resp = s.getresponse() if not test_val("Hello World Data", "Hello World!", resp.read().decode()): @@ -637,7 +639,7 @@ def code_500_server_error_test(dut, port): Utility.console_log("[test] 500 Server Error test =>", end=' ') s = Session(dut, port) # Sending a very large content length will cause malloc to fail - content_len = 2**31 + content_len = 2**30 s.client.sendall(("POST /echo HTTP/1.1\r\nHost: " + dut + "\r\nContent-Length: " + str(content_len) + "\r\n\r\nABCD").encode()) s.read_resp_hdrs() s.read_resp_data() @@ -802,7 +804,7 @@ def send_postx_hdr_len(dut, port, length): hdr = s.read_resp_hdrs() resp = s.read_resp_data() s.close() - if "Custom" in hdr: + if hdr and ("Custom" in hdr): return (hdr["Custom"] == custom_hdr_val), resp return False, s.status @@ -826,7 +828,7 @@ def test_upgrade_not_supported(dut, port): s.client.sendall(("OPTIONS * HTTP/1.1\r\nHost:" + dut + "\r\nUpgrade: TLS/1.0\r\nConnection: Upgrade\r\n\r\n").encode()) s.read_resp_hdrs() s.read_resp_data() - if not test_val("Client Error", "200", s.status): + if not test_val("Client Error", "400", s.status): s.close() return False s.close() diff --git a/examples/protocols/http_server/file_serving/main/file_server.c b/examples/protocols/http_server/file_serving/main/file_server.c index 2ca428f9d..9d301b45b 100644 --- a/examples/protocols/http_server/file_serving/main/file_server.c +++ b/examples/protocols/http_server/file_serving/main/file_server.c @@ -72,9 +72,10 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req) const size_t entrypath_offset = strlen(fullpath); if (!dir) { - /* If opening directory failed then send 404 server error */ - httpd_resp_send_404(req); - return ESP_OK; + ESP_LOGE(TAG, "Failed to stat dir : %s", fullpath); + /* Respond with 404 Not Found */ + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Directory does not exist"); + return ESP_FAIL; } /* Send HTML file header */ @@ -172,18 +173,17 @@ static esp_err_t http_resp_file(httpd_req_t *req) strcat(filepath, req->uri); if (stat(filepath, &file_stat) == -1) { ESP_LOGE(TAG, "Failed to stat file : %s", filepath); - /* If file doesn't exist respond with 404 Not Found */ - httpd_resp_send_404(req); - return ESP_OK; + /* Respond with 404 Not Found */ + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist"); + return ESP_FAIL; } fd = fopen(filepath, "r"); if (!fd) { ESP_LOGE(TAG, "Failed to read existing file : %s", filepath); - /* If file exists but unable to open respond with 500 Server Error */ - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to read existing file!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file"); + return ESP_FAIL; } ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filepath, file_stat.st_size); @@ -202,10 +202,9 @@ static esp_err_t http_resp_file(httpd_req_t *req) ESP_LOGE(TAG, "File sending failed!"); /* Abort sending file */ httpd_resp_sendstr_chunk(req, NULL); - /* Send error message with status code */ - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to send file!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); + return ESP_FAIL; } /* Keep looping till the whole file is sent */ @@ -249,10 +248,8 @@ static esp_err_t upload_post_handler(httpd_req_t *req) if (strlen(filename) == 0 || filename[strlen(filename) - 1] == '/') { ESP_LOGE(TAG, "Invalid file name : %s", filename); /* Respond with 400 Bad Request */ - httpd_resp_set_status(req, "400 Bad Request"); - /* Send failure reason */ - httpd_resp_sendstr(req, "Invalid file name!"); - return ESP_OK; + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid file name"); + return ESP_FAIL; } /* Retrieve the base path of file storage to construct the full path */ @@ -262,18 +259,18 @@ static esp_err_t upload_post_handler(httpd_req_t *req) strcat(filepath, filename); if (stat(filepath, &file_stat) == 0) { ESP_LOGE(TAG, "File already exists : %s", filepath); - /* If file exists respond with 400 Bad Request */ - httpd_resp_set_status(req, "400 Bad Request"); - httpd_resp_sendstr(req, "File already exists!"); - return ESP_OK; + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File already exists"); + return ESP_FAIL; } /* File cannot be larger than a limit */ if (req->content_len > MAX_FILE_SIZE) { ESP_LOGE(TAG, "File too large : %d bytes", req->content_len); - httpd_resp_set_status(req, "400 Bad Request"); - httpd_resp_sendstr(req, "File size must be less than " - MAX_FILE_SIZE_STR "!"); + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, + "File size must be less than " + MAX_FILE_SIZE_STR "!"); /* Return failure to close underlying connection else the * incoming file content will keep the socket busy */ return ESP_FAIL; @@ -282,10 +279,9 @@ static esp_err_t upload_post_handler(httpd_req_t *req) fd = fopen(filepath, "w"); if (!fd) { ESP_LOGE(TAG, "Failed to create file : %s", filepath); - /* If file creation failed, respond with 500 Server Error */ - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to create file!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create file"); + return ESP_FAIL; } ESP_LOGI(TAG, "Receiving file : %s...", filename); @@ -314,10 +310,9 @@ static esp_err_t upload_post_handler(httpd_req_t *req) unlink(filepath); ESP_LOGE(TAG, "File reception failed!"); - /* Return failure reason with status code */ - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to receive file!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file"); + return ESP_FAIL; } /* Write buffer content to file on storage */ @@ -328,9 +323,9 @@ static esp_err_t upload_post_handler(httpd_req_t *req) unlink(filepath); ESP_LOGE(TAG, "File write failed!"); - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to write file to storage!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write file to storage"); + return ESP_FAIL; } /* Keep track of remaining size of @@ -363,10 +358,8 @@ static esp_err_t delete_post_handler(httpd_req_t *req) if (strlen(filename) == 0 || filename[strlen(filename) - 1] == '/') { ESP_LOGE(TAG, "Invalid file name : %s", filename); /* Respond with 400 Bad Request */ - httpd_resp_set_status(req, "400 Bad Request"); - /* Send failure reason */ - httpd_resp_sendstr(req, "Invalid file name!"); - return ESP_OK; + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid file name"); + return ESP_FAIL; } /* Retrieve the base path of file storage to construct the full path */ @@ -376,10 +369,9 @@ static esp_err_t delete_post_handler(httpd_req_t *req) strcat(filepath, filename); if (stat(filepath, &file_stat) == -1) { ESP_LOGE(TAG, "File does not exist : %s", filename); - /* If file does not exist respond with 400 Bad Request */ - httpd_resp_set_status(req, "400 Bad Request"); - httpd_resp_sendstr(req, "File does not exist!"); - return ESP_OK; + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File does not exist"); + return ESP_FAIL; } ESP_LOGI(TAG, "Deleting file : %s", filename); diff --git a/examples/protocols/http_server/simple/main/main.c b/examples/protocols/http_server/simple/main/main.c index d4b7b3869..9ada49cab 100644 --- a/examples/protocols/http_server/simple/main/main.c +++ b/examples/protocols/http_server/simple/main/main.c @@ -152,6 +152,33 @@ httpd_uri_t echo = { .user_ctx = NULL }; +/* This handler allows the custom error handling functionality to be + * tested from client side. For that, when a PUT request 0 is sent to + * URI /ctrl, the /hello and /echo URIs are unregistered and following + * custom error handler http_404_error_handler() is registered. + * Afterwards, when /hello or /echo is requested, this custom error + * handler is invoked which, after sending an error message to client, + * either closes the underlying socket (when requested URI is /echo) + * or keeps it open (when requested URI is /hello). This allows the + * client to infer if the custom error handler is functioning as expected + * by observing the socket state. + */ +esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err) +{ + if (strcmp("/hello", req->uri) == 0) { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "/hello URI is not available"); + /* Return ESP_OK to keep underlying socket open */ + return ESP_OK; + } else if (strcmp("/echo", req->uri) == 0) { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "/echo URI is not available"); + /* Return ESP_FAIL to close underlying socket */ + return ESP_FAIL; + } + /* For any other URI send 404 and close socket */ + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Some 404 error message"); + return ESP_FAIL; +} + /* An HTTP PUT handler. This demonstrates realtime * registration and deregistration of URI handlers */ @@ -168,15 +195,19 @@ esp_err_t ctrl_put_handler(httpd_req_t *req) } if (buf == '0') { - /* Handler can be unregistered using the uri string */ + /* URI handlers can be unregistered using the uri string */ ESP_LOGI(TAG, "Unregistering /hello and /echo URIs"); httpd_unregister_uri(req->handle, "/hello"); httpd_unregister_uri(req->handle, "/echo"); + /* Register the custom error handler */ + httpd_register_err_handler(req->handle, HTTPD_404_NOT_FOUND, http_404_error_handler); } else { ESP_LOGI(TAG, "Registering /hello and /echo URIs"); httpd_register_uri_handler(req->handle, &hello); httpd_register_uri_handler(req->handle, &echo); + /* Unregister custom error handler */ + httpd_register_err_handler(req->handle, HTTPD_404_NOT_FOUND, NULL); } /* Respond with empty body */ diff --git a/examples/protocols/http_server/simple/scripts/client.py b/examples/protocols/http_server/simple/scripts/client.py index b43af0801..5e070b98b 100644 --- a/examples/protocols/http_server/simple/scripts/client.py +++ b/examples/protocols/http_server/simple/scripts/client.py @@ -19,7 +19,20 @@ from __future__ import unicode_literals from builtins import str import http.client import argparse -import Utility + +try: + import Utility +except ImportError: + import sys + import os + + # This environment variable is expected on the host machine + # > export TEST_FW_PATH=~/esp/esp-idf/tools/tiny-test-fw + 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 Utility def verbose_print(verbosity, *args): @@ -27,6 +40,16 @@ def verbose_print(verbosity, *args): Utility.console_log(''.join(str(elems) for elems in args)) +def test_val(text, expected, received): + if expected != received: + Utility.console_log(" Fail!") + Utility.console_log(" [reason] " + text + ":") + Utility.console_log(" expected: " + str(expected)) + Utility.console_log(" received: " + str(received)) + return False + return True + + def test_get_handler(ip, port, verbosity=False): verbose_print(verbosity, "======== GET HANDLER TEST =============") # Establish HTTP connection @@ -44,12 +67,15 @@ def test_get_handler(ip, port, verbosity=False): resp = sess.getresponse() resp_hdrs = resp.getheaders() resp_data = resp.read().decode() - try: - if resp.getheader("Custom-Header-1") != "Custom-Value-1": - return False - if resp.getheader("Custom-Header-2") != "Custom-Value-2": - return False - except Exception: + # Close HTTP connection + sess.close() + + if not ( + test_val("Status code mismatch", 200, resp.status) and + test_val("Response mismatch", "Custom-Value-1", resp.getheader("Custom-Header-1")) and + test_val("Response mismatch", "Custom-Value-2", resp.getheader("Custom-Header-2")) and + test_val("Response mismatch", "Hello World!", resp_data) + ): return False verbose_print(verbosity, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv") @@ -59,10 +85,7 @@ def test_get_handler(ip, port, verbosity=False): verbose_print(verbosity, "\t", k, ": ", v) verbose_print(verbosity, "Response Data : " + resp_data) verbose_print(verbosity, "========================================\n") - - # Close HTTP connection - sess.close() - return (resp_data == "Hello World!") + return True def test_post_handler(ip, port, msg, verbosity=False): @@ -82,7 +105,7 @@ def test_post_handler(ip, port, msg, verbosity=False): # Close HTTP connection sess.close() - return (resp_data == msg) + return test_val("Response mismatch", msg, resp_data) def test_put_handler(ip, port, verbosity=False): @@ -91,31 +114,125 @@ def test_put_handler(ip, port, verbosity=False): verbose_print(verbosity, "Connecting to => " + ip + ":" + port) sess = http.client.HTTPConnection(ip + ":" + port, timeout=15) - # PUT message to /ctrl to disable /hello URI handler - verbose_print(verbosity, "Disabling /hello handler") + # PUT message to /ctrl to disable /hello and /echo URI handlers + # and set 404 error handler to custom http_404_error_handler() + verbose_print(verbosity, "Disabling /hello and /echo handlers") sess.request("PUT", url="/ctrl", body="0") resp = sess.getresponse() resp.read() - sess.request("GET", url="/hello") - resp = sess.getresponse() - resp_data1 = resp.read().decode() - verbose_print(verbosity, "Response on GET /hello : " + resp_data1) + try: + # Send HTTP request to /hello URI + sess.request("GET", url="/hello") + resp = sess.getresponse() + resp_data = resp.read().decode() - # PUT message to /ctrl to enable /hello URI handler - verbose_print(verbosity, "Enabling /hello handler") - sess.request("PUT", url="/ctrl", body="1") - resp = sess.getresponse() - resp.read() + # 404 Error must be returned from server as URI /hello is no longer available. + # But the custom error handler http_404_error_handler() will not close the + # session if the requested URI is /hello + if not test_val("Status code mismatch", 404, resp.status): + raise AssertionError - sess.request("GET", url="/hello") - resp = sess.getresponse() - resp_data2 = resp.read().decode() - verbose_print(verbosity, "Response on GET /hello : " + resp_data2) + # Compare error response string with expectation + verbose_print(verbosity, "Response on GET /hello : " + resp_data) + if not test_val("Response mismatch", "/hello URI is not available", resp_data): + raise AssertionError - # Close HTTP connection - sess.close() - return ((resp_data2 == "Hello World!") and (resp_data1 == "This URI doesn't exist")) + # Using same session for sending an HTTP request to /echo, as it is expected + # that the custom error handler http_404_error_handler() would not have closed + # the session + sess.request("POST", url="/echo", body="Some content") + resp = sess.getresponse() + resp_data = resp.read().decode() + + # 404 Error must be returned from server as URI /hello is no longer available. + # The custom error handler http_404_error_handler() will close the session + # this time as the requested URI is /echo + if not test_val("Status code mismatch", 404, resp.status): + raise AssertionError + + # Compare error response string with expectation + verbose_print(verbosity, "Response on POST /echo : " + resp_data) + if not test_val("Response mismatch", "/echo URI is not available", resp_data): + raise AssertionError + + try: + # Using same session should fail as by now the session would have closed + sess.request("POST", url="/hello", body="Some content") + resp = sess.getresponse() + resp.read().decode() + + # If control reaches this point then the socket was not closed. + # This is not expected + verbose_print(verbosity, "Socket not closed by server") + raise AssertionError + + except http.client.HTTPException: + # Catch socket error as we tried to communicate with an already closed socket + pass + + except http.client.HTTPException: + verbose_print(verbosity, "Socket closed by server") + return False + + except AssertionError: + return False + + finally: + # Close HTTP connection + sess.close() + + verbose_print(verbosity, "Enabling /hello handler") + # Create new connection + sess = http.client.HTTPConnection(ip + ":" + port, timeout=15) + # PUT message to /ctrl to enable /hello URI handler + # and restore 404 error handler to default + sess.request("PUT", url="/ctrl", body="1") + resp = sess.getresponse() + resp.read() + # Close HTTP connection + sess.close() + + # Create new connection + sess = http.client.HTTPConnection(ip + ":" + port, timeout=15) + + try: + # Sending HTTP request to /hello should work now + sess.request("GET", url="/hello") + resp = sess.getresponse() + resp_data = resp.read().decode() + + if not test_val("Status code mismatch", 200, resp.status): + raise AssertionError + + verbose_print(verbosity, "Response on GET /hello : " + resp_data) + if not test_val("Response mismatch", "Hello World!", resp_data): + raise AssertionError + + # 404 Error handler should have been restored to default + sess.request("GET", url="/invalid") + resp = sess.getresponse() + resp_data = resp.read().decode() + + if not test_val("Status code mismatch", 404, resp.status): + raise AssertionError + + verbose_print(verbosity, "Response on GET /invalid : " + resp_data) + if not test_val("Response mismatch", "This URI does not exist", resp_data): + raise AssertionError + + except http.client.HTTPException: + verbose_print(verbosity, "Socket closed by server") + return False + + except AssertionError: + return False + + finally: + # Close HTTP connection + sess.close() + + return True def test_custom_uri_query(ip, port, query, verbosity=False): @@ -138,7 +255,7 @@ def test_custom_uri_query(ip, port, query, verbosity=False): # Close HTTP connection sess.close() - return (resp_data == "Hello World!") + return "Hello World!" == resp_data if __name__ == '__main__': @@ -154,9 +271,9 @@ if __name__ == '__main__': port = args['port'] msg = args['msg'] - if not test_get_handler(ip, port, True): - Utility.console_log("Failed!") - if not test_post_handler(ip, port, msg, True): - Utility.console_log("Failed!") - if not test_put_handler(ip, port, True): + if not ( + test_get_handler(ip, port, True) and + test_put_handler(ip, port, True) and + test_post_handler(ip, port, msg, True) + ): Utility.console_log("Failed!")