├── .gitignore ├── LICENSE ├── PicoHTTPServer ├── CMakeLists.txt ├── FreeRTOSConfig.h ├── PicoHTTPServer.sln ├── PicoHTTPServer.vgdbproj ├── PicoSDKConfig.cmake ├── debug_printf.h ├── dhcpserver │ ├── LICENSE │ ├── dhcpserver.c │ └── dhcpserver.h ├── dns │ ├── dnsserver.c │ └── dnsserver.h ├── httpserver.c ├── httpserver.h ├── lwipopts.h ├── lwipopts_examples_common.h ├── main.c ├── server_settings.c ├── server_settings.h └── www │ ├── img │ └── configure.png │ ├── index.html │ └── pinout.svg ├── README.md ├── build-all.sh ├── lwip_patch └── lwip.patch ├── screenshots ├── 01-pins.png ├── 02-login.png └── 03-settings.png └── tools └── SimpleFSBuilder ├── CMakeLists.txt ├── SimpleFS.h ├── SimpleFSBuilder.cpp ├── SimpleFSBuilder.exe └── SimpleFSBuilder.vgdbcmake /.gitignore: -------------------------------------------------------------------------------- 1 | .visualgdb 2 | .vs 3 | build 4 | *.user 5 | CMakeLists.txt.old 6 | pico-sdk 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sysprogs Software Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PicoHTTPServer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | include(${PICO_SDK_PATH}/pico_sdk_init.cmake) 3 | 4 | project(PicoHTTPServer C CXX ASM) 5 | include(PicoSDKConfig.cmake) 6 | add_subdirectory("${PICO_SDK_PATH}/FreeRTOS/portable/ThirdParty/GCC/RP2040" FREERTOS_KERNEL) 7 | pico_sdk_init() 8 | 9 | if(DEFINED SYSPROGS_FRAMEWORKS_FILE) 10 | include(${SYSPROGS_FRAMEWORKS_FILE}) 11 | endif() 12 | 13 | set(CMAKE_C_STANDARD 11) 14 | set(CMAKE_CXX_STANDARD 17) 15 | 16 | if (CMAKE_HOST_WIN32) 17 | set(SIMPLE_FS_BUILDER_EXE ${CMAKE_CURRENT_SOURCE_DIR}/../tools/SimpleFSBuilder/SimpleFSBuilder.exe) 18 | else() 19 | set(SIMPLE_FS_BUILDER_EXE ${CMAKE_CURRENT_SOURCE_DIR}/../tools/SimpleFSBuilder/build/SimpleFSBuilder) 20 | endif() 21 | 22 | if (NOT EXISTS ${SIMPLE_FS_BUILDER_EXE}) 23 | message(FATAL_ERROR "Missing ${SIMPLE_FS_BUILDER_EXE}. Please build it before building this project.") 24 | endif() 25 | 26 | function(add_resource_folder target name path) 27 | add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${name}.fs ${CMAKE_CURRENT_BINARY_DIR}/__rerun_${name}.fs 28 | COMMAND ${SIMPLE_FS_BUILDER_EXE} 29 | ARGS ${path} ${CMAKE_CURRENT_BINARY_DIR}/${name}.fs 30 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 31 | COMMENT "Generating ${name}.fs") 32 | 33 | add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${name}.o0 34 | COMMAND ${CMAKE_LINKER} 35 | ARGS -r -b binary ${name}.fs -o ${CMAKE_CURRENT_BINARY_DIR}/${name}.o0 36 | DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${name}.fs ${CMAKE_CURRENT_BINARY_DIR}/__rerun_${name}.fs 37 | COMMENT "Wrapping ${name}.fs") 38 | 39 | add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${name}.o 40 | COMMAND ${CMAKE_OBJCOPY} 41 | ARGS --rename-section .data=.rodata 42 | ${CMAKE_CURRENT_BINARY_DIR}/${name}.o0 ${CMAKE_CURRENT_BINARY_DIR}/${name}.o 43 | DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${name}.o0 44 | COMMENT "Renaming ${name}") 45 | 46 | add_custom_target(${name}-fs DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/${name}.o) 47 | add_dependencies(${target} ${name}-fs) 48 | target_link_libraries(${target} ${CMAKE_CURRENT_BINARY_DIR}/${name}.o) 49 | 50 | endfunction() 51 | 52 | add_executable(PicoHTTPServer 53 | main.c 54 | dhcpserver/dhcpserver.c 55 | dns/dnsserver.c 56 | httpserver.c 57 | server_settings.c) 58 | 59 | add_resource_folder(PicoHTTPServer www www) 60 | 61 | target_compile_definitions(PicoHTTPServer PRIVATE 62 | WIFI_SSID=\"${WIFI_SSID}\" 63 | WIFI_PASSWORD=\"${WIFI_PASSWORD}\" 64 | NO_SYS=0) 65 | target_include_directories(PicoHTTPServer PRIVATE 66 | ${CMAKE_CURRENT_LIST_DIR} 67 | ${CMAKE_CURRENT_LIST_DIR}/../..) 68 | 69 | target_link_libraries(PicoHTTPServer 70 | pico_cyw43_arch_lwip_sys_freertos 71 | pico_stdlib 72 | pico_lwip_iperf 73 | FreeRTOS-Kernel-Heap4) 74 | 75 | if (TARGET Profiler) 76 | target_link_libraries(PicoHTTPServer 77 | Profiler) 78 | endif() 79 | 80 | pico_add_extra_outputs(PicoHTTPServer) 81 | -------------------------------------------------------------------------------- /PicoHTTPServer/FreeRTOSConfig.h: -------------------------------------------------------------------------------- 1 | /* 2 | * FreeRTOS V202111.00 3 | * Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | * this software and associated documentation files (the "Software"), to deal in 7 | * the Software without restriction, including without limitation the rights to 8 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | * the Software, and to permit persons to whom the Software is furnished to do so, 10 | * subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | * 22 | * http://www.FreeRTOS.org 23 | * http://aws.amazon.com/freertos 24 | * 25 | * 1 tab == 4 spaces! 26 | */ 27 | 28 | #ifndef FREERTOS_CONFIG_H 29 | #define FREERTOS_CONFIG_H 30 | 31 | /*----------------------------------------------------------- 32 | * Application specific definitions. 33 | * 34 | * These definitions should be adjusted for your particular hardware and 35 | * application requirements. 36 | * 37 | * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE 38 | * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE. 39 | * 40 | * See http://www.freertos.org/a00110.html 41 | *----------------------------------------------------------*/ 42 | 43 | /* Scheduler Related */ 44 | #define configUSE_PREEMPTION 1 45 | #define configUSE_TICKLESS_IDLE 0 46 | #define configUSE_IDLE_HOOK 0 47 | #define configUSE_TICK_HOOK 0 48 | #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) 49 | #define configMAX_PRIORITIES 32 50 | #define configMINIMAL_STACK_SIZE ( configSTACK_DEPTH_TYPE ) 512 51 | #define configUSE_16_BIT_TICKS 0 52 | 53 | #define configIDLE_SHOULD_YIELD 1 54 | 55 | /* Synchronization Related */ 56 | #define configUSE_MUTEXES 1 57 | #define configUSE_RECURSIVE_MUTEXES 1 58 | #define configUSE_APPLICATION_TASK_TAG 0 59 | #define configUSE_COUNTING_SEMAPHORES 1 60 | #define configQUEUE_REGISTRY_SIZE 8 61 | #define configUSE_QUEUE_SETS 1 62 | #define configUSE_TIME_SLICING 1 63 | #define configUSE_NEWLIB_REENTRANT 0 64 | // todo need this for lwip FreeRTOS sys_arch to compile 65 | #define configENABLE_BACKWARD_COMPATIBILITY 1 66 | #define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5 67 | 68 | /* System */ 69 | #define configSTACK_DEPTH_TYPE uint32_t 70 | #define configMESSAGE_BUFFER_LENGTH_TYPE size_t 71 | 72 | /* Memory allocation related definitions. */ 73 | #define configSUPPORT_STATIC_ALLOCATION 0 74 | #define configSUPPORT_DYNAMIC_ALLOCATION 1 75 | #define configTOTAL_HEAP_SIZE (128*1024) 76 | #define configAPPLICATION_ALLOCATED_HEAP 0 77 | 78 | /* Hook function related definitions. */ 79 | #define configCHECK_FOR_STACK_OVERFLOW 0 80 | #define configUSE_MALLOC_FAILED_HOOK 0 81 | #define configUSE_DAEMON_TASK_STARTUP_HOOK 0 82 | 83 | /* Run time and task stats gathering related definitions. */ 84 | #define configGENERATE_RUN_TIME_STATS 0 85 | #define configUSE_TRACE_FACILITY 1 86 | #define configUSE_STATS_FORMATTING_FUNCTIONS 0 87 | 88 | /* Co-routine related definitions. */ 89 | #define configUSE_CO_ROUTINES 0 90 | #define configMAX_CO_ROUTINE_PRIORITIES 1 91 | 92 | /* Software timer related definitions. */ 93 | #define configUSE_TIMERS 1 94 | #define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 ) 95 | #define configTIMER_QUEUE_LENGTH 10 96 | #define configTIMER_TASK_STACK_DEPTH 1024 97 | 98 | /* Interrupt nesting behaviour configuration. */ 99 | /* 100 | #define configKERNEL_INTERRUPT_PRIORITY [dependent of processor] 101 | #define configMAX_SYSCALL_INTERRUPT_PRIORITY [dependent on processor and application] 102 | #define configMAX_API_CALL_INTERRUPT_PRIORITY [dependent on processor and application] 103 | */ 104 | 105 | #if FREE_RTOS_KERNEL_SMP // set by the RP2040 SMP port of FreeRTOS 106 | /* SMP port only */ 107 | #define configNUM_CORES 2 108 | #define configTICK_CORE 0 109 | #define configRUN_MULTIPLE_PRIORITIES 1 110 | #define configUSE_CORE_AFFINITY 1 111 | #endif 112 | 113 | /* RP2040 specific */ 114 | #define configSUPPORT_PICO_SYNC_INTEROP 1 115 | #define configSUPPORT_PICO_TIME_INTEROP 1 116 | 117 | #include 118 | /* Define to trap errors during development. */ 119 | #define configASSERT(x) assert(x) 120 | 121 | /* Set the following definitions to 1 to include the API function, or zero 122 | to exclude the API function. */ 123 | #define INCLUDE_vTaskPrioritySet 1 124 | #define INCLUDE_uxTaskPriorityGet 1 125 | #define INCLUDE_vTaskDelete 1 126 | #define INCLUDE_vTaskSuspend 1 127 | #define INCLUDE_vTaskDelayUntil 1 128 | #define INCLUDE_vTaskDelay 1 129 | #define INCLUDE_xTaskGetSchedulerState 1 130 | #define INCLUDE_xTaskGetCurrentTaskHandle 1 131 | #define INCLUDE_uxTaskGetStackHighWaterMark 1 132 | #define INCLUDE_xTaskGetIdleTaskHandle 1 133 | #define INCLUDE_eTaskGetState 1 134 | #define INCLUDE_xTimerPendFunctionCall 1 135 | #define INCLUDE_xTaskAbortDelay 1 136 | #define INCLUDE_xTaskGetHandle 1 137 | #define INCLUDE_xTaskResumeFromISR 1 138 | #define INCLUDE_xQueueGetMutexHolder 1 139 | 140 | /* A header file that defines trace macro can be included here. */ 141 | 142 | #endif /* FREERTOS_CONFIG_H */ 143 | 144 | -------------------------------------------------------------------------------- /PicoHTTPServer/PicoHTTPServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32811.315 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{803FD0C6-D64E-4E16-9DC3-1DAEC859A3D2}") = "PicoHTTPServer", "PicoHTTPServer.vgdbproj", "{ACC40D02-2301-4CA2-816D-2DC4079047DF}" 7 | EndProject 8 | Project("{803FD0C6-D64E-4E16-9DC3-1DAEC859A3D2}") = "SimpleFSBuilder", "..\tools\SimpleFSBuilder\SimpleFSBuilder.vgdbcmake", "{74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|VisualGDB = Debug|VisualGDB 13 | MinSizeRel|VisualGDB = MinSizeRel|VisualGDB 14 | Release|VisualGDB = Release|VisualGDB 15 | RelWithDebInfo|VisualGDB = RelWithDebInfo|VisualGDB 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {ACC40D02-2301-4CA2-816D-2DC4079047DF}.Debug|VisualGDB.ActiveCfg = Debug|VisualGDB 19 | {ACC40D02-2301-4CA2-816D-2DC4079047DF}.Debug|VisualGDB.Build.0 = Debug|VisualGDB 20 | {ACC40D02-2301-4CA2-816D-2DC4079047DF}.MinSizeRel|VisualGDB.ActiveCfg = MinSizeRel|VisualGDB 21 | {ACC40D02-2301-4CA2-816D-2DC4079047DF}.MinSizeRel|VisualGDB.Build.0 = MinSizeRel|VisualGDB 22 | {ACC40D02-2301-4CA2-816D-2DC4079047DF}.Release|VisualGDB.ActiveCfg = Release|VisualGDB 23 | {ACC40D02-2301-4CA2-816D-2DC4079047DF}.Release|VisualGDB.Build.0 = Release|VisualGDB 24 | {ACC40D02-2301-4CA2-816D-2DC4079047DF}.RelWithDebInfo|VisualGDB.ActiveCfg = RelWithDebInfo|VisualGDB 25 | {ACC40D02-2301-4CA2-816D-2DC4079047DF}.RelWithDebInfo|VisualGDB.Build.0 = RelWithDebInfo|VisualGDB 26 | {74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}.Debug|VisualGDB.ActiveCfg = Debug|VisualGDB 27 | {74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}.Debug|VisualGDB.Build.0 = Debug|VisualGDB 28 | {74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}.MinSizeRel|VisualGDB.ActiveCfg = MinSizeRel|VisualGDB 29 | {74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}.MinSizeRel|VisualGDB.Build.0 = MinSizeRel|VisualGDB 30 | {74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}.Release|VisualGDB.ActiveCfg = Release|VisualGDB 31 | {74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}.Release|VisualGDB.Build.0 = Release|VisualGDB 32 | {74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}.RelWithDebInfo|VisualGDB.ActiveCfg = RelWithDebInfo|VisualGDB 33 | {74B44ADD-7CE6-4F55-929C-FE4C5CEFB6A6}.RelWithDebInfo|VisualGDB.Build.0 = RelWithDebInfo|VisualGDB 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {0F239011-7CCE-40A6-A877-F5606301FEC8} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /PicoHTTPServer/PicoHTTPServer.vgdbproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Unknown 7 | 8 | true 9 | 10 | 11 | 12 | 13 | com.visualgdb.arm-eabi 14 | 15 | 10.3.1 16 | 10.2.90 17 | 1 18 | 19 | 20 | 21 | DEBUG 22 | build/$(PlatformName)/$(ConfigurationName) 23 | 24 | false 25 | $(VISUALGDB_DIR)/ninja.exe 26 | $(BuildDir) 27 | 28 | 29 | 30 | false 31 | $(SYSPROGS_CMAKE_PATH) 32 | 33 | 34 | true 35 | false 36 | false 37 | Ninja 38 | false 39 | RemoveBuildDirectory 40 | false 41 | 42 | 43 | true 44 | true 45 | true 46 | false 47 | true 48 | true 49 | true 50 | HideOuterProjectTargets 51 | true 52 | false 53 | true 54 | 55 | 56 | 57 | PicoHTTPServer 58 | 59 | 60 | Auto 61 | false 62 | false 63 | 64 | Default 65 | 66 | 67 | www 68 | www 69 | * 70 | true 71 | 72 | 73 | 74 | 75 | true 76 | acc40d02-2301-4ca2-816d-2dc4079047df 77 | 78 | Upper 79 | HeaderDirectoryAndSubdirectories 80 | true 81 | 82 | 83 | Package 84 | 1.4.0 85 | 86 | 87 | pico_w 88 | PicoHTTP 89 | 90 | 91 | true 92 | 93 | 94 | com.sysprogs.embedded.semihosting_and_profiler 95 | 96 | 97 | 98 | 99 | com.sysprogs.efp.profiling.debugger_check 100 | SYSPROGS_PROFILER_DEBUGGER_CHECK_MODE=1 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Default 120 | 121 | 122 | 123 | true 124 | 125 | 126 | 127 | 128 | Unknown 129 | 130 | true 131 | true 132 | true 133 | 134 | 135 | 136 | false 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | false 146 | false 147 | false 148 | false 149 | false 150 | false 151 | false 152 | false 153 | false 154 | 155 | false 156 | false 157 | false 158 | false 159 | false 160 | false 161 | true 162 | false 163 | None 164 | false 165 | false 166 | main 167 | true 168 | false 169 | false 170 | true 171 | 0 172 | false 173 | 0 174 | true 175 | false 176 | 177 | 178 | com.sysprogs.arm.openocd 179 | olimex-arm-usb-ocd-h 180 | OLZ6HS42 181 | 182 | -f interface/ftdi/olimex-arm-usb-ocd-h.cfg -c "adapter speed 3000" -f transport/olimex-arm-jtag-swd.cfg -c "transport select swd" -f target/rp2040.cfg -c init -c "reset init" 183 | 184 | 185 | 186 | false 187 | 188 | 131072 189 | Enabled 190 | 191 | set remotetimeout 60 192 | target remote :$$SYS:GDB_PORT$$ 193 | mon halt 194 | mon reset init 195 | load 196 | 197 | false 198 | 0 199 | 0 200 | false 201 | 202 | 5000 203 | 204 | 205 | true 206 | Auto 207 | 0 208 | false 209 | false 210 | true 211 | false 212 | false 213 | 214 | _estack 215 | 0 216 | false 217 | 218 | true 219 | 220 | -------------------------------------------------------------------------------- /PicoHTTPServer/PicoSDKConfig.cmake: -------------------------------------------------------------------------------- 1 | #This file was generated by VisualGDB and will be overwritten when you change the configuration via GUI. 2 | 3 | -------------------------------------------------------------------------------- /PicoHTTPServer/debug_printf.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void debug_printf(const char *fmt, ...); 4 | void debug_write(const void *data, int size); 5 | -------------------------------------------------------------------------------- /PicoHTTPServer/dhcpserver/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2022 Damien P. George 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /PicoHTTPServer/dhcpserver/dhcpserver.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the MicroPython project, http://micropython.org/ 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2018-2019 Damien P. George 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // For DHCP specs see: 28 | // https://www.ietf.org/rfc/rfc2131.txt 29 | // https://tools.ietf.org/html/rfc2132 -- DHCP Options and BOOTP Vendor Extensions 30 | 31 | #include 32 | #include 33 | #include 34 | 35 | #include "cyw43_config.h" 36 | #include "dhcpserver.h" 37 | #include "lwip/udp.h" 38 | #include "../debug_printf.h" 39 | 40 | #define DHCPDISCOVER (1) 41 | #define DHCPOFFER (2) 42 | #define DHCPREQUEST (3) 43 | #define DHCPDECLINE (4) 44 | #define DHCPACK (5) 45 | #define DHCPNACK (6) 46 | #define DHCPRELEASE (7) 47 | #define DHCPINFORM (8) 48 | 49 | #define DHCP_OPT_PAD (0) 50 | #define DHCP_OPT_SUBNET_MASK (1) 51 | #define DHCP_OPT_ROUTER (3) 52 | #define DHCP_OPT_DNS (6) 53 | #define DHCP_OPT_HOST_NAME (12) 54 | #define DHCP_OPT_DOMAIN_NAME (15) 55 | #define DHCP_OPT_REQUESTED_IP (50) 56 | #define DHCP_OPT_IP_LEASE_TIME (51) 57 | #define DHCP_OPT_MSG_TYPE (53) 58 | #define DHCP_OPT_SERVER_ID (54) 59 | #define DHCP_OPT_PARAM_REQUEST_LIST (55) 60 | #define DHCP_OPT_MAX_MSG_SIZE (57) 61 | #define DHCP_OPT_VENDOR_CLASS_ID (60) 62 | #define DHCP_OPT_CLIENT_ID (61) 63 | 64 | /*Note that on Android, the captive portal option will only work via HTTPS. 65 | See this file for details: https://cs.android.com/android/platform/superproject/+/master:packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java 66 | */ 67 | #define DHCP_OPT_CAPTIVE_PORTAL (114) 68 | #define DHCP_OPT_END (255) 69 | 70 | #define PORT_DHCP_SERVER (67) 71 | #define PORT_DHCP_CLIENT (68) 72 | 73 | #define DEFAULT_LEASE_TIME_S (24 * 60 * 60) // in seconds 74 | 75 | #define MAC_LEN (6) 76 | #define MAKE_IP4(a, b, c, d) ((a) << 24 | (b) << 16 | (c) << 8 | (d)) 77 | 78 | typedef struct { 79 | uint8_t op; // message opcode 80 | uint8_t htype; // hardware address type 81 | uint8_t hlen; // hardware address length 82 | uint8_t hops; 83 | uint32_t xid; // transaction id, chosen by client 84 | uint16_t secs; // client seconds elapsed 85 | uint16_t flags; 86 | uint8_t ciaddr[4]; // client IP address 87 | uint8_t yiaddr[4]; // your IP address 88 | uint8_t siaddr[4]; // next server IP address 89 | uint8_t giaddr[4]; // relay agent IP address 90 | uint8_t chaddr[16]; // client hardware address 91 | uint8_t sname[64]; // server host name 92 | uint8_t file[128]; // boot file name 93 | uint8_t options[312]; // optional parameters, variable, starts with magic 94 | } dhcp_msg_t; 95 | 96 | static int dhcp_socket_new_dgram(struct udp_pcb **udp, void *cb_data, udp_recv_fn cb_udp_recv) { 97 | // family is AF_INET 98 | // type is SOCK_DGRAM 99 | 100 | *udp = udp_new(); 101 | if (*udp == NULL) { 102 | return -ENOMEM; 103 | } 104 | 105 | // Register callback 106 | udp_recv(*udp, cb_udp_recv, (void *)cb_data); 107 | 108 | return 0; // success 109 | } 110 | 111 | static void dhcp_socket_free(struct udp_pcb **udp) { 112 | if (*udp != NULL) { 113 | udp_remove(*udp); 114 | *udp = NULL; 115 | } 116 | } 117 | 118 | static int dhcp_socket_bind(struct udp_pcb **udp, uint32_t ip, uint16_t port) { 119 | ip_addr_t addr; 120 | IP4_ADDR(&addr, ip >> 24 & 0xff, ip >> 16 & 0xff, ip >> 8 & 0xff, ip & 0xff); 121 | // TODO convert lwIP errors to errno 122 | return udp_bind(*udp, &addr, port); 123 | } 124 | 125 | static int dhcp_socket_sendto(struct udp_pcb **udp, const void *buf, size_t len, uint32_t ip, uint16_t port) { 126 | if (len > 0xffff) { 127 | len = 0xffff; 128 | } 129 | 130 | struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); 131 | if (p == NULL) { 132 | return -ENOMEM; 133 | } 134 | 135 | memcpy(p->payload, buf, len); 136 | 137 | ip_addr_t dest; 138 | IP4_ADDR(&dest, ip >> 24 & 0xff, ip >> 16 & 0xff, ip >> 8 & 0xff, ip & 0xff); 139 | err_t err = udp_sendto(*udp, p, &dest, port); 140 | 141 | pbuf_free(p); 142 | 143 | if (err != ERR_OK) { 144 | return err; 145 | } 146 | 147 | return len; 148 | } 149 | 150 | static uint8_t *opt_find(uint8_t *opt, uint8_t cmd) { 151 | for (int i = 0; i < 308 && opt[i] != DHCP_OPT_END;) { 152 | if (opt[i] == cmd) { 153 | return &opt[i]; 154 | } 155 | i += 2 + opt[i + 1]; 156 | } 157 | return NULL; 158 | } 159 | 160 | static void opt_write_n(uint8_t **opt, uint8_t cmd, size_t n, const void *data) { 161 | uint8_t *o = *opt; 162 | *o++ = cmd; 163 | *o++ = n; 164 | memcpy(o, data, n); 165 | *opt = o + n; 166 | } 167 | 168 | static void opt_write_u8(uint8_t **opt, uint8_t cmd, uint8_t val) { 169 | uint8_t *o = *opt; 170 | *o++ = cmd; 171 | *o++ = 1; 172 | *o++ = val; 173 | *opt = o; 174 | } 175 | 176 | static void opt_write_u32(uint8_t **opt, uint8_t cmd, uint32_t val) { 177 | uint8_t *o = *opt; 178 | *o++ = cmd; 179 | *o++ = 4; 180 | *o++ = val >> 24; 181 | *o++ = val >> 16; 182 | *o++ = val >> 8; 183 | *o++ = val; 184 | *opt = o; 185 | } 186 | 187 | static void dhcp_server_process(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *src_addr, u16_t src_port) { 188 | dhcp_server_t *d = arg; 189 | (void)upcb; 190 | (void)src_addr; 191 | (void)src_port; 192 | 193 | // This is around 548 bytes 194 | dhcp_msg_t dhcp_msg; 195 | 196 | #define DHCP_MIN_SIZE (240 + 3) 197 | if (p->tot_len < DHCP_MIN_SIZE) { 198 | goto ignore_request; 199 | } 200 | 201 | size_t len = pbuf_copy_partial(p, &dhcp_msg, sizeof(dhcp_msg), 0); 202 | if (len < DHCP_MIN_SIZE) { 203 | goto ignore_request; 204 | } 205 | 206 | dhcp_msg.op = DHCPOFFER; 207 | memcpy(&dhcp_msg.yiaddr, &d->ip.addr, 4); 208 | 209 | uint8_t *opt = (uint8_t *)&dhcp_msg.options; 210 | opt += 4; // assume magic cookie: 99, 130, 83, 99 211 | 212 | switch (opt[2]) { 213 | case DHCPDISCOVER: { 214 | int yi = DHCPS_MAX_IP; 215 | for (int i = 0; i < DHCPS_MAX_IP; ++i) { 216 | if (memcmp(d->lease[i].mac, dhcp_msg.chaddr, MAC_LEN) == 0) { 217 | // MAC match, use this IP address 218 | yi = i; 219 | break; 220 | } 221 | if (yi == DHCPS_MAX_IP) { 222 | // Look for a free IP address 223 | if (memcmp(d->lease[i].mac, "\x00\x00\x00\x00\x00\x00", MAC_LEN) == 0) { 224 | // IP available 225 | yi = i; 226 | } 227 | uint32_t expiry = d->lease[i].expiry << 16 | 0xffff; 228 | if ((int32_t)(expiry - cyw43_hal_ticks_ms()) < 0) { 229 | // IP expired, reuse it 230 | memset(d->lease[i].mac, 0, MAC_LEN); 231 | yi = i; 232 | } 233 | } 234 | } 235 | if (yi == DHCPS_MAX_IP) { 236 | // No more IP addresses left 237 | goto ignore_request; 238 | } 239 | dhcp_msg.yiaddr[3] = DHCPS_BASE_IP + yi; 240 | opt_write_u8(&opt, DHCP_OPT_MSG_TYPE, DHCPOFFER); 241 | break; 242 | } 243 | 244 | case DHCPREQUEST: { 245 | uint8_t *o = opt_find(opt, DHCP_OPT_REQUESTED_IP); 246 | if (o == NULL) { 247 | // Should be NACK 248 | goto ignore_request; 249 | } 250 | if (memcmp(o + 2, &d->ip.addr, 3) != 0) { 251 | // Should be NACK 252 | goto ignore_request; 253 | } 254 | uint8_t yi = o[5] - DHCPS_BASE_IP; 255 | if (yi >= DHCPS_MAX_IP) { 256 | // Should be NACK 257 | goto ignore_request; 258 | } 259 | if (memcmp(d->lease[yi].mac, dhcp_msg.chaddr, MAC_LEN) == 0) { 260 | // MAC match, ok to use this IP address 261 | } else if (memcmp(d->lease[yi].mac, "\x00\x00\x00\x00\x00\x00", MAC_LEN) == 0) { 262 | // IP unused, ok to use this IP address 263 | memcpy(d->lease[yi].mac, dhcp_msg.chaddr, MAC_LEN); 264 | } else { 265 | // IP already in use 266 | // Should be NACK 267 | goto ignore_request; 268 | } 269 | d->lease[yi].expiry = (cyw43_hal_ticks_ms() + DEFAULT_LEASE_TIME_S * 1000) >> 16; 270 | dhcp_msg.yiaddr[3] = DHCPS_BASE_IP + yi; 271 | opt_write_u8(&opt, DHCP_OPT_MSG_TYPE, DHCPACK); 272 | debug_printf("DHCPS: client connected: MAC=%02x:%02x:%02x:%02x:%02x:%02x IP=%u.%u.%u.%u\n", 273 | dhcp_msg.chaddr[0], dhcp_msg.chaddr[1], dhcp_msg.chaddr[2], dhcp_msg.chaddr[3], dhcp_msg.chaddr[4], dhcp_msg.chaddr[5], 274 | dhcp_msg.yiaddr[0], dhcp_msg.yiaddr[1], dhcp_msg.yiaddr[2], dhcp_msg.yiaddr[3]); 275 | break; 276 | } 277 | 278 | default: 279 | goto ignore_request; 280 | } 281 | 282 | opt_write_n(&opt, DHCP_OPT_SERVER_ID, 4, &d->ip.addr); 283 | opt_write_n(&opt, DHCP_OPT_SUBNET_MASK, 4, &d->nm.addr); 284 | opt_write_n(&opt, DHCP_OPT_ROUTER, 4, &d->ip.addr); // aka gateway; can have mulitple addresses 285 | opt_write_n(&opt, DHCP_OPT_DNS, 4, &d->ip.addr); // can have mulitple addresses 286 | 287 | if (d->domain_name) 288 | { 289 | opt_write_n(&opt, DHCP_OPT_DOMAIN_NAME, strlen(d->domain_name), d->domain_name); 290 | } 291 | 292 | opt_write_u32(&opt, DHCP_OPT_IP_LEASE_TIME, DEFAULT_LEASE_TIME_S); 293 | *opt++ = DHCP_OPT_END; 294 | dhcp_socket_sendto(&d->udp, &dhcp_msg, opt - (uint8_t *)&dhcp_msg, 0xffffffff, PORT_DHCP_CLIENT); 295 | 296 | ignore_request: 297 | pbuf_free(p); 298 | } 299 | 300 | void dhcp_server_init(dhcp_server_t *d, ip_addr_t *ip, ip_addr_t *nm, const char *domain_name) { 301 | ip_addr_copy(d->ip, *ip); 302 | ip_addr_copy(d->nm, *nm); 303 | d->domain_name = domain_name; 304 | memset(d->lease, 0, sizeof(d->lease)); 305 | if (dhcp_socket_new_dgram(&d->udp, d, dhcp_server_process) != 0) { 306 | return; 307 | } 308 | dhcp_socket_bind(&d->udp, 0, PORT_DHCP_SERVER); 309 | } 310 | 311 | void dhcp_server_deinit(dhcp_server_t *d) { 312 | dhcp_socket_free(&d->udp); 313 | } 314 | -------------------------------------------------------------------------------- /PicoHTTPServer/dhcpserver/dhcpserver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the MicroPython project, http://micropython.org/ 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2018-2019 Damien P. George 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | #ifndef MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H 27 | #define MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H 28 | 29 | #include "lwip/ip_addr.h" 30 | 31 | #define DHCPS_BASE_IP (16) 32 | #define DHCPS_MAX_IP (8) 33 | 34 | typedef struct _dhcp_server_lease_t { 35 | uint8_t mac[6]; 36 | uint16_t expiry; 37 | } dhcp_server_lease_t; 38 | 39 | typedef struct _dhcp_server_t { 40 | ip_addr_t ip; 41 | ip_addr_t nm; 42 | dhcp_server_lease_t lease[DHCPS_MAX_IP]; 43 | struct udp_pcb *udp; 44 | const char *domain_name; 45 | } dhcp_server_t; 46 | 47 | void dhcp_server_init(dhcp_server_t *d, ip_addr_t *ip, ip_addr_t *nm, const char *domain_name); 48 | void dhcp_server_deinit(dhcp_server_t *d); 49 | 50 | #endif // MICROPY_INCLUDED_LIB_NETUTILS_DHCPSERVER_H 51 | -------------------------------------------------------------------------------- /PicoHTTPServer/dns/dnsserver.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include "../debug_printf.h" 11 | #include "../server_settings.h" 12 | 13 | static struct 14 | { 15 | uint32_t primary_ip; 16 | uint32_t secondary_ip; 17 | const char *host_name; 18 | const char *domain_name; 19 | bool ignore_network_suffix; 20 | } s_DNSServerSettings; 21 | 22 | //DNS protocol definitions and parsing/formatting logic from https://github.com/devyte/ESPAsyncDNSServer/blob/master/src/ESPAsyncDNSServer.cpp 23 | struct DNSHeader 24 | { 25 | uint16_t ID; // identification number 26 | unsigned char RD : 1; // recursion desired 27 | unsigned char TC : 1; // truncated message 28 | unsigned char AA : 1; // authoritive answer 29 | unsigned char OPCode : 4; // message_type 30 | unsigned char QR : 1; // query/response flag 31 | unsigned char RCode : 4; // response code 32 | unsigned char Z : 3; // its z! reserved 33 | unsigned char RA : 1; // recursion available 34 | uint16_t QDCount; // number of question entries 35 | uint16_t ANCount; // number of answer entries 36 | uint16_t NSCount; // number of authority entries 37 | uint16_t ARCount; // number of resource entries 38 | }; 39 | 40 | 41 | struct IPResourceRecord 42 | { 43 | uint16_t Type; 44 | uint16_t Class; 45 | uint32_t TTL; 46 | uint16_t DataLength; 47 | uint32_t Data; 48 | } __attribute__((packed)); 49 | 50 | static const char *get_encoded_domain_name_component(const uint8_t *buffer, size_t *offset, size_t max_input_size, int *component_len) 51 | { 52 | for (int i = *offset; i < max_input_size;) 53 | { 54 | if (buffer[i] & 0xC0) 55 | { 56 | i = ((buffer[i] & 0x3F) << 8) | (buffer[i+1]); 57 | continue; 58 | } 59 | 60 | uint8_t len = buffer[i] & 0x3F; 61 | 62 | if ((i + len) >= max_input_size) 63 | return NULL; 64 | 65 | if (!len) 66 | return NULL; 67 | 68 | *offset = ++i + len; 69 | *component_len = len; 70 | return (const char *)buffer + i; 71 | } 72 | 73 | return NULL; 74 | } 75 | 76 | static uint32_t get_address_for_encoded_domain(const uint8_t *buffer, size_t offset, size_t buffer_size) 77 | { 78 | debug_printf("DNS server: "); 79 | bool match = false, loose_match = false; 80 | 81 | int domain_off = 0, domain_len = 0; 82 | const char *domain_comp = get_next_domain_name_component(s_DNSServerSettings.domain_name, &domain_off, &domain_len); 83 | 84 | for (int i = 0; ; i++) 85 | { 86 | int len; 87 | const char *component = get_encoded_domain_name_component(buffer, &offset, buffer_size, &len); 88 | if (component) 89 | { 90 | if (i) 91 | debug_write(".", 1); 92 | debug_write(component, len); 93 | 94 | if (i == 0 && !strncasecmp(component, s_DNSServerSettings.host_name, len)) 95 | { 96 | match = true; 97 | if (s_DNSServerSettings.ignore_network_suffix) 98 | loose_match = true; 99 | } 100 | else if (i > 0 && match && domain_comp && len == domain_len && !strncasecmp(component, domain_comp, len)) 101 | { 102 | match = true; 103 | domain_comp = get_next_domain_name_component(s_DNSServerSettings.domain_name, &domain_off, &domain_len); 104 | } 105 | else 106 | match = false; 107 | } 108 | else 109 | { 110 | uint32_t ip = (match || loose_match) ? s_DNSServerSettings.primary_ip : s_DNSServerSettings.secondary_ip ; 111 | debug_printf(" -> %d.%d.%d.%d\n", (ip >> 0) & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); 112 | return ip; 113 | } 114 | } 115 | } 116 | 117 | #define DNS_QR_QUERY 0 118 | #define DNS_QR_RESPONSE 1 119 | #define DNS_OPCODE_QUERY 0 120 | 121 | static void dns_server_thread(void *unused) 122 | { 123 | int server_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); 124 | struct sockaddr_in listen_addr = 125 | { 126 | .sin_len = sizeof(struct sockaddr_in), 127 | .sin_family = AF_INET, 128 | .sin_port = htons(53), 129 | .sin_addr = 0, 130 | }; 131 | 132 | if (server_sock < 0) 133 | { 134 | debug_printf("Unable to create DNS server socket: error %d", errno); 135 | return; 136 | } 137 | 138 | if (bind(server_sock, (struct sockaddr *)&listen_addr, sizeof(listen_addr)) < 0) 139 | { 140 | debug_printf("Unable to bind DNS server socket: error %d\n", errno); 141 | return; 142 | } 143 | 144 | while (true) 145 | { 146 | static struct 147 | { 148 | struct DNSHeader header; 149 | char payload[1536 - sizeof(struct DNSHeader)]; 150 | } packet; 151 | 152 | struct sockaddr_storage fromaddr; 153 | socklen_t addr_size = sizeof(fromaddr); 154 | int done = recvfrom(server_sock, &packet, sizeof(packet), 0, (struct sockaddr *)&fromaddr, &addr_size); 155 | if (done >= sizeof(packet.header) && done < (sizeof(packet) - sizeof(struct IPResourceRecord) - 2) && 156 | packet.header.QR == DNS_QR_QUERY && 157 | packet.header.OPCode == DNS_OPCODE_QUERY && 158 | packet.header.QDCount == htons(1) && 159 | packet.header.ANCount == 0 && 160 | packet.header.NSCount == 0 && 161 | packet.header.ARCount == 0) 162 | { 163 | packet.header.QR = DNS_QR_RESPONSE; 164 | packet.header.ANCount = ntohs(1); 165 | 166 | int ptr = done - sizeof(packet.header); 167 | packet.payload[ptr++] = 0xC0; //Domain name is a pointer 168 | packet.payload[ptr++] = sizeof(packet.header); //The pointer points to the original domain name 169 | 170 | struct IPResourceRecord rec = { 171 | .Type = htons(1), //A 172 | .Class = htons(1), //IN 173 | .TTL = htonl(1), //1 second 174 | .DataLength = htons(4), 175 | }; 176 | 177 | rec.Data = get_address_for_encoded_domain((char *)&packet.header, sizeof(packet.header), done); 178 | 179 | memcpy(packet.payload + ptr, &rec, sizeof(rec)); 180 | ptr += sizeof(rec); 181 | sendto(server_sock, &packet, ptr + sizeof(packet.header), 0, (const struct sockaddr *)&fromaddr, addr_size); 182 | } 183 | } 184 | } 185 | 186 | void dns_server_init(uint32_t primary_ip, 187 | uint32_t secondary_ip, 188 | const char *host_name, 189 | const char *domain_name, 190 | bool dns_ignores_network_suffix) 191 | { 192 | s_DNSServerSettings.primary_ip = primary_ip; 193 | s_DNSServerSettings.secondary_ip = secondary_ip; 194 | s_DNSServerSettings.host_name = host_name; 195 | s_DNSServerSettings.domain_name = domain_name; 196 | s_DNSServerSettings.ignore_network_suffix = dns_ignores_network_suffix; 197 | 198 | TaskHandle_t task; 199 | xTaskCreate(dns_server_thread, "DNS server", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, &task); 200 | } -------------------------------------------------------------------------------- /PicoHTTPServer/dns/dnsserver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | void dns_server_init(uint32_t primary_ip, 5 | uint32_t secondary_ip, 6 | const char *host_name, 7 | const char *domain_name, 8 | bool dns_ignores_network_suffix); 9 | -------------------------------------------------------------------------------- /PicoHTTPServer/httpserver.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "debug_printf.h" 14 | #include "httpserver.h" 15 | 16 | struct _http_server_instance 17 | { 18 | int socket; 19 | int buffer_size; 20 | const char *hostname; 21 | const char *domain_name; 22 | xSemaphoreHandle semaphore; 23 | http_zone *first_zone; 24 | }; 25 | 26 | struct _http_connection 27 | { 28 | http_server_instance server; 29 | int socket; 30 | size_t buffered_size; 31 | struct 32 | { 33 | int buffer_used, buffer_pos; 34 | int remaining_input_len; 35 | int offset_from_main_buffer; 36 | } post; 37 | 38 | char buffer[1]; 39 | }; 40 | 41 | //Receive at least one line into the buffer. Return the total size of received data. 42 | static int recv_line(int socket, char *buffer, int buffer_size) 43 | { 44 | int buffer_done = 0; 45 | while (buffer_done < buffer_size) 46 | { 47 | int done = recv(socket, buffer, buffer_size - buffer_done, 0); 48 | if (done <= 0) 49 | return 0; 50 | 51 | buffer_done += done; 52 | char *p = strnstr(buffer, "\r\n", buffer_done); 53 | if (p) 54 | return buffer_done; 55 | } 56 | 57 | return 0; 58 | } 59 | 60 | //Read next line using the buffer (multiple lines can be buffered at once). 61 | //If the line was too long to fit into the buffer, returned length will be negative, but the next line will still get found correctly. 62 | static char *recv_next_line_buffered(int socket, char *buffer, int buffer_size, int *buffer_used, int *offset, int *len, int *recv_limit) 63 | { 64 | int skipped_len = 0; 65 | if (*offset > *buffer_used) 66 | return NULL; 67 | 68 | for (;;) 69 | { 70 | char *start = buffer + *offset; 71 | char *limit = buffer + *buffer_used; 72 | char *p = strnstr(start, "\r\n", limit - start); 73 | if (p) 74 | { 75 | *p = 0; 76 | *offset = p + 2 - buffer; 77 | if (skipped_len) 78 | *len = -(p - start + skipped_len); 79 | else 80 | *len = p - start; 81 | return start; 82 | } 83 | 84 | if (*offset == 0 && buffer_size == *buffer_used) 85 | { 86 | /* The length of this line exceeds the entire buffer. 87 | * Discard the buffer contents and continue searching for the end-of-line.*/ 88 | 89 | buffer[0] = buffer[buffer_size - 1]; 90 | *buffer_used = (buffer[0] == '\r') ? 1 : 0; 91 | skipped_len = buffer_size - *buffer_used; 92 | *offset = 0; 93 | } 94 | else if (start < limit && start > buffer) 95 | { 96 | memmove(buffer, start, limit - start); 97 | *buffer_used -= *offset; 98 | *offset = 0; 99 | } 100 | 101 | int buffer_avail = buffer_size - *buffer_used; 102 | if (recv_limit) 103 | buffer_avail = MIN(buffer_avail, *recv_limit); 104 | 105 | if (buffer_avail <= 0) 106 | return NULL; 107 | 108 | int done = recv(socket, buffer + *buffer_used, buffer_avail, 0); 109 | if (done <= 0) 110 | return NULL; 111 | 112 | if (recv_limit) 113 | *recv_limit -= done; 114 | 115 | *buffer_used += done; 116 | } 117 | } 118 | 119 | static bool host_name_matches(http_connection ctx, char *host) 120 | { 121 | int len = strlen(ctx->server->hostname); 122 | if (strncasecmp(host, ctx->server->hostname, len)) 123 | return false; 124 | 125 | if (!host[len]) 126 | return true; //Host name without domain 127 | 128 | if (host[len] == '.' && !strcasecmp(host + len + 1, ctx->server->domain_name)) 129 | return true; //Host name with domain 130 | 131 | return false; 132 | } 133 | 134 | static bool send_all(int socket, const char *buf, int size) 135 | { 136 | while (size > 0) 137 | { 138 | #if MEM_SIZE < 16384 139 | /* As of SDK 1.4.0, lwIP running out of memory to allocate a network buffer on TCP send 140 | * permanently stalls the entire netconn. As it doesn't free up the resources taken by 141 | * that netconn, the effect quickly snowballs, rendering the entire network stack unusable: 142 | * 143 | * 1. tcp_pbuf_prealloc() called by tcp_write() returns a NULL. 144 | * 2. tcp_write() returns ERR_MEM 145 | * 3. lwip_netconn_do_write() receives ERR_MEM and assumes that it needs to wait for 146 | * the remote side to acknowledge the receipt. So it begins waiting on the netconn semaphore: 147 | * sys_arch_sem_wait(LWIP_API_MSG_SEM(msg), 0) 148 | * 4. As we did not send out any meaningful data, the acknowledgement (normally done in tcp_receive()) 149 | * never happens, and the lwip_send() never returns. 150 | * 151 | * You can easily detect this condition by checking lwip_stats.tcp.memerr. If the value is not 0, 152 | * the IP stack has run out of memory at some point and might get stuck as described before. 153 | * 154 | * Increasing MEM_SIZE generally solves this issue, although it might return if multiple threads 155 | * attempt to send large amounts of data simultaneously. 156 | **/ 157 | #error Too little memory allocated for lwIP buffers. 158 | #endif 159 | int done = send(socket, buf, size, 0); 160 | if (done <= 0) 161 | return false; 162 | 163 | buf += done; 164 | size -= done; 165 | } 166 | 167 | return true; 168 | } 169 | 170 | static inline void append(char *buf, int *offset, const char *data, int len) 171 | { 172 | memcpy(buf + *offset, data, len); 173 | *offset += len; 174 | } 175 | 176 | static void parse_and_handle_http_request(http_connection ctx) 177 | { 178 | int len = recv_line(ctx->socket, ctx->buffer, ctx->server->buffer_size); 179 | char *path = NULL; 180 | char *header_buf = NULL; 181 | int header_buf_size = 0, header_buf_pos = 0, header_buf_used = 0; 182 | char host[32]; 183 | host[0] = 0; 184 | enum http_request_type reqtype = HTTP_GET; 185 | ctx->post.remaining_input_len = ctx->post.buffer_used = ctx->post.buffer_pos = 0; 186 | 187 | if (len) 188 | { 189 | //Expected request format: GET HTTP/1.0 190 | char *p1 = strchr(ctx->buffer, ' '), *p2 = NULL, *p3 = NULL; 191 | if (p1) 192 | p2 = strchr(++p1, ' '); 193 | 194 | if (!strncasecmp(ctx->buffer, "POST ", 5)) 195 | reqtype = HTTP_POST; 196 | 197 | if (p2) 198 | p3 = strstr(p2, "\r\n"); 199 | 200 | if (p3) 201 | { 202 | path = p1; 203 | *p2 = 0; 204 | 205 | int off = p3 + 2 - ctx->buffer; 206 | header_buf = ctx->buffer + off; 207 | header_buf_size = ctx->server->buffer_size - off; 208 | header_buf_used = len - off; 209 | } 210 | } 211 | 212 | if (!header_buf || header_buf_size < 32) 213 | { 214 | debug_printf("HTTP: invalid first line"); 215 | return; 216 | } 217 | 218 | for (;;) 219 | { 220 | char *line = recv_next_line_buffered(ctx->socket, header_buf, header_buf_size, &header_buf_used, &header_buf_pos, &len, NULL); 221 | if (!line) 222 | { 223 | debug_printf("HTTP: unexpected end of headers"); 224 | return; 225 | } 226 | 227 | if (!line[0]) 228 | break; //Proper end of headers 229 | 230 | if (len > 0 && !strncasecmp(line, "Host: ", 6) && (len - 6) < (sizeof(host) - 1)) 231 | { 232 | memcpy(host, line + 6, len - 6); 233 | host[len - 6] = 0; 234 | } 235 | else if (!strncasecmp(line, "Content-length: ", 16)) 236 | { 237 | ctx->post.remaining_input_len = atoi(line + 16); 238 | } 239 | } 240 | 241 | if (reqtype == HTTP_POST && ctx->post.remaining_input_len) 242 | { 243 | ctx->post.buffer_pos = header_buf_pos; 244 | ctx->post.buffer_used = header_buf_used; 245 | ctx->post.remaining_input_len -= (header_buf_used - header_buf_pos); 246 | ctx->post.offset_from_main_buffer = header_buf - ctx->buffer; 247 | } 248 | 249 | debug_printf("HTTP: %s%s\n", host, path); 250 | 251 | if (!host_name_matches(ctx, host)) 252 | { 253 | static const char header[] = "HTTP/1.0 302 Found\r\nLocation: http://"; 254 | static const char footer[] = "\r\nConnection: Close\r\n\r\n"; 255 | int host_len = strlen(ctx->server->hostname), domain_len = strlen(ctx->server->domain_name); 256 | 257 | int len = sizeof(header) + sizeof(footer) + host_len + domain_len + 2; 258 | if (len < ctx->server->buffer_size) 259 | { 260 | int off = 0; 261 | append(ctx->buffer, &off, header, sizeof(header) - 1); 262 | append(ctx->buffer, &off, ctx->server->hostname, host_len); 263 | if (domain_len) 264 | { 265 | append(ctx->buffer, &off, ".", 1); 266 | append(ctx->buffer, &off, ctx->server->domain_name, domain_len); 267 | } 268 | append(ctx->buffer, &off, footer, sizeof(footer) - 1); 269 | send_all(ctx->socket, ctx->buffer, off); 270 | } 271 | } 272 | else 273 | { 274 | for (http_zone *zone = ctx->server->first_zone; zone; zone = zone->next) 275 | { 276 | if (strncasecmp(path, zone->prefix, zone->prefix_len)) 277 | continue; 278 | 279 | int off = zone->prefix_len; 280 | if (path[off] == 0 || path[off] == '/') 281 | { 282 | while (path[off] == '/') 283 | off++; 284 | 285 | if (zone->handler(ctx, reqtype, path + off, zone->context)) 286 | return; 287 | } 288 | } 289 | 290 | http_server_send_reply(ctx, "404 Not Found", "text/plain", "File not found", -1); 291 | } 292 | } 293 | 294 | static void do_handle_connection(void *arg) 295 | { 296 | http_connection ctx = (http_connection)arg; 297 | parse_and_handle_http_request(ctx); 298 | closesocket(ctx->socket); 299 | vPortFree(ctx); 300 | xSemaphoreGive(ctx->server->semaphore); 301 | vTaskDelete(NULL); 302 | } 303 | 304 | static void http_server_thread(void *arg) 305 | { 306 | http_server_instance sctx = (http_server_instance)arg; 307 | 308 | while (true) 309 | { 310 | struct sockaddr_storage remote_addr; 311 | socklen_t len = sizeof(remote_addr); 312 | int conn_sock = accept(sctx->socket, (struct sockaddr *)&remote_addr, &len); 313 | if (conn_sock >= 0) 314 | { 315 | http_connection cctx = pvPortMalloc(sizeof(struct _http_connection) + sctx->buffer_size); 316 | if (cctx) 317 | { 318 | cctx->server = sctx; 319 | cctx->socket = conn_sock; 320 | TaskHandle_t task; 321 | xSemaphoreTake(sctx->semaphore, portMAX_DELAY); 322 | if (xTaskCreate(do_handle_connection, "HTTP Connection", configMINIMAL_STACK_SIZE, cctx, tskIDLE_PRIORITY + 2, &task) != pdTRUE) 323 | { 324 | vPortFree(cctx); 325 | xSemaphoreGive(sctx->semaphore); 326 | cctx = NULL; 327 | } 328 | } 329 | 330 | if (!cctx) 331 | closesocket(conn_sock); 332 | } 333 | } 334 | } 335 | 336 | http_server_instance http_server_create(const char *main_host, const char *main_domain, int max_thread_count, int buffer_size) 337 | { 338 | int server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); 339 | struct sockaddr_in listen_addr = 340 | { 341 | .sin_len = sizeof(struct sockaddr_in), 342 | .sin_family = AF_INET, 343 | .sin_port = htons(80), 344 | .sin_addr = 0, 345 | }; 346 | 347 | if (server_sock < 0) 348 | { 349 | debug_printf("Unable to create HTTP socket: error %d", errno); 350 | return NULL; 351 | } 352 | 353 | if (bind(server_sock, (struct sockaddr *)&listen_addr, sizeof(listen_addr)) < 0) 354 | { 355 | closesocket(server_sock); 356 | debug_printf("Unable to bind HTTP socket: error %d\n", errno); 357 | return NULL; 358 | } 359 | 360 | if (listen(server_sock, max_thread_count * 2) < 0) 361 | { 362 | closesocket(server_sock); 363 | debug_printf("Unable to listen on HTTP socket: error %d\n", errno); 364 | return NULL; 365 | } 366 | 367 | http_server_instance ctx = (http_server_instance)pvPortMalloc(sizeof(struct _http_server_instance)); 368 | if (!ctx) 369 | { 370 | closesocket(server_sock); 371 | return NULL; 372 | } 373 | 374 | ctx->socket = server_sock; 375 | ctx->semaphore = xSemaphoreCreateCounting(max_thread_count, max_thread_count); 376 | ctx->hostname = main_host; 377 | ctx->domain_name = main_domain; 378 | ctx->buffer_size = buffer_size; 379 | ctx->first_zone = NULL; 380 | 381 | TaskHandle_t task; 382 | xTaskCreate(http_server_thread, "HTTP Server", configMINIMAL_STACK_SIZE, ctx, tskIDLE_PRIORITY + 2, &task); 383 | return ctx; 384 | } 385 | 386 | void http_server_add_zone(http_server_instance server, http_zone *zone, const char *prefix, http_request_handler handler, void *context) 387 | { 388 | zone->next = server->first_zone; 389 | zone->prefix = prefix; 390 | zone->prefix_len = strlen(prefix); 391 | zone->handler = handler; 392 | zone->context = context; 393 | server->first_zone = zone; 394 | } 395 | 396 | void http_server_send_reply(http_connection conn, const char *code, const char *contentType, const char *content, int size) 397 | { 398 | if (size < 0) 399 | size = strlen(content); 400 | 401 | int done = snprintf(conn->buffer, conn->server->buffer_size, "HTTP/1.0 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n", code, contentType, size); 402 | send_all(conn->socket, conn->buffer, done); 403 | send_all(conn->socket, content, size); 404 | } 405 | 406 | http_write_handle http_server_begin_write_reply(http_connection conn, const char *code, const char *contentType) 407 | { 408 | conn->buffered_size = snprintf(conn->buffer, conn->server->buffer_size, "HTTP/1.0 %s\r\nContent-Type: %s\r\nConnection: close\r\n\r\n", code, contentType); 409 | return (http_write_handle)conn; 410 | } 411 | 412 | void http_server_write_reply(http_write_handle handle, const char *format, ...) 413 | { 414 | http_connection conn = (http_connection)handle; 415 | va_list args; 416 | va_start(args, format); 417 | int written = vsnprintf(conn->buffer + conn->buffered_size, conn->server->buffer_size - conn->buffered_size, format, args); 418 | va_end(args); 419 | if ((conn->buffered_size + written) < (conn->server->buffer_size - 16)) 420 | { 421 | conn->buffered_size += written; 422 | return; 423 | } 424 | 425 | send_all(conn->socket, conn->buffer, conn->buffered_size); 426 | va_start(args, format); 427 | conn->buffered_size = vsnprintf(conn->buffer, conn->server->buffer_size, format, args); 428 | va_end(args); 429 | } 430 | 431 | void http_server_end_write_reply(http_write_handle handle, const char *footer) 432 | { 433 | http_connection conn = (http_connection)handle; 434 | int len = footer ? strlen(footer) : 0; 435 | if (len && len < (conn->server->buffer_size - conn->buffered_size)) 436 | { 437 | memcpy(conn->buffer + conn->buffered_size, footer, len); 438 | conn->buffered_size += len; 439 | len = 0; 440 | } 441 | 442 | if (conn->buffered_size) 443 | send_all(conn->socket, conn->buffer, conn->buffered_size); 444 | 445 | if (len) 446 | send_all(conn->socket, footer, len); 447 | 448 | conn->buffered_size = 0; 449 | } 450 | 451 | char *http_server_read_post_line(http_connection conn) 452 | { 453 | if (conn->post.remaining_input_len <= 0 && conn->post.buffer_pos >= conn->post.buffer_used) 454 | return NULL; 455 | 456 | int len = 0; 457 | char *result = recv_next_line_buffered(conn->socket, 458 | conn->buffer + conn->post.offset_from_main_buffer, 459 | conn->server->buffer_size - conn->post.offset_from_main_buffer, 460 | &conn->post.buffer_used, 461 | &conn->post.buffer_pos, 462 | &len, 463 | &conn->post.remaining_input_len); 464 | 465 | if (!result) 466 | return NULL; 467 | 468 | if (len < 0) 469 | return NULL; //Too long line got truncated 470 | 471 | result[len] = 0; 472 | 473 | return result; 474 | } 475 | -------------------------------------------------------------------------------- /PicoHTTPServer/httpserver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef struct _http_server_instance *http_server_instance; 4 | typedef struct _http_connection *http_connection, *http_write_handle; 5 | 6 | enum http_request_type 7 | { 8 | HTTP_GET = 0, 9 | HTTP_POST = 1, 10 | }; 11 | 12 | typedef bool(*http_request_handler)(http_connection conn, enum http_request_type type, char *path, void *context); 13 | 14 | typedef struct http_zone 15 | { 16 | const char *prefix; 17 | http_request_handler handler; 18 | void *context; 19 | struct http_zone *next; 20 | int prefix_len; 21 | } http_zone; 22 | 23 | 24 | http_server_instance http_server_create(const char *main_host, const char *main_domain, int max_thread_count, int buffer_size); 25 | void http_server_add_zone(http_server_instance server, http_zone *instance, const char *prefix, http_request_handler handler, void *context); 26 | void http_server_send_reply(http_connection conn, const char *code, const char *contentType, const char *content, int size); 27 | 28 | /* Reads a single line from the POST request using the internal connection buffer. Returns NULL when the entire request has been read. */ 29 | char *http_server_read_post_line(http_connection conn); 30 | 31 | 32 | http_write_handle http_server_begin_write_reply(http_connection conn, const char *code, const char *contentType); 33 | void http_server_write_reply(http_write_handle handle, const char *format, ...); 34 | void http_server_end_write_reply(http_write_handle handle, const char *footer); 35 | -------------------------------------------------------------------------------- /PicoHTTPServer/lwipopts.h: -------------------------------------------------------------------------------- 1 | #ifndef _LWIPOPTS_H 2 | #define _LWIPOPTS_H 3 | 4 | // Generally you would define your own explicit list of lwIP options 5 | // (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html) 6 | // 7 | // This example uses a common include to avoid repetition 8 | #include "lwipopts_examples_common.h" 9 | 10 | #if !NO_SYS 11 | #define TCPIP_THREAD_STACKSIZE 2048 12 | #define DEFAULT_THREAD_STACKSIZE 1024 13 | #define DEFAULT_RAW_RECVMBOX_SIZE 8 14 | #define TCPIP_MBOX_SIZE 8 15 | 16 | #define DEFAULT_UDP_RECVMBOX_SIZE TCPIP_MBOX_SIZE 17 | #define DEFAULT_TCP_RECVMBOX_SIZE TCPIP_MBOX_SIZE 18 | #define DEFAULT_ACCEPTMBOX_SIZE TCPIP_MBOX_SIZE 19 | #define MEMP_NUM_NETCONN (TCPIP_MBOX_SIZE * 2) 20 | 21 | #define LWIP_TIMEVAL_PRIVATE 0 22 | 23 | // not necessary, can be done either way 24 | #define LWIP_TCPIP_CORE_LOCKING_INPUT 1 25 | #endif 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /PicoHTTPServer/lwipopts_examples_common.h: -------------------------------------------------------------------------------- 1 | #ifndef _LWIPOPTS_EXAMPLE_COMMONH_H 2 | #define _LWIPOPTS_EXAMPLE_COMMONH_H 3 | 4 | 5 | // Common settings used in most of the pico_w examples 6 | // (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details) 7 | 8 | // allow override in some examples 9 | #ifndef NO_SYS 10 | #define NO_SYS 1 11 | #endif 12 | // allow override in some examples 13 | #ifndef LWIP_SOCKET 14 | #define LWIP_SOCKET 1 15 | #endif 16 | #if PICO_CYW43_ARCH_POLL 17 | #define MEM_LIBC_MALLOC 1 18 | #else 19 | // MEM_LIBC_MALLOC is incompatible with non polling versions 20 | #define MEM_LIBC_MALLOC 0 21 | #endif 22 | #define MEM_ALIGNMENT 4 23 | #define MEM_SIZE 16384 24 | #define MEMP_NUM_TCP_SEG 32 25 | #define MEMP_NUM_ARP_QUEUE 10 26 | #define PBUF_POOL_SIZE 24 27 | #define LWIP_ARP 1 28 | #define LWIP_ETHERNET 1 29 | #define LWIP_ICMP 1 30 | #define LWIP_RAW 1 31 | #define TCP_WND (8 * TCP_MSS) 32 | #define TCP_MSS 1460 33 | #define TCP_SND_BUF (8 * TCP_MSS) 34 | #define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) 35 | #define LWIP_NETIF_STATUS_CALLBACK 1 36 | #define LWIP_NETIF_LINK_CALLBACK 1 37 | #define LWIP_NETIF_HOSTNAME 1 38 | #define LWIP_NETCONN 0 39 | #define MEM_STATS 0 40 | #define SYS_STATS 0 41 | #define MEMP_STATS 0 42 | #define LINK_STATS 0 43 | // #define ETH_PAD_SIZE 2 44 | #define LWIP_CHKSUM_ALGORITHM 3 45 | #define LWIP_DHCP 1 46 | #define LWIP_IPV4 1 47 | #define LWIP_TCP 1 48 | #define LWIP_UDP 1 49 | #define LWIP_DNS 1 50 | #define LWIP_TCP_KEEPALIVE 1 51 | #define LWIP_NETIF_TX_SINGLE_PBUF 1 52 | #define DHCP_DOES_ARP_CHECK 0 53 | #define LWIP_DHCP_DOES_ACD_CHECK 0 54 | 55 | #ifndef NDEBUG 56 | #define LWIP_DEBUG 1 57 | #define LWIP_STATS 1 58 | #define LWIP_STATS_DISPLAY 1 59 | #endif 60 | 61 | #define ETHARP_DEBUG LWIP_DBG_OFF 62 | #define NETIF_DEBUG LWIP_DBG_OFF 63 | #define PBUF_DEBUG LWIP_DBG_OFF 64 | #define API_LIB_DEBUG LWIP_DBG_OFF 65 | #define API_MSG_DEBUG LWIP_DBG_OFF 66 | #define SOCKETS_DEBUG LWIP_DBG_OFF 67 | #define ICMP_DEBUG LWIP_DBG_OFF 68 | #define INET_DEBUG LWIP_DBG_OFF 69 | #define IP_DEBUG LWIP_DBG_OFF 70 | #define IP_REASS_DEBUG LWIP_DBG_OFF 71 | #define RAW_DEBUG LWIP_DBG_OFF 72 | #define MEM_DEBUG LWIP_DBG_OFF 73 | #define MEMP_DEBUG LWIP_DBG_OFF 74 | #define SYS_DEBUG LWIP_DBG_OFF 75 | #define TCP_DEBUG LWIP_DBG_OFF 76 | #define TCP_INPUT_DEBUG LWIP_DBG_OFF 77 | #define TCP_OUTPUT_DEBUG LWIP_DBG_OFF 78 | #define TCP_RTO_DEBUG LWIP_DBG_OFF 79 | #define TCP_CWND_DEBUG LWIP_DBG_OFF 80 | #define TCP_WND_DEBUG LWIP_DBG_OFF 81 | #define TCP_FR_DEBUG LWIP_DBG_OFF 82 | #define TCP_QLEN_DEBUG LWIP_DBG_OFF 83 | #define TCP_RST_DEBUG LWIP_DBG_OFF 84 | #define UDP_DEBUG LWIP_DBG_OFF 85 | #define TCPIP_DEBUG LWIP_DBG_OFF 86 | #define PPP_DEBUG LWIP_DBG_OFF 87 | #define SLIP_DEBUG LWIP_DBG_OFF 88 | #define DHCP_DEBUG LWIP_DBG_OFF 89 | 90 | #endif /* __LWIPOPTS_H__ */ 91 | -------------------------------------------------------------------------------- /PicoHTTPServer/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "dhcpserver/dhcpserver.h" 14 | #include "dns/dnsserver.h" 15 | #include "server_settings.h" 16 | #include "httpserver.h" 17 | #include "../tools/SimpleFSBuilder/SimpleFS.h" 18 | 19 | #define TEST_TASK_PRIORITY (tskIDLE_PRIORITY + 2UL) 20 | 21 | struct SimpleFSContext 22 | { 23 | GlobalFSHeader *header; 24 | StoredFileEntry *entries; 25 | char *names, *data; 26 | } s_SimpleFS; 27 | 28 | bool simplefs_init(struct SimpleFSContext *ctx, void *data) 29 | { 30 | ctx->header = (GlobalFSHeader *)data; 31 | if (ctx->header->Magic != kSimpleFSHeaderMagic) 32 | return false; 33 | ctx->entries = (StoredFileEntry *)(ctx->header + 1); 34 | ctx->names = (char *)(ctx->entries + ctx->header->EntryCount); 35 | ctx->data = (char *)(ctx->names + ctx->header->NameBlockSize); 36 | return true; 37 | } 38 | 39 | static bool do_retrieve_file(http_connection conn, enum http_request_type type, char *path, void *context) 40 | { 41 | for (int i = 0; i < s_SimpleFS.header->EntryCount; i++) 42 | { 43 | if (!strcmp(s_SimpleFS.names + s_SimpleFS.entries[i].NameOffset, path)) 44 | { 45 | http_server_send_reply(conn, 46 | "200 OK", 47 | s_SimpleFS.names + s_SimpleFS.entries[i].ContentTypeOffset, 48 | s_SimpleFS.data + s_SimpleFS.entries[i].DataOffset, 49 | s_SimpleFS.entries[i].FileSize); 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | 57 | static char *parse_server_settings(http_connection conn, pico_server_settings *settings) 58 | { 59 | bool has_password = false, use_domain = false, use_second_ip = false; 60 | bool bad_password = false, bad_domain = false; 61 | 62 | for (;;) 63 | { 64 | char *line = http_server_read_post_line(conn); 65 | if (!line) 66 | break; 67 | 68 | char *p = strchr(line, '='); 69 | if (!p) 70 | continue; 71 | *p++ = 0; 72 | if (!strcasecmp(line, "has_password")) 73 | has_password = !strcasecmp(p, "true") || p[0] == '1'; 74 | else if (!strcasecmp(line, "use_domain")) 75 | use_domain = !strcasecmp(p, "true") || p[0] == '1'; 76 | else if (!strcasecmp(line, "use_second_ip")) 77 | use_second_ip = !strcasecmp(p, "true") || p[0] == '1'; 78 | else if (!strcasecmp(line, "dns_ignores_network_suffix")) 79 | settings->dns_ignores_network_suffix = !strcasecmp(p, "true") || p[0] == '1'; 80 | else if (!strcasecmp(line, "ssid")) 81 | { 82 | if (strlen(p) >= sizeof(settings->network_name)) 83 | return "SSID too long"; 84 | if (!p[0]) 85 | return "missing SSID"; 86 | strcpy(settings->network_name, p); 87 | } 88 | else if (!strcasecmp(line, "password")) 89 | { 90 | if (strlen(p) >= sizeof(settings->network_password)) 91 | bad_password = true; 92 | else 93 | strcpy(settings->network_password, p); 94 | } 95 | else if (!strcasecmp(line, "hostname")) 96 | { 97 | if (strlen(p) >= sizeof(settings->hostname)) 98 | return "hostname too long"; 99 | if (!p[0]) 100 | return "missing hostname"; 101 | strcpy(settings->hostname, p); 102 | } 103 | else if (!strcasecmp(line, "domain")) 104 | { 105 | if (strlen(p) >= sizeof(settings->domain_name)) 106 | bad_domain = true; 107 | else 108 | strcpy(settings->domain_name, p); 109 | } 110 | else if (!strcasecmp(line, "ipaddr")) 111 | { 112 | settings->ip_address = ipaddr_addr(p); 113 | if (!settings->ip_address || settings->ip_address == -1) 114 | return "invalid IP address"; 115 | } 116 | else if (!strcasecmp(line, "netmask")) 117 | { 118 | settings->network_mask = ipaddr_addr(p); 119 | if (!settings->network_mask || settings->network_mask == -1) 120 | return "invalid network mask"; 121 | } 122 | else if (!strcasecmp(line, "ipaddr2")) 123 | { 124 | settings->secondary_address = ipaddr_addr(p); 125 | } 126 | } 127 | 128 | if (!has_password) 129 | memset(settings->network_password, 0, sizeof(settings->network_password)); 130 | else if (bad_password) 131 | return "password too long"; 132 | 133 | if (!use_domain) 134 | memset(settings->domain_name, 0, sizeof(settings->domain_name)); 135 | else if (bad_domain) 136 | return "domain too long"; 137 | 138 | if (!use_second_ip) 139 | settings->secondary_address = 0; 140 | else if (!settings->secondary_address || settings->secondary_address == -1) 141 | return "invalid secondary IP address"; 142 | 143 | return NULL; 144 | } 145 | 146 | static bool do_handle_api_call(http_connection conn, enum http_request_type type, char *path, void *context) 147 | { 148 | static int s_InitializedMask = 0; 149 | 150 | if (!strcmp(path, "readpins")) 151 | { 152 | http_write_handle reply = http_server_begin_write_reply(conn, "200 OK", "text/json"); 153 | http_server_write_reply(reply, "{\"led0v\": \"%d\"", cyw43_arch_gpio_get(0)); 154 | 155 | int values = gpio_get_all(); 156 | 157 | for (int i = 0; i < 29; i++) 158 | { 159 | if (i > 22 && i < 26) 160 | continue; 161 | 162 | if (s_InitializedMask & (1 << i)) 163 | http_server_write_reply(reply, ",\"gpio%dd\": \"%s\",\"gpio%dv\": \"%d\"", i, gpio_get_dir(i) ? "OUT" : "IN", i, (values >> i) & 1); 164 | } 165 | 166 | http_server_end_write_reply(reply, "}"); 167 | return true; 168 | } 169 | else if (!memcmp(path, "writepin/", 9)) 170 | { 171 | //e.g. 'writepin/led0?v=1' 172 | char *port = path + 9; 173 | char *arg = strchr(port, '?'); 174 | if (arg) 175 | { 176 | *arg++ = 0; 177 | char *value = strchr(arg, '='); 178 | *value++ = 0; 179 | 180 | if (!strcmp(port, "led0")) 181 | cyw43_arch_gpio_put(0, value[0] == '1'); 182 | else if (!memcmp(port, "gpio", 4)) 183 | { 184 | int gpio = atoi(port + 4); 185 | if (!(s_InitializedMask & (1 << gpio))) 186 | { 187 | gpio_init(gpio); 188 | s_InitializedMask |= (1 << gpio); 189 | } 190 | 191 | if (arg[0] == 'd' && value[0] == 'I') 192 | { 193 | gpio_set_pulls(gpio, true, false); 194 | gpio_set_dir(gpio, GPIO_IN); 195 | } 196 | else 197 | { 198 | gpio_set_pulls(gpio, false, false); 199 | gpio_set_dir(gpio, GPIO_OUT); 200 | 201 | if (arg[0] == 'v') 202 | gpio_put(gpio, value[0] == '1'); 203 | } 204 | } 205 | 206 | return true; 207 | } 208 | } 209 | else if (!strcmp(path, "settings")) 210 | { 211 | if (type == HTTP_POST) 212 | { 213 | static pico_server_settings settings; 214 | settings = *get_pico_server_settings(); 215 | 216 | char *err = parse_server_settings(conn, &settings); 217 | if (err) 218 | { 219 | http_server_send_reply(conn, "200 OK", "text/plain", err, -1); 220 | return true; 221 | } 222 | 223 | write_pico_server_settings(&settings); 224 | http_server_send_reply(conn, "200 OK", "text/plain", "OK", -1); 225 | watchdog_reboot(0, SRAM_END, 500); 226 | return true; 227 | } 228 | else 229 | { 230 | const pico_server_settings *settings = get_pico_server_settings(); 231 | http_write_handle reply = http_server_begin_write_reply(conn, "200 OK", "text/json"); 232 | http_server_write_reply(reply, "{\"ssid\": \"%s\"", settings->network_name); 233 | http_server_write_reply(reply, ",\"has_password\": %d, \"password\" : \"%s\"", settings->network_password[0] != 0, settings->network_password); 234 | http_server_write_reply(reply, ",\"hostname\" : \"%s\"", settings->hostname); 235 | http_server_write_reply(reply, ",\"use_domain\": %d, \"domain\" : \"%s\"", settings->domain_name[0] != 0, settings->domain_name); 236 | http_server_write_reply(reply, ",\"ipaddr\" : \"%d.%d.%d.%d\"", (settings->ip_address >> 0) & 0xFF, (settings->ip_address >> 8) & 0xFF, (settings->ip_address >> 16) & 0xFF, (settings->ip_address >> 24) & 0xFF); 237 | http_server_write_reply(reply, ",\"netmask\" : \"%d.%d.%d.%d\"", (settings->network_mask >> 0) & 0xFF, (settings->network_mask >> 8) & 0xFF, (settings->network_mask >> 16) & 0xFF, (settings->network_mask >> 24) & 0xFF); 238 | http_server_write_reply(reply, ",\"use_second_ip\": %d", settings->secondary_address != 0); 239 | http_server_write_reply(reply, ",\"ipaddr2\" : \"%d.%d.%d.%d\"", (settings->secondary_address >> 0) & 0xFF, (settings->secondary_address >> 8) & 0xFF, (settings->secondary_address >> 16) & 0xFF, (settings->secondary_address >> 24) & 0xFF); 240 | http_server_write_reply(reply, ",\"dns_ignores_network_suffix\" : %d", !!settings->dns_ignores_network_suffix); 241 | 242 | http_server_end_write_reply(reply, "}"); 243 | return true; 244 | } 245 | } 246 | 247 | return false; 248 | } 249 | 250 | 251 | static void set_secondary_ip_address(int address) 252 | { 253 | /************************************ !!! WARNING !!! ************************************ 254 | * If you get an 'undefined reference to ip4_secondary_ip_address' error here, * 255 | * you need to patch your lwIP using the lwip_patch/lwip.patch file from this repository.* 256 | * This ensures that this device can pretend to be a router redirecting requests to * 257 | * external IPs to its login page, so the OS can automatically navigate there. * 258 | *****************************************************************************************/ 259 | 260 | extern int ip4_secondary_ip_address; 261 | ip4_secondary_ip_address = address; 262 | } 263 | 264 | static void main_task(__unused void *params) 265 | { 266 | 267 | if (cyw43_arch_init()) 268 | { 269 | printf("failed to initialise\n"); 270 | return; 271 | } 272 | 273 | extern void *_binary_www_fs_start; 274 | if (!simplefs_init(&s_SimpleFS, &_binary_www_fs_start)) 275 | { 276 | printf("missing/corrupt FS image"); 277 | return; 278 | } 279 | 280 | const pico_server_settings *settings = get_pico_server_settings(); 281 | 282 | cyw43_arch_enable_ap_mode(settings->network_name, settings->network_password, settings->network_password[0] ? CYW43_AUTH_WPA2_MIXED_PSK : CYW43_AUTH_OPEN); 283 | 284 | struct netif *netif = netif_default; 285 | ip4_addr_t addr = { .addr = settings->ip_address }, mask = { .addr = settings->network_mask }; 286 | 287 | netif_set_addr(netif, &addr, &mask, &addr); 288 | 289 | // Start the dhcp server 290 | static dhcp_server_t dhcp_server; 291 | dhcp_server_init(&dhcp_server, &netif->ip_addr, &netif->netmask, settings->domain_name); 292 | dns_server_init(netif->ip_addr.addr, settings->secondary_address, settings->hostname, settings->domain_name, settings->dns_ignores_network_suffix); 293 | set_secondary_ip_address(settings->secondary_address); 294 | http_server_instance server = http_server_create(settings->hostname, settings->domain_name, 4, 4096); 295 | static http_zone zone1, zone2; 296 | http_server_add_zone(server, &zone1, "", do_retrieve_file, NULL); 297 | http_server_add_zone(server, &zone2, "/api", do_handle_api_call, NULL); 298 | vTaskDelete(NULL); 299 | } 300 | 301 | xSemaphoreHandle s_PrintfSemaphore; 302 | 303 | void debug_printf(const char *format, ...) 304 | { 305 | va_list args; 306 | va_start(args, format); 307 | xSemaphoreTake(s_PrintfSemaphore, portMAX_DELAY); 308 | vprintf(format, args); 309 | va_end(args); 310 | xSemaphoreGive(s_PrintfSemaphore); 311 | } 312 | 313 | void debug_write(const void *data, int size) 314 | { 315 | xSemaphoreTake(s_PrintfSemaphore, portMAX_DELAY); 316 | _write(1, data, size); 317 | xSemaphoreGive(s_PrintfSemaphore); 318 | } 319 | 320 | int main(void) 321 | { 322 | stdio_init_all(); 323 | TaskHandle_t task; 324 | s_PrintfSemaphore = xSemaphoreCreateMutex(); 325 | xTaskCreate(main_task, "MainThread", configMINIMAL_STACK_SIZE, NULL, TEST_TASK_PRIORITY, &task); 326 | vTaskStartScheduler(); 327 | } -------------------------------------------------------------------------------- /PicoHTTPServer/server_settings.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "server_settings.h" 3 | #include 4 | #include "hardware/flash.h" 5 | 6 | const union 7 | { 8 | pico_server_settings settings; 9 | char padding[FLASH_SECTOR_SIZE]; 10 | } __attribute__((aligned(FLASH_SECTOR_SIZE))) s_Settings = { 11 | .settings = { 12 | .ip_address = 0x017BA8C0, 13 | .network_mask = 0x00FFFFFF, 14 | .secondary_address = 0x006433c6, //TEST-NET-2. See the comment before 'secondary_address' definition for details. 15 | .network_name = WIFI_SSID, 16 | .network_password = WIFI_PASSWORD, 17 | .hostname = "picohttp", 18 | .domain_name = "piconet.local", 19 | .dns_ignores_network_suffix = true, 20 | } 21 | }; 22 | 23 | 24 | const pico_server_settings *get_pico_server_settings() 25 | { 26 | return &s_Settings.settings; 27 | } 28 | 29 | void write_pico_server_settings(const pico_server_settings *new_settings) 30 | { 31 | portENTER_CRITICAL(); 32 | flash_range_erase((uint32_t)&s_Settings - XIP_BASE, FLASH_SECTOR_SIZE); 33 | flash_range_program((uint32_t)&s_Settings - XIP_BASE, (const uint8_t *)new_settings, sizeof(*new_settings)); 34 | portEXIT_CRITICAL(); 35 | } 36 | 37 | 38 | const char *get_next_domain_name_component(const char *domain_name, int *position, int *length) 39 | { 40 | if (!domain_name || !position || !length) 41 | return NULL; 42 | 43 | int pos = *position; 44 | const char *p = strchr(domain_name + pos, '.'); 45 | if (p) 46 | { 47 | *position = p + 1 - domain_name; 48 | *length = p - domain_name - pos; 49 | return domain_name + pos; 50 | } 51 | else if (domain_name[pos]) 52 | { 53 | *length = strlen(domain_name + pos); 54 | *position = pos + *length; 55 | return domain_name + pos; 56 | } 57 | else 58 | return NULL; 59 | } 60 | 61 | -------------------------------------------------------------------------------- /PicoHTTPServer/server_settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct 6 | { 7 | uint32_t ip_address; 8 | uint32_t network_mask; 9 | /* The secondary IP address is needed to support the "sign into network" mechanism. 10 | * Modern OSes will automatically show the 'sign into network' page if: 11 | * 1. The network has valid DHCP/DNS servers 12 | * 2. The DNS server resolves requests to test names to valid EXTERNAL IPs (not 192.168.x.y) 13 | * 3. Issuing a HTTP GET request to the external IP results in a HTTP 302 redirect to the login page. 14 | * 15 | * E.g. see https://cs.android.com/android/platform/superproject/+/master:packages/modules/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java, 16 | * specifically the isDnsPrivateIpResponse() check and the "DNS response to the URL is private IP" error. 17 | */ 18 | uint32_t secondary_address; 19 | char network_name[32]; 20 | char network_password[32]; 21 | char hostname[32]; 22 | char domain_name[32]; 23 | uint32_t dns_ignores_network_suffix; 24 | } pico_server_settings; 25 | 26 | const pico_server_settings *get_pico_server_settings(); 27 | void write_pico_server_settings(const pico_server_settings *new_settings); 28 | 29 | const char *get_next_domain_name_component(const char *domain_name, int *position, int *length); 30 | -------------------------------------------------------------------------------- /PicoHTTPServer/www/img/configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysprogs/PicoHTTPServer/0fa1b98fc8998e64a96031b4421b826dcecde237/PicoHTTPServer/www/img/configure.png -------------------------------------------------------------------------------- /PicoHTTPServer/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Raspberry Pi Pico W Demo 4 | 211 | 356 | 357 | 358 | 359 | 360 |
361 |
362 |

