diff --git a/examples/protocols/http_server/ws_echo_server/CMakeLists.txt b/examples/protocols/http_server/ws_echo_server/CMakeLists.txt new file mode 100644 index 000000000..6ee7dd4fc --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/CMakeLists.txt @@ -0,0 +1,10 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +# (Not part of the boilerplate) +# This example uses an extra component for common functions such as Wi-Fi and Ethernet connection. +set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(ws_echo_server) diff --git a/examples/protocols/http_server/ws_echo_server/Makefile b/examples/protocols/http_server/ws_echo_server/Makefile new file mode 100644 index 000000000..394c5e53a --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/Makefile @@ -0,0 +1,11 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := ws_echo_server + +EXTRA_COMPONENT_DIRS = $(IDF_PATH)/examples/common_components/protocol_examples_common + +include $(IDF_PATH)/make/project.mk + diff --git a/examples/protocols/http_server/ws_echo_server/README.md b/examples/protocols/http_server/ws_echo_server/README.md new file mode 100644 index 000000000..5b0cef507 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/README.md @@ -0,0 +1,24 @@ +# WS test + +The Example consists of HTTPD server demo with demostration of URI handling : + 1. URI \hello for GET command returns "Hello World!" message + 2. URI \echo for POST command echoes back the POSTed message + +* Open the project configuration menu (`idf.py menuconfig`) to configure Wi-Fi or Ethernet. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details. + +* In order to test the HTTPD server persistent sockets demo : + 1. compile and burn the firmware `idf.py -p PORT flash` + 2. run `idf.py -p PORT monitor` and note down the IP assigned to your ESP module. The default port is 80 + 3. test the example : + * run the test script : "python scripts/client.py \ \ \" + * the provided test script first does a GET \hello and displays the response + * the script does a POST to \echo with the user input \ and displays the response + * or use curl (asssuming IP is 192.168.43.130): + 1. "curl 192.168.43.130:80/hello" - tests the GET "\hello" handler + 2. "curl -X POST --data-binary @anyfile 192.168.43.130:80/echo > tmpfile" + * "anyfile" is the file being sent as request body and "tmpfile" is where the body of the response is saved + * since the server echoes back the request body, the two files should be same, as can be confirmed using : "cmp anyfile tmpfile" + 3. "curl -X PUT -d "0" 192.168.43.130:80/ctrl" - disable /hello and /echo handlers + 4. "curl -X PUT -d "1" 192.168.43.130:80/ctrl" - enable /hello and /echo handlers + +See the README.md file in the upper level 'examples' directory for more information about examples. diff --git a/examples/protocols/http_server/ws_echo_server/main/CMakeLists.txt b/examples/protocols/http_server/ws_echo_server/main/CMakeLists.txt new file mode 100644 index 000000000..addcda453 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "ws_echo_server.c" + INCLUDE_DIRS ".") \ No newline at end of file diff --git a/examples/protocols/http_server/ws_echo_server/main/component.mk b/examples/protocols/http_server/ws_echo_server/main/component.mk new file mode 100644 index 000000000..0b9d7585e --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/main/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c b/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c new file mode 100644 index 000000000..9809ec62f --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/main/ws_echo_server.c @@ -0,0 +1,128 @@ +/* WebSocket Echo Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include +#include +#include +#include +#include "nvs_flash.h" +#include "esp_netif.h" +#include "esp_eth.h" +#include "protocol_examples_common.h" + +#include + +/* A simple example that demonstrates using websocket echo server + */ + +static const char *TAG = "ws_echo_server"; + + +static esp_err_t ws_ping_handler(httpd_req_t *req) +{ + uint8_t buf[128] = { 0 }; + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.payload = buf; + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 128); + ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload); + ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type); + + ret = ret ? : httpd_ws_send_frame(req, &ws_pkt); + return ret; +} + +static const httpd_uri_t ws = { + .uri = "/ws", + .method = HTTP_GET, + .handler = ws_ping_handler, + .user_ctx = NULL, + .is_websocket = true +}; + + +static httpd_handle_t start_webserver(void) +{ + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + if (httpd_start(&server, &config) == ESP_OK) { + // Set URI handlers + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &ws); + return server; + } + + ESP_LOGI(TAG, "Error starting server!"); + return NULL; +} + +static void stop_webserver(httpd_handle_t server) +{ + // Stop the httpd server + httpd_stop(server); +} + +static void disconnect_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + httpd_handle_t* server = (httpd_handle_t*) arg; + if (*server) { + ESP_LOGI(TAG, "Stopping webserver"); + stop_webserver(*server); + *server = NULL; + } +} + +static void connect_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + httpd_handle_t* server = (httpd_handle_t*) arg; + if (*server == NULL) { + ESP_LOGI(TAG, "Starting webserver"); + *server = start_webserver(); + } +} + + +void app_main(void) +{ + static httpd_handle_t server = NULL; + + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + * Read "Establishing Wi-Fi or Ethernet Connection" section in + * examples/protocols/README.md for more information about this function. + */ + ESP_ERROR_CHECK(example_connect()); + + /* Register event handlers to stop the server when Wi-Fi or Ethernet is disconnected, + * and re-start it upon connection. + */ +#ifdef CONFIG_EXAMPLE_CONNECT_WIFI + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &connect_handler, &server)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &disconnect_handler, &server)); +#endif // CONFIG_EXAMPLE_CONNECT_WIFI +#ifdef CONFIG_EXAMPLE_CONNECT_ETHERNET + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &connect_handler, &server)); + ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ETHERNET_EVENT_DISCONNECTED, &disconnect_handler, &server)); +#endif // CONFIG_EXAMPLE_CONNECT_ETHERNET + + /* Start the server for the first time */ + server = start_webserver(); +} diff --git a/examples/protocols/http_server/ws_echo_server/sdkconfig.ci b/examples/protocols/http_server/ws_echo_server/sdkconfig.ci new file mode 100644 index 000000000..2cc339d53 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/sdkconfig.ci @@ -0,0 +1,2 @@ +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y + diff --git a/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py b/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py new file mode 100644 index 000000000..2d282d7a2 --- /dev/null +++ b/examples/protocols/http_server/ws_echo_server/ws_server_example_test.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +import re +from tiny_test_fw import Utility +import ttfw_idf +import os +import six +import socket +import hashlib +import base64 + + +OPCODE_TEXT = 0x1 +OPCODE_BIN = 0x2 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xa + + +class WsClient: + def __init__(self, ip, port): + self.port = port + self.ip = ip + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client_key = "abcdefghjk" + self.socket.settimeout(10.0) + + def __enter__(self): + self.socket.connect((self.ip, self.port)) + self._handshake() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.socket.close() + + def _handshake(self): + MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + client_key = self.client_key + MAGIC_STRING + expected_accept = base64.standard_b64encode(hashlib.sha1(client_key.encode()).digest()) + request = 'GET /ws HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: ' \ + 'Upgrade\r\nSec-WebSocket-Key: {}\r\n' \ + 'Sec-WebSocket-Version: 13\r\n\r\n'.format(self.client_key) + self.socket.send(request.encode('utf-8')) + response = self.socket.recv(1024) + ws_accept = re.search(b'Sec-WebSocket-Accept: (.*)\r\n', response, re.IGNORECASE) + if ws_accept and ws_accept.group(1) is not None and ws_accept.group(1) == expected_accept: + pass + else: + raise("Unexpected Sec-WebSocket-Accept, handshake response: {}".format(response)) + + def _masked(self, data): + mask_key = os.urandom(4) + out = mask_key + for i in range(len(data)): + if six.PY3: + out += (data[i] ^ mask_key[i % 4]).to_bytes(1, byteorder="little") + else: + out += chr(ord(data[i]) ^ ord(mask_key[i % 4])) + return out + + def _ws_encode(self, data="", opcode=OPCODE_TEXT, mask=1): + data = data.encode('utf-8') + length = len(data) + if length >= 126: + raise("Packet length of {} not supported!".format(length)) + frame_header = chr(1 << 7 | opcode) + frame_header += chr(mask << 7 | length) + frame_header = six.b(frame_header) + if not mask: + return frame_header + data + return frame_header + self._masked(data) + + def read(self): + header = self.socket.recv(2) + if not six.PY3: + header = [ord(character) for character in header] + opcode = header[0] & 15 + length = header[1] & 127 + payload = self.socket.recv(length) + return opcode, payload.decode('utf-8') + + def write(self, data="", opcode=OPCODE_TEXT, mask=1): + return self.socket.sendall(self._ws_encode(data=data, opcode=opcode, mask=mask)) + + +@ttfw_idf.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_http_ws_echo_server(env, extra_data): + # Acquire DUT + dut1 = env.get_dut("http_server", "examples/protocols/http_server/ws_echo_server", dut_class=ttfw_idf.ESP32DUT) + + # Get binary file + binary_file = os.path.join(dut1.app.binary_path, "ws_echo_server.bin") + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance("http_ws_server_bin_size", "{}KB".format(bin_size // 1024)) + ttfw_idf.check_performance("http_ws_server_bin_size", bin_size // 1024) + + # Upload binary and start testing + Utility.console_log("Starting ws-echo-server test app based on http_server") + dut1.start_app() + + # Parse IP address of STA + Utility.console_log("Waiting to connect with AP") + got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)IPv4 address: (\d+.\d+.\d+.\d+)"), timeout=30)[0] + got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: '(\d+)'"), timeout=30)[0] + + Utility.console_log("Got IP : " + got_ip) + Utility.console_log("Got Port : " + got_port) + + # Start ws server test + with WsClient(got_ip, 80) as ws: + DATA = 'Espressif' + for expected_opcode in [OPCODE_TEXT, OPCODE_BIN, OPCODE_PING]: + Utility.console_log("Testing opcode {}".format(expected_opcode)) + ws.write(data=DATA, opcode=expected_opcode) + opcode, data = ws.read() + if expected_opcode == OPCODE_PING: + dut1.expect("Got a WS PING frame, Replying PONG") + if opcode != OPCODE_PONG or data != DATA: + raise RuntimeError("Failed to receive correct opcode:{} or data:{}".format(opcode, data)) + continue + dut_data = dut1.expect(re.compile(r"Got packet with message: ([A-Za-z0-9_]*)"))[0] + dut_opcode = int(dut1.expect(re.compile(r"Packet type: ([0-9]*)"))[0]) + if opcode != expected_opcode or data != DATA or opcode != dut_opcode or data != dut_data: + raise RuntimeError("Failed to receive correct opcode:{} or data:{}".format(opcode, data)) + + +if __name__ == '__main__': + test_examples_protocol_http_ws_echo_server()