mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
Merge branch 'feature/captive_portal' into 'master'
Captive Portal Example Closes IDFGH-5347, IDFGH-1868, and IDF-1123 See merge request espressif/esp-idf!14726
This commit is contained in:
commit
d5f58ab135
@ -0,0 +1,6 @@
|
||||
# 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)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(captive_portal)
|
8
examples/protocols/http_server/captive_portal/Makefile
Normal file
8
examples/protocols/http_server/captive_portal/Makefile
Normal file
@ -0,0 +1,8 @@
|
||||
#
|
||||
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
|
||||
# project subdirectory.
|
||||
#
|
||||
|
||||
PROJECT_NAME := captive_portal
|
||||
|
||||
include $(IDF_PATH)/make/project.mk
|
96
examples/protocols/http_server/captive_portal/README.md
Normal file
96
examples/protocols/http_server/captive_portal/README.md
Normal file
@ -0,0 +1,96 @@
|
||||
# Captive Portal Example
|
||||
|
||||
(See the README.md file in the upper level 'examples' directory for more information about examples.)
|
||||
|
||||
This example demonstrates a simple captive portal that will redirect all DNS IP questions to point to the softAP and redirect all HTTP requests to the captive portal root page. Triggers captive portal (sign in) pop up on Android, iOS and Windows. Note that the example will not redirect HTTPS requests.
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, be sure to set the correct chip target using `idf.py set-target <chip_name>`.
|
||||
|
||||
### Hardware Required
|
||||
|
||||
* A development board with ESP32/ESP32-S2/ESP32-C3 SoC (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.)
|
||||
* A USB cable for power supply and programming
|
||||
* WiFi interface
|
||||
|
||||
### Configure the project
|
||||
|
||||
Open the project configuration menu (`idf.py menuconfig`).
|
||||
|
||||
In the `Example Configuration` menu:
|
||||
|
||||
* Set the Wi-Fi configuration.
|
||||
* Set `SoftAP SSID`
|
||||
* Set `SoftAP Password`
|
||||
* Set `Maximal STA connections`
|
||||
|
||||
When using the legacy GNU Make build system, set `Default serial port` under `Serial flasher config`.
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Build the project and flash it to the board, then run monitor tool to view serial output:
|
||||
|
||||
```
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (733) example: Set up softAP with IP: 192.168.4.1
|
||||
I (743) example: wifi_init_softap finished. SSID:'esp32_ssid' password:'esp32_pwd'
|
||||
I (753) example: Starting server on port: '80'
|
||||
I (753) example: Registering URI handlers
|
||||
I (763) example_dns_redirect_server: Socket created
|
||||
I (763) example_dns_redirect_server: Socket bound, port 53
|
||||
I (773) example_dns_redirect_server: Waiting for data
|
||||
I (1873) wifi:new:<1,1>, old:<1,1>, ap:<1,1>, sta:<255,255>, prof:1
|
||||
I (1873) wifi:station: e8:84:a5:18:8f:80 join, AID=1, bgn, 40U
|
||||
I (2203) example: station e8:84:a5:18:8f:80 join, AID=1
|
||||
I (2833) example_dns_redirect_server: Received 50 bytes from 192.168.4.2 | DNS reply with len: 66
|
||||
I (2843) example_dns_redirect_server: Waiting for data
|
||||
I (3043) example_dns_redirect_server: Received 39 bytes from 192.168.4.2 | DNS reply with len: 55
|
||||
I (3043) example_dns_redirect_server: Waiting for data
|
||||
I (3043) example_dns_redirect_server: Received 42 bytes from 192.168.4.2 | DNS reply with len: 58
|
||||
I (3053) example_dns_redirect_server: Waiting for data
|
||||
W (3203) wifi:<ba-add>idx:4 (ifx:1, e8:84:a5:18:8f:80), tid:0, ssn:9, winSize:64
|
||||
I (3533) example: Redirecting to root
|
||||
I (5693) example_dns_redirect_server: Received 37 bytes from 192.168.4.2 | DNS reply with len: 53
|
||||
I (5693) example_dns_redirect_server: Waiting for data
|
||||
I (5783) example_dns_redirect_server: Received 46 bytes from 192.168.4.2 | DNS reply with len: 62
|
||||
I (5783) example_dns_redirect_server: Waiting for data
|
||||
I (6303) example_dns_redirect_server: Received 41 bytes from 192.168.4.2 | DNS reply with len: 57
|
||||
I (6303) example_dns_redirect_server: Waiting for data
|
||||
I (6303) example_dns_redirect_server: Received 41 bytes from 192.168.4.2 | DNS reply with len: 57
|
||||
I (6313) example_dns_redirect_server: Waiting for data
|
||||
I (6593) example: Redirecting to root
|
||||
I (9623) example: Redirecting to root
|
||||
I (12913) example: Redirecting to root
|
||||
I (13263) example_dns_redirect_server: Received 34 bytes from 192.168.4.2 | DNS reply with len: 50
|
||||
I (13273) example_dns_redirect_server: Waiting for data
|
||||
I (13273) example_dns_redirect_server: Received 34 bytes from 192.168.4.2 | DNS reply with len: 50
|
||||
I (13283) example_dns_redirect_server: Waiting for data
|
||||
I (16303) example_dns_redirect_server: Received 32 bytes from 192.168.4.2 | DNS reply with len: 48
|
||||
I (16303) example_dns_redirect_server: Waiting for data
|
||||
I (18073) example: Redirecting to root
|
||||
I (18273) example_dns_redirect_server: Received 34 bytes from 192.168.4.2 | DNS reply with len: 50
|
||||
I (18273) example_dns_redirect_server: Waiting for data
|
||||
I (18273) example_dns_redirect_server: Received 34 bytes from 192.168.4.2 | DNS reply with len: 50
|
||||
I (18283) example_dns_redirect_server: Waiting for data
|
||||
I (20683) example_dns_redirect_server: Received 42 bytes from 192.168.4.2 | DNS reply with len: 58
|
||||
I (20683) example_dns_redirect_server: Waiting for data
|
||||
I (20753) example: Redirecting to root
|
||||
I (21323) example: Redirecting to root
|
||||
I (22683) example_dns_redirect_server: Received 48 bytes from 192.168.4.2 | DNS reply with len: 64
|
||||
I (22693) example_dns_redirect_server: Waiting for data
|
||||
I (23443) example_dns_redirect_server: Received 48 bytes from 192.168.4.2 | DNS reply with len: 64
|
||||
I (23453) example_dns_redirect_server: Waiting for data
|
||||
I (23473) example: Serve root
|
||||
I (23503) example_dns_redirect_server: Received 48 bytes from 192.168.4.2 | DNS reply with len: 64
|
||||
I (23513) example_dns_redirect_server: Waiting for data
|
||||
```
|
142
examples/protocols/http_server/captive_portal/example_test.py
Normal file
142
examples/protocols/http_server/captive_portal/example_test.py
Normal file
@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2021 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 print_function
|
||||
|
||||
import http.client
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import tiny_test_fw
|
||||
import ttfw_idf
|
||||
from tiny_test_fw import Utility
|
||||
|
||||
try:
|
||||
import wifi_tools
|
||||
except ImportError:
|
||||
wifi_tools_path = str(os.getenv('IDF_PATH')) + '/examples/provisioning/softap_prov/utils'
|
||||
if wifi_tools_path and wifi_tools_path not in sys.path:
|
||||
sys.path.insert(0, wifi_tools_path)
|
||||
import wifi_tools
|
||||
|
||||
|
||||
def test_redirect(ip, port): # type: (str, str) -> str # pylint: disable=unused-argument
|
||||
# Establish HTTP connection
|
||||
sess = http.client.HTTPConnection(ip + ':' + port, timeout=15)
|
||||
|
||||
uri = '/test'
|
||||
# GET response
|
||||
sess.request('GET', url=uri)
|
||||
resp = sess.getresponse()
|
||||
resp_hdrs = resp.getheaders()
|
||||
|
||||
if resp.status != 302:
|
||||
raise RuntimeError('Redirect failed, response status: {}'.format(resp.status))
|
||||
|
||||
for hdr in resp_hdrs:
|
||||
if hdr[0] == 'location':
|
||||
uri = hdr[1]
|
||||
|
||||
print('Redirected to uri: {}'.format(uri))
|
||||
|
||||
# Close HTTP connection
|
||||
sess.close()
|
||||
|
||||
return uri
|
||||
|
||||
|
||||
def test_captive_page(ip, port, uri): # type: (str, str, str) -> bool # pylint: disable=unused-argument
|
||||
# Establish HTTP connection
|
||||
sess = http.client.HTTPConnection(ip + ':' + port, timeout=15)
|
||||
|
||||
# GET response
|
||||
sess.request('GET', url=uri)
|
||||
resp = sess.getresponse()
|
||||
|
||||
resp_data = resp.read().decode()
|
||||
search_str = 'Redirect to the captive portal'
|
||||
|
||||
if search_str not in resp_data:
|
||||
raise RuntimeError('Failed to match {} with data from captive portal: {}'.format(search_str, resp_data))
|
||||
|
||||
# Close HTTP connection
|
||||
sess.close()
|
||||
return True
|
||||
|
||||
|
||||
@ttfw_idf.idf_example_test(env_tag='Example_WIFI_BT', ignore=True)
|
||||
def test_example_captive_portal(env, extra_data): # type: (tiny_test_fw.Env.Env, None) -> None # pylint: disable=unused-argument
|
||||
# Acquire DUT
|
||||
dut1 = env.get_dut('captive_portal', 'examples/protocols/http_server/captive_portal', dut_class=ttfw_idf.ESP32DUT)
|
||||
|
||||
# Get binary file
|
||||
binary_file = os.path.join(dut1.app.binary_path, 'captive_portal.bin')
|
||||
bin_size = os.path.getsize(binary_file)
|
||||
ttfw_idf.log_performance('captive_portal_bin_size', '{}KB'.format(bin_size // 1024))
|
||||
|
||||
# Upload binary and start testing
|
||||
dut1.start_app()
|
||||
|
||||
# Parse IP address of STA
|
||||
Utility.console_log('Waiting to connect with softAP')
|
||||
ap_ip = dut1.expect(re.compile(r'Set up softAP with IP: (\d+.\d+.\d+.\d+)'), timeout=60)[0]
|
||||
|
||||
[ssid, password] = dut1.expect(re.compile(r"wifi_init_softap finished. SSID:'(\S+)' password:'(\S+)'"), timeout=30)
|
||||
port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: '(\d+)'"), timeout=30)[0]
|
||||
|
||||
iface = wifi_tools.get_wiface_name()
|
||||
if iface is None:
|
||||
raise RuntimeError('Failed to get Wi-Fi interface on host')
|
||||
print('Interface name : ' + iface)
|
||||
print('SoftAP SSID : ' + ssid)
|
||||
print('SoftAP Password : ' + password)
|
||||
|
||||
try:
|
||||
ctrl = wifi_tools.wpa_cli(iface, reset_on_exit=True)
|
||||
print('Connecting to DUT SoftAP...')
|
||||
try:
|
||||
ip = ctrl.connect(ssid, password)
|
||||
except RuntimeError as err:
|
||||
Utility.console_log('error: {}'.format(err))
|
||||
try:
|
||||
got_ip = dut1.expect(re.compile(r'DHCP server assigned IP to a station, IP is: (\d+.\d+.\d+.\d+)'), timeout=60)
|
||||
Utility.console_log('got_ip: {}'.format(got_ip))
|
||||
got_ip = got_ip[0]
|
||||
if ip != got_ip:
|
||||
raise RuntimeError('SoftAP connected to another host! {} != {}'.format(ip, got_ip))
|
||||
except tiny_test_fw.DUT.ExpectTimeout:
|
||||
# print what is happening on DUT side
|
||||
Utility.console_log('in exception tiny_test_fw.DUT.ExpectTimeout')
|
||||
Utility.console_log(dut1.read())
|
||||
raise
|
||||
print('Connected to DUT SoftAP')
|
||||
|
||||
host_name = 'www.google.com'
|
||||
host = socket.gethostbyname(host_name)
|
||||
print('hostname: {} resolved to: {}'.format(host_name, host))
|
||||
if host != ap_ip:
|
||||
raise RuntimeError("DNS server failed to redirect question to the softAP's IP")
|
||||
|
||||
uri = test_redirect(ap_ip, port)
|
||||
test_captive_page(ap_ip, port, uri)
|
||||
finally:
|
||||
ctrl.reset()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_example_captive_portal() # pylint: disable=no-value-for-parameter
|
@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "main.c" "dns_server.c"
|
||||
INCLUDE_DIRS "include"
|
||||
EMBED_FILES root.html)
|
@ -0,0 +1,20 @@
|
||||
menu "Example Configuration"
|
||||
|
||||
config ESP_WIFI_SSID
|
||||
string "SoftAP SSID"
|
||||
default "esp32_ssid"
|
||||
help
|
||||
SSID (network name) to set up the softAP with.
|
||||
|
||||
config ESP_WIFI_PASSWORD
|
||||
string "SoftAP Password"
|
||||
default "esp32_pwd"
|
||||
help
|
||||
WiFi password (WPA or WPA2) for the example to use for the softAP.
|
||||
|
||||
config ESP_MAX_STA_CONN
|
||||
int "Maximal STA connections"
|
||||
default 4
|
||||
help
|
||||
Max number of the STA connects to AP.
|
||||
endmenu
|
@ -0,0 +1,6 @@
|
||||
#
|
||||
# "main" pseudo-component makefile.
|
||||
#
|
||||
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
|
||||
|
||||
COMPONENT_EMBED_FILES := root.html
|
247
examples/protocols/http_server/captive_portal/main/dns_server.c
Normal file
247
examples/protocols/http_server/captive_portal/main/dns_server.c
Normal file
@ -0,0 +1,247 @@
|
||||
/* Captive Portal 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 <sys/param.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_netif.h"
|
||||
|
||||
#include "lwip/err.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/sys.h"
|
||||
#include "lwip/netdb.h"
|
||||
|
||||
#define DNS_PORT (53)
|
||||
#define DNS_MAX_LEN (256)
|
||||
|
||||
#define OPCODE_MASK (0x7800)
|
||||
#define QR_FLAG (1 << 7)
|
||||
#define QD_TYPE_A (0x0001)
|
||||
#define ANS_TTL_SEC (300)
|
||||
|
||||
static const char *TAG = "example_dns_redirect_server";
|
||||
|
||||
// DNS Header Packet
|
||||
typedef struct __attribute__((__packed__))
|
||||
{
|
||||
uint16_t id;
|
||||
uint16_t flags;
|
||||
uint16_t qd_count;
|
||||
uint16_t an_count;
|
||||
uint16_t ns_count;
|
||||
uint16_t ar_count;
|
||||
} dns_header_t;
|
||||
|
||||
// DNS Question Packet
|
||||
typedef struct {
|
||||
uint16_t type;
|
||||
uint16_t class;
|
||||
} dns_question_t;
|
||||
|
||||
// DNS Answer Packet
|
||||
typedef struct __attribute__((__packed__))
|
||||
{
|
||||
uint16_t ptr_offset;
|
||||
uint16_t type;
|
||||
uint16_t class;
|
||||
uint32_t ttl;
|
||||
uint16_t addr_len;
|
||||
uint32_t ip_addr;
|
||||
} dns_answer_t;
|
||||
|
||||
/*
|
||||
Parse the name from the packet from the DNS name format to a regular .-seperated name
|
||||
returns the pointer to the next part of the packet
|
||||
*/
|
||||
static char *parse_dns_name(char *raw_name, char *parsed_name, size_t parsed_name_max_len)
|
||||
{
|
||||
|
||||
char *label = raw_name;
|
||||
char *name_itr = parsed_name;
|
||||
int name_len = 0;
|
||||
|
||||
do {
|
||||
int sub_name_len = *label;
|
||||
// (len + 1) since we are adding a '.'
|
||||
name_len += (sub_name_len + 1);
|
||||
if (name_len > parsed_name_max_len) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Copy the sub name that follows the the label
|
||||
memcpy(name_itr, label + 1, sub_name_len);
|
||||
name_itr[sub_name_len] = '.';
|
||||
name_itr += (sub_name_len + 1);
|
||||
label += sub_name_len + 1;
|
||||
} while (*label != 0);
|
||||
|
||||
// Terminate the final string, replacing the last '.'
|
||||
parsed_name[name_len - 1] = '\0';
|
||||
// Return pointer to first char after the name
|
||||
return label + 1;
|
||||
}
|
||||
|
||||
// Parses the DNS request and prepares a DNS response with the IP of the softAP
|
||||
static int parse_dns_request(char *req, size_t req_len, char *dns_reply, size_t dns_reply_max_len)
|
||||
{
|
||||
if (req_len > dns_reply_max_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Prepare the reply
|
||||
memset(dns_reply, 0, dns_reply_max_len);
|
||||
memcpy(dns_reply, req, req_len);
|
||||
|
||||
// Endianess of NW packet different from chip
|
||||
dns_header_t *header = (dns_header_t *)dns_reply;
|
||||
ESP_LOGD(TAG, "DNS query with header id: 0x%X, flags: 0x%X, qd_count: %d",
|
||||
ntohs(header->id), ntohs(header->flags), ntohs(header->qd_count));
|
||||
|
||||
// Not a standard query
|
||||
if ((header->flags & OPCODE_MASK) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Set question response flag
|
||||
header->flags |= QR_FLAG;
|
||||
|
||||
uint16_t qd_count = ntohs(header->qd_count);
|
||||
header->an_count = htons(qd_count);
|
||||
|
||||
int reply_len = qd_count * sizeof(dns_answer_t) + req_len;
|
||||
if (reply_len > dns_reply_max_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Pointer to current answer and question
|
||||
char *cur_ans_ptr = dns_reply + req_len;
|
||||
char *cur_qd_ptr = dns_reply + sizeof(dns_header_t);
|
||||
char name[128];
|
||||
|
||||
// Respond to all questions with the ESP32's IP address
|
||||
for (int i = 0; i < qd_count; i++) {
|
||||
char *name_end_ptr = parse_dns_name(cur_qd_ptr, name, sizeof(name));
|
||||
if (name_end_ptr == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to parse DNS question: %s", cur_qd_ptr);
|
||||
return -1;
|
||||
}
|
||||
|
||||
dns_question_t *question = (dns_question_t *)(name_end_ptr);
|
||||
uint16_t qd_type = ntohs(question->type);
|
||||
uint16_t qd_class = ntohs(question->class);
|
||||
|
||||
ESP_LOGD(TAG, "Received type: %d | Class: %d | Question for: %s", qd_type, qd_class, name);
|
||||
|
||||
if (qd_type == QD_TYPE_A) {
|
||||
dns_answer_t *answer = (dns_answer_t *)cur_ans_ptr;
|
||||
|
||||
answer->ptr_offset = htons(0xC000 | (cur_qd_ptr - dns_reply));
|
||||
answer->type = htons(qd_type);
|
||||
answer->class = htons(qd_class);
|
||||
answer->ttl = htonl(ANS_TTL_SEC);
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info);
|
||||
ESP_LOGD(TAG, "Answer with PTR offset: 0x%X and IP 0x%X", ntohs(answer->ptr_offset), ip_info.ip.addr);
|
||||
|
||||
answer->addr_len = htons(sizeof(ip_info.ip.addr));
|
||||
answer->ip_addr = ip_info.ip.addr;
|
||||
}
|
||||
}
|
||||
return reply_len;
|
||||
}
|
||||
|
||||
/*
|
||||
Sets up a socket and listen for DNS queries,
|
||||
replies to all type A queries with the IP of the softAP
|
||||
*/
|
||||
void dns_server_task(void *pvParameters)
|
||||
{
|
||||
char rx_buffer[128];
|
||||
char addr_str[128];
|
||||
int addr_family;
|
||||
int ip_protocol;
|
||||
|
||||
while (1) {
|
||||
|
||||
struct sockaddr_in dest_addr;
|
||||
dest_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
dest_addr.sin_family = AF_INET;
|
||||
dest_addr.sin_port = htons(DNS_PORT);
|
||||
addr_family = AF_INET;
|
||||
ip_protocol = IPPROTO_IP;
|
||||
inet_ntoa_r(dest_addr.sin_addr, addr_str, sizeof(addr_str) - 1);
|
||||
|
||||
int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
|
||||
if (sock < 0) {
|
||||
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
|
||||
break;
|
||||
}
|
||||
ESP_LOGI(TAG, "Socket created");
|
||||
|
||||
int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
|
||||
}
|
||||
ESP_LOGI(TAG, "Socket bound, port %d", DNS_PORT);
|
||||
|
||||
while (1) {
|
||||
ESP_LOGI(TAG, "Waiting for data");
|
||||
struct sockaddr_in6 source_addr; // Large enough for both IPv4 or IPv6
|
||||
socklen_t socklen = sizeof(source_addr);
|
||||
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);
|
||||
|
||||
// Error occurred during receiving
|
||||
if (len < 0) {
|
||||
ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
|
||||
close(sock);
|
||||
break;
|
||||
}
|
||||
// Data received
|
||||
else {
|
||||
// Get the sender's ip address as string
|
||||
if (source_addr.sin6_family == PF_INET) {
|
||||
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
|
||||
} else if (source_addr.sin6_family == PF_INET6) {
|
||||
inet6_ntoa_r(source_addr.sin6_addr, addr_str, sizeof(addr_str) - 1);
|
||||
}
|
||||
|
||||
// Null-terminate whatever we received and treat like a string...
|
||||
rx_buffer[len] = 0;
|
||||
|
||||
char reply[DNS_MAX_LEN];
|
||||
int reply_len = parse_dns_request(rx_buffer, len, reply, DNS_MAX_LEN);
|
||||
|
||||
ESP_LOGI(TAG, "Received %d bytes from %s | DNS reply with len: %d", len, addr_str, reply_len);
|
||||
if (reply_len <= 0) {
|
||||
ESP_LOGE(TAG, "Failed to prepare a DNS reply");
|
||||
} else {
|
||||
int err = sendto(sock, reply, reply_len, 0, (struct sockaddr *)&source_addr, sizeof(source_addr));
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sock != -1) {
|
||||
ESP_LOGE(TAG, "Shutting down socket");
|
||||
shutdown(sock, 0);
|
||||
close(sock);
|
||||
}
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void start_dns_server(void)
|
||||
{
|
||||
xTaskCreate(dns_server_task, "dns_server", 4096, NULL, 5, NULL);
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/* Captive Portal 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Set ups and starts a simple DNS server that will respond to all queries
|
||||
* with the soft AP's IP address
|
||||
*
|
||||
*/
|
||||
void start_dns_server(void);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
163
examples/protocols/http_server/captive_portal/main/main.c
Normal file
163
examples/protocols/http_server/captive_portal/main/main.c
Normal file
@ -0,0 +1,163 @@
|
||||
/* Captive Portal 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 <sys/param.h>
|
||||
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_netif.h"
|
||||
#include "lwip/inet.h"
|
||||
|
||||
#include "esp_http_server.h"
|
||||
#include "dns_server.h"
|
||||
|
||||
#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
|
||||
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
|
||||
#define EXAMPLE_MAX_STA_CONN CONFIG_ESP_MAX_STA_CONN
|
||||
|
||||
extern const char root_start[] asm("_binary_root_html_start");
|
||||
extern const char root_end[] asm("_binary_root_html_end");
|
||||
|
||||
static const char *TAG = "example";
|
||||
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||
wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *)event_data;
|
||||
ESP_LOGI(TAG, "station " MACSTR " join, AID=%d",
|
||||
MAC2STR(event->mac), event->aid);
|
||||
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data;
|
||||
ESP_LOGI(TAG, "station " MACSTR " leave, AID=%d",
|
||||
MAC2STR(event->mac), event->aid);
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_init_softap(void)
|
||||
{
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.ap = {
|
||||
.ssid = EXAMPLE_ESP_WIFI_SSID,
|
||||
.ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
|
||||
.password = EXAMPLE_ESP_WIFI_PASS,
|
||||
.max_connection = EXAMPLE_MAX_STA_CONN,
|
||||
.authmode = WIFI_AUTH_WPA_WPA2_PSK
|
||||
},
|
||||
};
|
||||
if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
|
||||
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info);
|
||||
|
||||
char ip_addr[16];
|
||||
inet_ntoa_r(ip_info.ip.addr, ip_addr, 16);
|
||||
ESP_LOGI(TAG, "Set up softAP with IP: %s", ip_addr);
|
||||
|
||||
ESP_LOGI(TAG, "wifi_init_softap finished. SSID:'%s' password:'%s'",
|
||||
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
|
||||
}
|
||||
|
||||
// HTTP GET Handler
|
||||
static esp_err_t root_get_handler(httpd_req_t *req)
|
||||
{
|
||||
const uint32_t root_len = root_end - root_start;
|
||||
|
||||
ESP_LOGI(TAG, "Serve root");
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_send(req, root_start, root_len);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const httpd_uri_t root = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = root_get_handler
|
||||
};
|
||||
|
||||
// HTTP Error (404) Handler - Redirects all requests to the root page
|
||||
esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
|
||||
{
|
||||
// Set status
|
||||
httpd_resp_set_status(req, "302 Temporary Redirect");
|
||||
// Redirect to the "/" root directory
|
||||
httpd_resp_set_hdr(req, "Location", "/");
|
||||
// iOS requires content in the response to detect a captive portal, simply redirecting is not sufficient.
|
||||
httpd_resp_send(req, "Redirect to the captive portal", HTTPD_RESP_USE_STRLEN);
|
||||
|
||||
ESP_LOGI(TAG, "Redirecting to root");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static httpd_handle_t start_webserver(void)
|
||||
{
|
||||
httpd_handle_t server = NULL;
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.max_open_sockets = 13;
|
||||
config.lru_purge_enable = true;
|
||||
|
||||
// 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, &root);
|
||||
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/*
|
||||
Turn of warnings from HTTP server as redirecting traffic will yield
|
||||
lots of invalid requests
|
||||
*/
|
||||
esp_log_level_set("httpd_uri", ESP_LOG_ERROR);
|
||||
esp_log_level_set("httpd_txrx", ESP_LOG_ERROR);
|
||||
esp_log_level_set("httpd_parse", ESP_LOG_ERROR);
|
||||
|
||||
|
||||
// Initialize networking stack
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
// Create default event loop needed by the main app
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
// Initialize NVS needed by Wi-Fi
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
|
||||
// Initialize Wi-Fi including netif with default config
|
||||
esp_netif_create_default_wifi_ap();
|
||||
|
||||
// Initialise ESP32 in SoftAP mode
|
||||
wifi_init_softap();
|
||||
|
||||
// Start the server for the first time
|
||||
start_webserver();
|
||||
|
||||
// Start the DNS server that will redirect all queries to the softAP IP
|
||||
start_dns_server();
|
||||
}
|
15
examples/protocols/http_server/captive_portal/main/root.html
Normal file
15
examples/protocols/http_server/captive_portal/main/root.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
<title>ESP Captive Portal</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ESP Captive Portal</h1>
|
||||
<p>Hello World, this is ESP32!</p>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,2 @@
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
|
||||
CONFIG_LWIP_MAX_SOCKETS=16
|
Loading…
Reference in New Issue
Block a user