Raspberry Pi Pico W Demo

363 |

Use the selectors below to change the modes and values of GPIO pins:

364 |
365 |
366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 |
GPIO0
1
GPIO1
2
GND
3
GPIO2
4
GPIO3
5
GPIO4
6
GPIO5
7
GND
8
GPIO6
9
GPIO7
10
GPIO8
11
GPIO9
12
GND
13
GPIO10
14
GPIO11
15
GPIO12
16
GPIO13
17
GND
18
GPIO14
19
GPIO15
20
388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 |
40
VBUS
39
VSYS
38
GND
37
3V3_EN
36
3V3_OUT
35
ADC_VREF
34
GPIO28
33
GND
32
GPIO27
31
GPIO26
30
RUN
29
GPIO22
28
GND
27
GPIO21
26
GPIO20
25
GPIO19
24
GPIO18
23
GND
22
GPIO17
21
GPIO16
413 |
414 |
415 | 416 | 417 | 418 |
Built-in LED state:
419 |
420 | 421 | 445 | 446 | 449 | 450 | 453 | 454 |
455 | 456 | 457 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](https://img.shields.io/github/license/sysprogs/PicoHTTPServer?style=flat-square)](https://github.com/sysprogs/PicoHTTPServer) 2 | 3 | # PicoHTTPServer: Host responsive Web Apps on Raspberry Pi Pico W 4 | 5 | This project turns your Raspberry Pi Pico W into a Wi-Fi access-point. Connecting to it from your computer, tablet or phone will automatically redirect you to a page allowing to control individual pins of the board: 6 | 7 | ![Pin control page](https://github.com/sysprogs/PicoHTTPServer/raw/master/screenshots/01-pins.png) 8 | 9 | Simply click on the **IN** label to use a pin as an input (0/1 will be automatically updated every 500ms), or click on **OUT** to turn it into an output and then click on **0** or **1** to control it. 10 | 11 | ## Demonstrated Technologies 12 | 13 | This project demonstrates how to use Raspberry Pi Pico W to host a web app with responsive CSS, a simple API, modal dialogs and a zero-configuration setup that allows users to open the app by connecting to the Wi-Fi network without having to enter any host names or IP addresses. The project is licensed under the MIT license and can be used as a template to create web apps for managing the configuration of your IoT devices. 14 | 15 | The project demonstrates the following technologies: 16 | 17 | - The use of DHCP, DNS and HTTP redirects to automatically show the app for all Wi-Fi clients 18 | - A memory-efficient HTTP server capable of handling large requests with very little RAM 19 | - A simple file system for storing multiple files (HTML, images) in the FLASH memory and serving them via HTTP 20 | - A simple CSS layout sufficient to scale the page depending on the browser window size 21 | - A simple API showing how to fetch meaningful data directly from the C code and present it in a meaningful way 22 | 23 | ### "Sign into Network" Message 24 | 25 | This feature relies on the mechanism used by public networks (e.g. airport/hotel) to authenticate users, or make them accept the terms and conditions. When a client connects to such a network, it typically: 26 | 27 | 1. Assigns the client a private DHCP address (e.g. 192.168.0.100) and points it to a local DNS server. 28 | 2. When the client tries to resolve a hostname (e.g. `sysprogs.com`, it is resolved to the correct IP address). 29 | 3. When the client tries to connect to that address, the router intercepts the request and issues an HTTP 302 redirect to the login page. 30 | 31 | Most modern browsers and operating systems support this behavior out-of-the-box by trying to connect to a test URL (e.g. http://www.msftconnecttest.com/redirect). If the connection results in a HTTP redirect, the operating system suggests opening the browser to login: 32 | 33 | ![Automatic sign-into-network message](https://github.com/sysprogs/PicoHTTPServer/raw/master/screenshots/02-login.png) 34 | 35 | This mechanism is known as [Captive Portals](https://en.wikipedia.org/wiki/Captive_portal) and is implemented by PicoHTTPServer as follows: 36 | 37 | 1. The [DHCP server](https://github.com/sysprogs/PicoHTTPServer/blob/master/PicoHTTPServer/dhcpserver/dhcpserver.c) (taken from the [access point](https://github.com/raspberrypi/pico-examples/tree/master/pico_w/access_point/dhcpserver) example) issues the clients IP addresses from the pre-configured subnet, and reports itself as the gateway and DNS server (see `DHCP_OPT_ROUTER` and `DHCP_OPT_DNS`). 38 | 39 | 2. The [DNS server](https://github.com/sysprogs/PicoHTTPServer/blob/master/PicoHTTPServer/dns/dnsserver.c) compares the requested domain names to the configured name of the Pico W (e.g. `picohttp.piconet.local`). If the name matches, it returns the IP address of the Pico W itself. Note that if we return any private address when the client OS is testing the connection (e.g. resolving http://www.msftconnecttest.com/redirect), most OSes will conclude that it's a private network and won't show the login prompt. To work around it, we resolve all hostnames that don't match our own name to the **secondary IP** (the default configuration sets it to [TEST-NET-2](https://en.wikipedia.org/wiki/Reserved_IP_addresses) that is 198.51.100.0). 40 | 41 | 3. In order to answer requests to the **secondary IP**, we use a [patched version of lwIP](https://github.com/sysprogs/PicoHTTPServer/blob/master/lwip_patch/lwip.patch) that it routes packets with this IP address to our netconn instance. From the client's perspective, this is similar to a network router. 42 | 43 | 4. Finally, the HTTP server checks the `Host` field in the HTTP request. If the request came for our hostname (e.g. `picohttp.piconet.local`), it is handled normally. If not, it issues an `HTTP/1.0 302 Found` redirect pointing to the primary hostname. 44 | 45 | 46 | ### A Memory-efficient HTTP Server 47 | 48 | As the Raspberry Pi Pico W only has 256KB of RAM, this project uses its own memory-efficient implementation of the [HTTP server](https://github.com/sysprogs/PicoHTTPServer/blob/master/PicoHTTPServer/httpserver.c) designed with the following constraints in mind: 49 | 50 | - The entire HTTP request never needs to fit into the RAM. The server (see `parse_and_handle_http_request()`) reads and parses the request using a small memory window (4KB by default). The window needs to be big enough to fit the first line (method + path) and the **longest** line from the request header that we want to parse. All request headers that don't fit into the window (e.g. a very long `User-Agent` field) will be safely skipped without interfering with the rest of the fields. 51 | 52 | - Likewise, the application logic can generate API responses using the printf()-style `http_server_write_reply()` function without having to fit the entire response into the buffer. As long as a single fragment formatted at once fits, the server will manage the buffering automatically. 53 | 54 | - All data sent by the web app via POST requests can be read line-by-line without having to fit the entire request in memory. 55 | 56 | This architecture allows handling HTTP requests at decent speeds with only 4KB/thread (+2KB default stack) that can be reduced further at some performance cost. 57 | 58 | ### A Simple File System 59 | 60 | In order to support images, styles or multiple pages, the HTTP server includes a tool packing the served content into a single file (along with the content type for each file). The file is then embedded into the image, and is programmed together with the rest of the firmware. You can easily add more files to the web server by simply putting them into the [www](https://github.com/sysprogs/PicoHTTPServer/tree/master/PicoHTTPServer/www) directory and rebuilding the project with CMake. 61 | 62 | You can dramatically reduce the FLASH utilization by the web server content by pre-compressing the files with gzip and returning the `Content-Encoding: gzip` header for the affected files. The decompression will happen on the browser side, without the need to include decompression code in the firmware. 63 | 64 | ### The Web App 65 | 66 | Raspberry Pi Pico W has 2MB of FLASH memory (~256KB of which are used by the Pico SDK), so it cannot fit a full-scale web framework, or a PHP interpreter. However, it can easily serve JavaScript that will execute in the browser, making calls to various APIs and updating the page accordingly. This is demonstrated by the settings editing popup: 67 | 68 | ![Settings editing GUI](https://github.com/sysprogs/PicoHTTPServer/raw/master/screenshots/03-settings.png) 69 | 70 | Whenever you click the 'settings' button in the browser, the following events take place: 71 | 72 | 1. The JavaScript uses the `XMLHttpRequest` interface to send a GET request to the `/api/settings` endpoint. The code in `do_handle_api_call` in `main.c` handles this request, formatting the settings as a JSON object using `http_server_write_reply()`. 73 | 2. The JavaScript parses the reply and sets the fields in the settings popup. Note that the JSON parsing is done in the browser, so the code running on Raspberry Pi Pico doesn't need to handle it. 74 | 3. When the user clicks the 'OK' button in the browser, the JavaScript formats the settings fields into a set of `key=value` lines and sends it as a POST request. 75 | 4. The code in `parse_server_settings()` reads and validates the values from the browser. Because the values are sent in plain text, the entire request doesn't need to fit into the memory at the same time. Instead, the code can read it line-by-line using `http_server_read_post_line()`. 76 | 77 | The settings are stored in the FLASH memory together with the firmware and the web pages, so they are preserved when you reboot the device. 78 | 79 | ## Building the App 80 | 81 | You can download the pre-built binary of the HTTP server from the [releases](https://github.com/sysprogs/PicoHTTPServer/releases) page. Simply boot your Raspberry Pi Pico W into the bootloader, and copy the **PicoHTTPServer.uf2** file on it. The Pico W will restart and create the **PicoHTTP** network. 82 | 83 | The easiest way to build the sources on Windows is to install [VisualGDB](https://visualgdb.com/) and open the `PicoHTTPServer.sln` file in Visual Studio. VisualGDB will automatically install the necessary toolchain/SDK and will manage build and debugging for you. 84 | 85 | You can also build the project manually by running the [build-all.sh](https://github.com/sysprogs/PicoHTTPServer/blob/master/build-all.sh) file. Make sure you have CMake and GNU Make installed, and that you have the ARM GCC (arm-none-eabi) in the PATH. 86 | 87 | ## Modifying the App 88 | 89 | See [this tutorial](https://visualgdb.com/tutorials/raspberry/pico_w/http/) for detailed step-by-step instructions on adding a new dialog and the corresponding API to the app, as well as testing it out on the hardware. -------------------------------------------------------------------------------- /build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p tools/SimpleFSBuilder/build 3 | mkdir -p PicoHTTPServer/build 4 | cmake -S tools/SimpleFSBuilder -B tools/SimpleFSBuilder/build 5 | make -C tools/SimpleFSBuilder/build || exit 1 6 | 7 | test -d pico-sdk || git clone --recursive https://github.com/raspberrypi/pico-sdk 8 | test -d pico-sdk/FreeRTOS || git clone --recursive https://github.com/FreeRTOS/FreeRTOS-Kernel pico-sdk/FreeRTOS 9 | grep -e ip4_secondary_ip_address pico-sdk/lib/lwip/src/core/ipv4/ip4.c || patch -p1 -d pico-sdk/lib/lwip < lwip_patch/lwip.patch || (echo "Failed to apply patch" && exit 1) 10 | 11 | 12 | cmake -S PicoHTTPServer -B PicoHTTPServer/build -DPICO_SDK_PATH=`pwd`/pico-sdk -DPICO_GCC_TRIPLE=arm-none-eabi -DPICO_BOARD=pico_w -DWIFI_SSID="PicoHTTP" -DWIFI_PASSWORD="" 13 | make -C PicoHTTPServer/build || exit 1 14 | -------------------------------------------------------------------------------- /lwip_patch/lwip.patch: -------------------------------------------------------------------------------- 1 | diff -urN lwip.orig/src/core/ipv4/ip4.c lwip/src/core/ipv4/ip4.c 2 | --- lwip.orig/src/core/ipv4/ip4.c 2022-09-05 12:52:54.621160700 -0700 3 | +++ lwip/src/core/ipv4/ip4.c 2022-09-05 12:52:30.156428000 -0700 4 | @@ -403,6 +403,8 @@ 5 | } 6 | #endif /* IP_FORWARD */ 7 | 8 | +int ip4_secondary_ip_address; 9 | + 10 | /** Return true if the current input packet should be accepted on this netif */ 11 | static int 12 | ip4_input_accept(struct netif *netif) 13 | @@ -419,6 +421,7 @@ 14 | if (ip4_addr_eq(ip4_current_dest_addr(), netif_ip4_addr(netif)) || 15 | /* or broadcast on this interface network address? */ 16 | ip4_addr_isbroadcast(ip4_current_dest_addr(), netif) 17 | + || (ip4_secondary_ip_address && ip4_current_dest_addr()->addr == ip4_secondary_ip_address) 18 | #if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF 19 | || (ip4_addr_get_u32(ip4_current_dest_addr()) == PP_HTONL(IPADDR_LOOPBACK)) 20 | #endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */ 21 | -------------------------------------------------------------------------------- /screenshots/01-pins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysprogs/PicoHTTPServer/0fa1b98fc8998e64a96031b4421b826dcecde237/screenshots/01-pins.png -------------------------------------------------------------------------------- /screenshots/02-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysprogs/PicoHTTPServer/0fa1b98fc8998e64a96031b4421b826dcecde237/screenshots/02-login.png -------------------------------------------------------------------------------- /screenshots/03-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysprogs/PicoHTTPServer/0fa1b98fc8998e64a96031b4421b826dcecde237/screenshots/03-settings.png -------------------------------------------------------------------------------- /tools/SimpleFSBuilder/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #Generated by VisualGDB project wizard. 2 | #Note: VisualGDB will automatically update this file when you add new sources to the project. 3 | 4 | cmake_minimum_required(VERSION 2.7) 5 | project(SimpleFSBuilder) 6 | add_executable(SimpleFSBuilder SimpleFSBuilder.cpp) 7 | set_property(TARGET SimpleFSBuilder PROPERTY CXX_STANDARD 17) 8 | target_link_libraries(SimpleFSBuilder -static -static-libgcc -static-libstdc++) 9 | -------------------------------------------------------------------------------- /tools/SimpleFSBuilder/SimpleFS.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef struct 4 | { 5 | uint32_t FileSize; 6 | uint32_t NameOffset; 7 | uint32_t ContentTypeOffset; 8 | uint32_t DataOffset; 9 | } StoredFileEntry; 10 | 11 | typedef struct 12 | { 13 | uint32_t Magic; 14 | uint32_t EntryCount; 15 | uint32_t NameBlockSize; 16 | uint32_t DataBlockSize; 17 | } GlobalFSHeader; 18 | 19 | enum 20 | { 21 | kSimpleFSHeaderMagic = '1SFS', 22 | }; 23 | -------------------------------------------------------------------------------- /tools/SimpleFSBuilder/SimpleFSBuilder.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "SimpleFS.h" 12 | 13 | using namespace std; 14 | using namespace std::filesystem; 15 | 16 | struct TemporaryFileEntry 17 | { 18 | string PathInArchive; 19 | string FullPath, Extension; 20 | uintmax_t Size; 21 | 22 | TemporaryFileEntry(const string &pathInArchive, const path &fullPath, uintmax_t size) 23 | : PathInArchive(pathInArchive), 24 | FullPath(fullPath.u8string()), 25 | Extension(fullPath.extension().u8string()), 26 | Size(size) 27 | { 28 | for (int i = 0; i < Extension.size(); i++) 29 | Extension[i] = tolower(Extension[i]); 30 | } 31 | }; 32 | 33 | static std::string CombinePaths(std::string left, const path &right) 34 | { 35 | if (!left.empty()) 36 | left += "/"; 37 | 38 | left += right.u8string(); 39 | return left; 40 | } 41 | 42 | static void BuildFileListRecursively(path dir, std::list &entries, std::string pathBase) 43 | { 44 | for (const auto &entry : directory_iterator(dir)) 45 | { 46 | if (entry.is_directory()) 47 | BuildFileListRecursively(entry.path(), entries, CombinePaths(pathBase, entry.path().filename())); 48 | else 49 | { 50 | path fn = entry.path().filename(); 51 | if (!strcasecmp(fn.u8string().c_str(), "index.html")) 52 | fn = ""; 53 | 54 | entries.emplace_back(CombinePaths(pathBase, fn), entry.path(), entry.file_size()); 55 | } 56 | } 57 | 58 | } 59 | void WriteIfNotMatches(std::string fn, std::vector &data) 60 | { 61 | { 62 | std::ifstream fs(fn, ios::binary); 63 | std::vector tmp(data.size()); 64 | if (fs.readsome(tmp.data(), tmp.size()) == tmp.size()) 65 | { 66 | if (tmp == data) 67 | return; 68 | } 69 | } 70 | 71 | std::ofstream fs(fn, ios::binary | ios::trunc); 72 | fs.write(data.data(), data.size()); 73 | } 74 | 75 | struct ContentType 76 | { 77 | std::string Value; 78 | int Offset; 79 | 80 | ContentType(const char *value) 81 | : Value(value) 82 | { 83 | } 84 | }; 85 | 86 | int main(int argc, char *argv[]) 87 | { 88 | if (argc < 3) 89 | { 90 | cout << "Usage: SimpleFSBuilder " << endl; 91 | return 1; 92 | } 93 | 94 | try 95 | { 96 | std::list entries; 97 | BuildFileListRecursively(argv[1], entries, ""); 98 | GlobalFSHeader hdr = { kSimpleFSHeaderMagic, }; 99 | 100 | map contentTypes = { 101 | { ".txt", "text/plain" }, 102 | { ".htm", "text/html" }, 103 | { ".html", "text/html" }, 104 | { ".css", "text/css" }, 105 | { ".png", "image/png" }, 106 | { ".jpg", "image/jpeg" }, 107 | { ".svg", "image/svg+xml" }, 108 | }; 109 | 110 | for (const auto &entry : entries) 111 | { 112 | hdr.EntryCount++; 113 | hdr.NameBlockSize += entry.PathInArchive.size() + 1; 114 | hdr.DataBlockSize += entry.Size; 115 | } 116 | 117 | for (const auto &kv : contentTypes) 118 | hdr.NameBlockSize += kv.second.Value.size() + 1; 119 | 120 | std::vector buffer(sizeof(GlobalFSHeader) + hdr.EntryCount * sizeof(StoredFileEntry) + hdr.NameBlockSize + hdr.DataBlockSize); 121 | 122 | *((GlobalFSHeader *)buffer.data()) = hdr; 123 | StoredFileEntry *storedEntries = (StoredFileEntry *)(buffer.data() + sizeof(GlobalFSHeader)); 124 | char *names = (char *)(storedEntries + hdr.EntryCount); 125 | char *data = names + hdr.NameBlockSize; 126 | 127 | int i = 0, nameOff = 0, dataOff = 0; 128 | 129 | for (auto &kv : contentTypes) 130 | { 131 | kv.second.Offset = nameOff; 132 | memcpy(names + nameOff, kv.second.Value.c_str(), kv.second.Value.size() + 1); 133 | nameOff += kv.second.Value.size() + 1; 134 | } 135 | 136 | for (const auto &entry : entries) 137 | { 138 | storedEntries[i].FileSize = entry.Size; 139 | storedEntries[i].NameOffset = nameOff; 140 | storedEntries[i].DataOffset = dataOff; 141 | 142 | auto it = contentTypes.find(entry.Extension); 143 | if (it == contentTypes.end()) 144 | it = contentTypes.find(".html"); 145 | 146 | storedEntries[i].ContentTypeOffset = it->second.Offset; 147 | 148 | i++; 149 | 150 | ifstream ifs(entry.FullPath, ios::in | ios::binary); 151 | ifs.read(data + dataOff, entry.Size); 152 | 153 | memcpy(names + nameOff, entry.PathInArchive.c_str(), entry.PathInArchive.size() + 1); 154 | 155 | nameOff += entry.PathInArchive.size() + 1; 156 | dataOff += entry.Size; 157 | } 158 | 159 | if (nameOff != hdr.NameBlockSize) 160 | throw runtime_error("Unexpected name block size"); 161 | if (dataOff != hdr.DataBlockSize) 162 | throw runtime_error("Unexpected data block size"); 163 | if (i != hdr.EntryCount) 164 | throw runtime_error("Unexpected entry count"); 165 | 166 | WriteIfNotMatches(argv[2], buffer); 167 | return 0; 168 | } 169 | catch (exception &ex) 170 | { 171 | cout << ex.what() << endl; 172 | return 1; 173 | } 174 | } -------------------------------------------------------------------------------- /tools/SimpleFSBuilder/SimpleFSBuilder.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sysprogs/PicoHTTPServer/0fa1b98fc8998e64a96031b4421b826dcecde237/tools/SimpleFSBuilder/SimpleFSBuilder.exe -------------------------------------------------------------------------------- /tools/SimpleFSBuilder/SimpleFSBuilder.vgdbcmake: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Unknown 7 | 8 | true 9 | $(ProjectDir) 10 | 11 | 12 | 13 | 14 | com.visualgdb.mingw32 15 | 16 | 9.1.0 17 | 8.3 18 | 1 19 | 20 | 21 | 22 | DEBUG 23 | build/$(PlatformName)/$(ConfigurationName) 24 | 25 | false 26 | 27 | BuildMachine 28 | BuiltinShortcut 29 | 30 | $(VISUALGDB_DIR)/ninja.exe 31 | 32 | $(BuildDir) 33 | 34 | 35 | 36 | false 37 | 38 | BuildMachine 39 | BuiltinShortcut 40 | 41 | $(SYSPROGS_CMAKE_PATH) 42 | 43 | 44 | true 45 | false 46 | false 47 | Ninja 48 | false 49 | RemoveBuildDirectory 50 | false 51 | 52 | 53 | true 54 | true 55 | true 56 | false 57 | true 58 | false 59 | true 60 | HideOuterProjectTargets 61 | true 62 | false 63 | true 64 | 65 | 66 | 67 | SimpleFSBuilder 68 | 69 | E:\projects\sysprogs-github\PicoHTTPServer\PicoHTTPServer\PicoHTTPServer\www test.fs 70 | 71 | 72 | Auto 73 | false 74 | false 75 | 76 | Default 77 | 78 | 79 | 80 | true 81 | 74b44add-7ce6-4f55-929c-fe4c5cefb6a6 82 | 83 | Upper 84 | None 85 | true 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Default 100 | 101 | 102 | 103 | true 104 | 105 | 106 | 107 | 108 | Unknown 109 | 110 | true 111 | true 112 | true 113 | 114 | 115 | 116 | false 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | false 126 | false 127 | false 128 | false 129 | false 130 | false 131 | false 132 | false 133 | false 134 | 135 | false 136 | false 137 | false 138 | false 139 | false 140 | false 141 | true 142 | false 143 | None 144 | false 145 | false 146 | main 147 | true 148 | false 149 | false 150 | true 151 | 0 152 | false 153 | 0 154 | true 155 | false 156 | 157 | 158 | $(TargetPath) 159 | 2000 160 | $(SelectedCMakeTargetArgs) 161 | $(SelectedCMakeTargetLaunchDir) 162 | Auto 163 | 164 | false 165 | false 166 | 167 | --------------------------------------------------------------------------------