diff --git a/components/dns_server/CMakeLists.txt b/components/dns_server/CMakeLists.txt new file mode 100644 index 0000000..4230585 --- /dev/null +++ b/components/dns_server/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS dns_server.c + INCLUDE_DIRS include + PRIV_REQUIRES esp_netif) diff --git a/components/dns_server/dns_server.c b/components/dns_server/dns_server.c new file mode 100644 index 0000000..0fd5c4e --- /dev/null +++ b/components/dns_server/dns_server.c @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include + +#include "esp_check.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_system.h" + +#include "dns_server.h" +#include "lwip/err.h" +#include "lwip/netdb.h" +#include "lwip/sockets.h" +#include "lwip/sys.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; + +// DNS server handle +struct dns_server_handle +{ + bool started; + TaskHandle_t task; + int num_of_entries; + dns_entry_pair_t entry[]; +}; + +/* + 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, dns_server_handle_t h) +{ + ESP_LOGD(TAG, "TEST"); + 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 based on configured rules + for (int qd_i = 0; qd_i < qd_count; qd_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) { + esp_ip4_addr_t ip = {.addr = IPADDR_ANY}; + // Check the configured rules to decide whether to answer this question or not + for (int i = 0; i < h->num_of_entries; ++i) { + // check if the name either corresponds to the entry, or if we should answer to all queries ("*") + if (strcmp(h->entry[i].name, "*") == 0 || strcmp(h->entry[i].name, name) == 0) { + if (h->entry[i].if_key) { + + esp_netif_ip_info_t ip_info; + esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey(h->entry[i].if_key), &ip_info); + ip.addr = ip_info.ip.addr; + ESP_LOGD(TAG, "TEST %s", h->entry[i].if_key); + break; + } else if (h->entry->ip.addr != IPADDR_ANY) { + ip.addr = h->entry[i].ip.addr; + break; + } + } + } + uint8_t octet1 = (ip.addr >> 24) & 0xFF; + uint8_t octet2 = (ip.addr >> 16) & 0xFF; + uint8_t octet3 = (ip.addr >> 8) & 0xFF; + uint8_t octet4 = ip.addr & 0xFF; + + printf("IP address: %u.%u.%u.%u\n", octet1, octet2, octet3, octet4); + + if (ip.addr == IPADDR_ANY) { // no rule applies, continue with another question + continue; + } + 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_LOGD(TAG, "Answer with PTR offset: 0x%" PRIX16 " and IP 0x%" PRIX32, ntohs(answer->ptr_offset), ip.addr); + + answer->addr_len = htons(sizeof(ip.addr)); + answer->ip_addr = 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; + dns_server_handle_t handle = pvParameters; + + while (handle->started) { + + 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 (handle->started) { + 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, handle); + + 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); +} + +dns_server_handle_t start_dns_server(dns_server_config_t * config) +{ + dns_server_handle_t handle = calloc(1, sizeof(struct dns_server_handle) + config->num_of_entries * sizeof(dns_entry_pair_t)); + ESP_RETURN_ON_FALSE(handle, NULL, TAG, "Failed to allocate dns server handle"); + + handle->started = true; + handle->num_of_entries = config->num_of_entries; + memcpy(handle->entry, config->item, config->num_of_entries * sizeof(dns_entry_pair_t)); + + xTaskCreate(dns_server_task, "dns_server", 4096, handle, 5, &handle->task); + return handle; +} + +void stop_dns_server(dns_server_handle_t handle) +{ + if (handle) { + handle->started = false; + vTaskDelete(handle->task); + free(handle); + } +} diff --git a/components/dns_server/include/dns_server.h b/components/dns_server/include/dns_server.h new file mode 100644 index 0000000..6349b69 --- /dev/null +++ b/components/dns_server/include/dns_server.h @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifndef DNS_SERVER_MAX_ITEMS +#define DNS_SERVER_MAX_ITEMS 1 +#endif + +#include "esp_netif.h" +#include "esp_system.h" + +#define DNS_SERVER_CONFIG_SINGLE(queried_name, netif_key) \ + { \ + .num_of_entries = 1, .item = { {.name = queried_name, .if_key = netif_key} } \ + } + + /** + * @brief Definition of one DNS entry: NAME - IP (or the netif whose IP to answer) + * + * @note Please use string literals (or ensure they are valid during dns_server lifetime) as names, since + * we don't take copies of the config values `name` and `if_key` + */ + typedef struct dns_entry_pair + { + const char * name; /**AxeOS \ No newline at end of file +
AxeOS
\ No newline at end of file diff --git a/main/http_server/axe-os/src/app/components/header/header.component.scss b/main/http_server/axe-os/src/app/components/header/header.component.scss index 5784e5e..bc728bd 100644 --- a/main/http_server/axe-os/src/app/components/header/header.component.scss +++ b/main/http_server/axe-os/src/app/components/header/header.component.scss @@ -14,4 +14,9 @@ padding: 6px; margin-left: 10px; cursor: pointer; + font-weight: bold; +} + +.os { + color: #64B5F6; } \ No newline at end of file diff --git a/main/http_server/axe-os/src/app/components/home/home.component.ts b/main/http_server/axe-os/src/app/components/home/home.component.ts index 886b0ce..564e576 100644 --- a/main/http_server/axe-os/src/app/components/home/home.component.ts +++ b/main/http_server/axe-os/src/app/components/home/home.component.ts @@ -1,6 +1,6 @@ -import { AfterViewChecked, Component, ElementRef, ViewChild } from '@angular/core'; +import { AfterViewChecked, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'; import { ToastrService } from 'ngx-toastr'; -import { interval, Observable, shareReplay, startWith, switchMap } from 'rxjs'; +import { interval, Observable, shareReplay, startWith, Subscription, switchMap } from 'rxjs'; import { LoadingService } from 'src/app/services/loading.service'; import { SystemService } from 'src/app/services/system.service'; import { WebsocketService } from 'src/app/services/web-socket.service'; @@ -10,13 +10,16 @@ import { WebsocketService } from 'src/app/services/web-socket.service'; templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) -export class HomeComponent implements AfterViewChecked { +export class HomeComponent implements AfterViewChecked, OnDestroy { @ViewChild('scrollContainer') private scrollContainer!: ElementRef; public info$: Observable; public logs: string[] = []; + private websocketSubscription: Subscription; + + constructor( private systemService: SystemService, private toastr: ToastrService, @@ -24,7 +27,7 @@ export class HomeComponent implements AfterViewChecked { private websocketService: WebsocketService ) { - this.info$ = interval(3000).pipe( + this.info$ = interval(5000).pipe( startWith(() => this.systemService.getInfo()), switchMap(() => { return this.systemService.getInfo() @@ -32,7 +35,7 @@ export class HomeComponent implements AfterViewChecked { shareReplay({ refCount: true, bufferSize: 1 }) ) - this.websocketService.ws$.subscribe({ + this.websocketSubscription = this.websocketService.ws$.subscribe({ next: (val) => { this.logs.push(val); if (this.logs.length > 100) { @@ -43,6 +46,9 @@ export class HomeComponent implements AfterViewChecked { }) } + ngOnDestroy(): void { + this.websocketSubscription.unsubscribe(); + } ngAfterViewChecked(): void { if (this.scrollContainer?.nativeElement != null) { this.scrollContainer.nativeElement.scrollTo({ left: 0, top: this.scrollContainer.nativeElement.scrollHeight, behavior: 'smooth' }); diff --git a/main/http_server/axe-os/src/index.html b/main/http_server/axe-os/src/index.html index 5c5d6bf..300ded3 100644 --- a/main/http_server/axe-os/src/index.html +++ b/main/http_server/axe-os/src/index.html @@ -1,13 +1,16 @@ + - AxeOs + AxeOS + - + + \ No newline at end of file diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c index 0e9819a..463ab12 100644 --- a/main/http_server/http_server.c +++ b/main/http_server/http_server.c @@ -17,6 +17,7 @@ #include #include +#include "dns_server.h" #include "esp_mac.h" #include "esp_netif.h" #include "esp_ota_ops.h" @@ -120,17 +121,25 @@ static esp_err_t rest_common_get_handler(httpd_req_t * req) if (req->uri[strlen(req->uri) - 1] == '/') { strlcat(filepath, "/index.html", filePathLength); } else { - httpd_resp_set_hdr(req, "Cache-Control", "max-age=2592000"); strlcat(filepath, req->uri, filePathLength); } set_content_type_from_file(req, filepath); strcat(filepath, ".gz"); int fd = open(filepath, O_RDONLY, 0); if (fd == -1) { - ESP_LOGE(TAG, "Failed to open file : %s", filepath); - /* Respond with 500 Internal Server Error */ - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file"); - return ESP_FAIL; + // 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; + } + + if (req->uri[strlen(req->uri) - 1] != '/') { + httpd_resp_set_hdr(req, "Cache-Control", "max-age=2592000"); } httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); @@ -386,6 +395,21 @@ esp_err_t echo_handler(httpd_req_t * req) return ESP_OK; } + +// 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; +} + esp_err_t start_rest_server(void * pvParameters) { GLOBAL_STATE = (GlobalState *) pvParameters; @@ -432,6 +456,13 @@ esp_err_t start_rest_server(void * pvParameters) httpd_uri_t common_get_uri = {.uri = "/*", .method = HTTP_GET, .handler = rest_common_get_handler, .user_ctx = rest_context}; httpd_register_uri_handler(server, &common_get_uri); + httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler); + + // Start the DNS server that will redirect all queries to the softAP IP + + dns_server_config_t dns_config = DNS_SERVER_CONFIG_SINGLE("*" /* all A queries */, "WIFI_AP_DEF" /* softAP netif ID */); + start_dns_server(&dns_config); + return ESP_OK; err_start: free(rest_context); diff --git a/sdkconfig.defaults b/sdkconfig.defaults index ea90101..012202d 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -7,4 +7,5 @@ CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y CONFIG_ESPTOOLPY_FLASHSIZE="8MB" CONFIG_ESP_MAXIMUM_RETRY=5 CONFIG_HTTPD_WS_SUPPORT=y -CONFIG_SPIFFS_OBJ_NAME_LEN=64 \ No newline at end of file +CONFIG_SPIFFS_OBJ_NAME_LEN=64 +CONFIG_HTTPD_MAX_URI_LEN=2048 \ No newline at end of file