diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..3059ab744 --- /dev/null +++ b/.clang-format @@ -0,0 +1,118 @@ +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - list_for_each + - list_for_each_off + - list_for_each_rev + - list_for_each_safe + - list_for_each_safe_off + - LWAN_ARRAY_FOREACH + - LWAN_ARRAY_FOREACH_REVERSE + - LWAN_SECTION_FOREACH + - STRING_SWITCH + - STRING_SWITCH_L + - STRING_SWITCH_LARGE + - STRING_SWITCH_LARGE_L + - STRING_SWITCH_SMALL + - STRING_SWITCH_SMALL_L +IncludeCategories: + - Regex: '^"lwan-.*' + Priority: 2 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 8 +UseTab: Never +... + diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..d38101d81 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: lpereira diff --git a/.github/disabled-workflows/mayhem.yml b/.github/disabled-workflows/mayhem.yml new file mode 100644 index 000000000..d7f72dc34 --- /dev/null +++ b/.github/disabled-workflows/mayhem.yml @@ -0,0 +1,81 @@ +name: Mayhem +on: + push: + pull_request: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: '${{ matrix.os }} shared=${{ matrix.shared }} ${{ matrix.build_type }}' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + shared: [false] + build_type: [Release] + include: + - os: ubuntu-latest + triplet: x64-linux + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Log in to the Container registry + uses: docker/login-action@v2.1.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4.1.1 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v3.2.0 + with: + context: . + push: true + file: fuzz/mayhem/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + outputs: + image: ${{ steps.meta.outputs.tags }} + + mayhem: + needs: build + name: 'fuzz ${{ matrix.mayhemfile }}' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + mayhemfile: + - fuzz/mayhem/config_fuzzer.mayhemfile + - fuzz/mayhem/request_fuzzer.mayhemfile + - fuzz/mayhem/pattern_fuzzer.mayhemfile + - fuzz/mayhem/template_fuzzer.mayhemfile + - fuzz/mayhem/h2_huffman_fuzzer.mayhemfile + + steps: + - uses: actions/checkout@v3 + + - name: Start analysis for ${{ matrix.mayhemfile }} + uses: ForAllSecure/mcode-action@v1 + with: + mayhem-token: ${{ secrets.MAYHEM_TOKEN }} + args: --image ${{ needs.build.outputs.image }} --file ${{ matrix.mayhemfile }} --duration 300 + sarif-output: sarif + + - name: Upload SARIF file(s) + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: sarif diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 000000000..4a9bf1a3b --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,26 @@ +name: CIFuzz +on: [pull_request] +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'lwan' + dry-run: false + language: c++ + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'lwan' + fuzz-seconds: 300 + dry-run: false + language: c++ + - name: Upload Crash + uses: actions/upload-artifact@v3 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/.github/workflows/container-images.yml b/.github/workflows/container-images.yml new file mode 100644 index 000000000..47ff11306 --- /dev/null +++ b/.github/workflows/container-images.yml @@ -0,0 +1,40 @@ +name: Build and publish lwan container images + +on: + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Build image + id: build-image + uses: redhat-actions/buildah-build@v2 + with: + image: ${{ env.IMAGE_NAME }} + tags: latest ${{ github.event.release.tag_name }} + dockerfiles: | + ./Containerfile + + - name: Push to ghcr.io + id: push-to-ghcr + uses: redhat-actions/push-to-registry@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + image: ${{ steps.build-image.outputs.image }} + tags: ${{ steps.build-image.outputs.tags }} diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..ec27b915d --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +L. Pereira \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 03fbdad85..bdffa6b95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,113 +1,422 @@ +cmake_minimum_required(VERSION 3.20) project(lwan C) -cmake_minimum_required(VERSION 2.8) +set(PROJECT_DESCRIPTION "Scalable, high performance, experimental web server") +message(STATUS "Running CMake for ${PROJECT_NAME} (${PROJECT_DESCRIPTION})") + +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/src/cmake") + +include_directories(${CMAKE_BINARY_DIR}) + +include(CheckCCompilerFlag) +include(CheckCSourceCompiles) +include(CheckFunctionExists) +include(CheckSymbolExists) +include(CheckIncludeFile) +include(CheckIncludeFiles) +include(EnableCFlag) +include(FindPkgConfig) +include(TrySanitizer) +include(GNUInstallDirs) +include(GitVersionDetect) + + +message(STATUS "Lwan version ${GITVERSIONDETECT_VERSION}") +set(LWAN_VERSION ${GITVERSIONDETECT_VERSION}) if (NOT CMAKE_BUILD_TYPE) message(STATUS "No build type selected, defaulting to Debug") set(CMAKE_BUILD_TYPE "Debug") endif () +# +# Find libraries +# find_package(ZLIB REQUIRED) find_package(Threads REQUIRED) set(ADDITIONAL_LIBRARIES ${ZLIB_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) +if (NOT ZLIB_INCLUDES STREQUAL "") + include_directories(${ZLIB_INCLUDES}) +endif () - -include(CheckCSourceCompiles) -check_c_source_compiles("int main(void) { __builtin_cpu_init(); }" HAVE_BUILTIN_CPU_INIT) -if (HAVE_BUILTIN_CPU_INIT) - add_definitions("-DHAVE_BUILTIN_CPU_INIT") +foreach (pc_file luajit lua lua51 lua5.1 lua-5.1) + if (${pc_file} STREQUAL "luajit") + pkg_check_modules(LUA luajit>=2.0 luajit<2.2) + set(LWAN_HAVE_LUA_JIT 1) + else () + pkg_check_modules(LUA ${pc_file}>=5.1.0 ${pc_file}<=5.1.999) + endif () + if (LUA_FOUND) + list(APPEND ADDITIONAL_LIBRARIES "${LUA_LDFLAGS}") + include_directories(${LUA_INCLUDE_DIRS}) + break() + endif() +endforeach () +if (NOT LUA_FOUND) + message(STATUS "Disabling Lua support") +else () + message(STATUS "Building with Lua support using ${LUA_LIBRARIES}") + set(LWAN_HAVE_LUA 1) endif () -check_c_source_compiles("int main(void) { __builtin_clzll(0); }" HAVE_BUILTIN_CLZLL) -if (HAVE_BUILTIN_CLZLL) - add_definitions("-DHAVE_BUILTIN_CLZLL") + +option(ENABLE_BROTLI "Enable support for brotli" "ON") +if (ENABLE_BROTLI) + pkg_check_modules(BROTLI libbrotlienc libbrotlidec libbrotlicommon) endif () -check_c_source_compiles("int main(void) { unsigned long long p; (void)__builtin_mul_overflow(0, 0, &p); }" HAVE_BUILTIN_MUL_OVERFLOW) -if (HAVE_BUILTIN_MUL_OVERFLOW) - add_definitions("-DHAVE_BUILTIN_MUL_OVERFLOW") +if (BROTLI_FOUND) + list(APPEND ADDITIONAL_LIBRARIES "${BROTLI_LDFLAGS}") + if (NOT BROTLI_INCLUDE_DIRS STREQUAL "") + include_directories(${BROTLI_INCLUDE_DIRS}) + endif () + set(LWAN_HAVE_BROTLI 1) endif () -check_c_source_compiles("int main(void) { _Static_assert(1, \"\"); }" HAVE_STATIC_ASSERT) -if (HAVE_STATIC_ASSERT) - add_definitions("-DHAVE_STATIC_ASSERT") +option(ENABLE_ZSTD "Enable support for zstd" "ON") +if (ENABLE_ZSTD) + pkg_check_modules(ZSTD libzstd) +endif () +if (ZSTD_FOUND) + list(APPEND ADDITIONAL_LIBRARIES "${ZSTD_LDFLAGS}") + if (NOT ZSTD_INCLUDE_DIRS STREQUAL "") + include_directories(${ZSTD_INCLUDE_DIRS}) + endif () + set(LWAN_HAVE_ZSTD 1) endif () +option(ENABLE_TLS "Enable support for TLS (Linux-only)" "OFF") +if (ENABLE_TLS) + check_include_file(linux/tls.h LWAN_HAVE_LINUX_TLS_H) + if (LWAN_HAVE_LINUX_TLS_H) + # TLS support requires Linux, as Lwan uses the kTLS flavor + # only supported there. + # TODO: Try using BearSSL instead of mbedTLS and only link + # against things that are absolutely necessary to perform + # a TLS 1.2 handshake to inform the kernel about the keys. + find_library(MBEDTLS NAMES mbedtls) + if (NOT ${MBEDTLS} STREQUAL "MBEDTLS-NOTFOUND") + find_library(MBEDTLS_CRYPTO NAMES mbedcrypto REQUIRED) + find_library(MBEDTLS_X509 NAMES mbedx509 REQUIRED) -include(CheckFunctionExists) -set(CMAKE_EXTRA_INCLUDE_FILES time.h) -check_function_exists(clock_gettime HAS_CLOCK_GETTIME) -if (HAS_CLOCK_GETTIME) - message(STATUS "libc has clock_gettime(). Good.") -else () - list(APPEND ADDITIONAL_LIBRARIES rt) - message(STATUS "No clock_gettime() in libc. Linking with -lrt.") + message(STATUS "Building with Linux kTLS + mbedTLS at ${MBEDTLS}") + set(LWAN_HAVE_MBEDTLS 1) + list(APPEND ADDITIONAL_LIBRARIES ${MBEDTLS} ${MBEDTLS_CRYPTO} ${MBEDTLS_X509}) + else () + message(STATUS "mbedTLS not found: not building with TLS support") + endif () + else () + message(STATUS " not found: not building with TLS support") + endif () endif () +option(USE_ALTERNATIVE_MALLOC "Use alternative malloc implementations" "OFF") +if (USE_ALTERNATIVE_MALLOC) + unset(ALTMALLOC_LIBS CACHE) + unset(ALTMALLOC_LIBRARY CACHE) -find_library(TCMALLOC_LIBRARY NAMES tcmalloc_minimal tcmalloc) -if (TCMALLOC_LIBRARY) - message(STATUS "tcmalloc found: ${TCMALLOC_LIBRARY}") - list(APPEND ADDITIONAL_LIBRARIES ${TCMALLOC_LIBRARY}) -else () - find_library(JEMALLOC_LIBRARY NAMES jemalloc) - if (JEMALLOC_LIBRARY) - message(STATUS "jemalloc found: ${JEMALLOC_LIBRARY}") - list(APPEND ADDITIONAL_LIBRARIES ${JEMALLOC_LIBRARY}) + if (${USE_ALTERNATIVE_MALLOC} STREQUAL "mimalloc") + set(ALTMALLOC_LIBS mimalloc) + elseif (${USE_ALTERNATIVE_MALLOC} STREQUAL "tcmalloc") + set(ALTMALLOC_LIBS tcmalloc_minimal tcmalloc) + elseif (${USE_ALTERNATIVE_MALLOC} STREQUAL "jemalloc") + set(ALTMALLOC_LIBS jemalloc) else () - message(STATUS "jemalloc and tcmalloc were not found, using system malloc") + set(ALTMALLOC_LIBS "mimalloc tcmalloc_minimal tcmalloc jemalloc") endif() -endif() -set(C_FLAGS_REL "-mtune=native") - -include(CheckCCompilerFlag) -if (${CMAKE_BUILD_TYPE} MATCHES "Rel") - check_c_compiler_flag(-fno-asynchronous-unwind-tables HAS_ASYNC_UNWIND_TABLES) - if (HAS_ASYNC_UNWIND_TABLES) - set(C_FLAGS_REL "${CFLAGS_REL} -fno-asynchronous-unwind-tables") + find_library(ALTMALLOC_LIBRARY NAMES ${ALTMALLOC_LIBS}) + if (ALTMALLOC_LIBRARY) + message(STATUS "Using alternative malloc (${USE_ALTERNATIVE_MALLOC}): ${ALTMALLOC_LIBRARY}") + list(PREPEND ADDITIONAL_LIBRARIES ${ALTMALLOC_LIBRARY}) endif () - check_c_compiler_flag(-flto HAS_LTO) - if (HAS_LTO) - set(C_FLAGS_REL "${C_FLAGS_REL} -flto") +endif () - check_c_compiler_flag(-ffat-lto-objects HAS_FAT_LTO_OBJECTS) - if (HAS_FAT_LTO_OBJECTS) - set(C_FLAGS_REL "${C_FLAGS_REL} -ffat-lto-objects") - endif () - endif () +### +# syslog +option(USE_SYSLOG "Enable syslog" "OFF") +if (${USE_SYSLOG} STREQUAL "ON" AND LWAN_HAVE_SYSLOG_FUNC) + set(LWAN_HAVE_SYSLOG 1) +endif () +if (LWAN_HAVE_SYSLOG) + message(STATUS "Using syslog/rsyslog for logging.") +endif () - check_c_compiler_flag(-mcrc32 HAVE_BUILTIN_IA32_CRC32) - if (HAVE_BUILTIN_IA32_CRC32) - add_definitions("-DHAVE_BUILTIN_IA32_CRC32=1") - set(C_FLAGS_REL "${C_FLAGS_REL} -mcrc32") - endif () -else () - option(UBSAN "Build with undefined behavior sanitizer" ON) - option(ASAN "Build with address sanitizer" OFF) +# +# Look for C library functions +# - if (UBSAN) - set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=undefined") - elseif (ASAN) - set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=address") - endif () +set(CMAKE_EXTRA_INCLUDE_FILES + fcntl.h + stdlib.h + sys/socket.h + sys/types.h + string.h + time.h + unistd.h + dlfcn.h + syslog.h +) +check_include_file(linux/capability.h LWAN_HAVE_LINUX_CAPABILITY) +check_include_file(sys/auxv.h LWAN_HAVE_SYS_AUXV) +check_include_file(sys/epoll.h LWAN_HAVE_EPOLL) +check_include_files("sys/time.h;sys/types.h;sys/event.h" LWAN_HAVE_SYS_EVENT) +if (LWAN_HAVE_SYS_EVENT) + set(CMAKE_EXTRA_INCLUDE_FILES + ${CMAKE_EXTRA_INCLUDE_FILES} + sys/event.h sys/types.h sys/time.h + ) + check_function_exists(kqueue LWAN_HAVE_KQUEUE) + check_function_exists(kqueue1 LWAN_HAVE_KQUEUE1) +endif () +check_include_file(alloca.h LWAN_HAVE_ALLOCA_H) +if (LWAN_HAVE_SYS_AUXV) + set(CMAKE_EXTRA_INCLUDE_FILES + ${CMAKE_EXTRA_INCLUDE_FILES} + sys/auxv.h + ) + check_function_exists(getauxval LWAN_HAVE_GETAUXVAL) endif () +set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_THREAD_LIBS_INIT}) + +check_function_exists(get_current_dir_name LWAN_HAVE_GET_CURRENT_DIR_NAME) +check_symbol_exists(reallocarray stdlib.h LWAN_HAVE_REALLOCARRAY) +check_symbol_exists(eventfd sys/eventfd.h LWAN_HAVE_EVENTFD) +check_symbol_exists(mincore sys/mman.h LWAN_HAVE_MINCORE) +check_function_exists(mempcpy LWAN_HAVE_MEMPCPY) +check_function_exists(memrchr LWAN_HAVE_MEMRCHR) +check_function_exists(pipe2 LWAN_HAVE_PIPE2) +check_function_exists(accept4 LWAN_HAVE_ACCEPT4) +check_function_exists(readahead LWAN_HAVE_READAHEAD) +check_function_exists(mkostemp LWAN_HAVE_MKOSTEMP) +check_function_exists(clock_gettime LWAN_HAVE_CLOCK_GETTIME) +check_function_exists(pthread_barrier_init LWAN_HAVE_PTHREADBARRIER) +check_function_exists(pthread_set_name_np LWAN_HAVE_PTHREAD_SET_NAME_NP) +check_function_exists(posix_fadvise LWAN_HAVE_POSIX_FADVISE) +check_function_exists(getentropy LWAN_HAVE_GETENTROPY) +check_function_exists(fwrite_unlocked LWAN_HAVE_FWRITE_UNLOCKED) +check_function_exists(gettid LWAN_HAVE_GETTID) +check_function_exists(secure_getenv LWAN_HAVE_SECURE_GETENV) +check_function_exists(statfs LWAN_HAVE_STATFS) +check_function_exists(syslog LWAN_HAVE_SYSLOG_FUNC) +check_function_exists(stpcpy LWAN_HAVE_STPCPY) +# This is available on -ldl in glibc, but some systems (such as OpenBSD) +# will bundle these in the C library. This isn't required for glibc anyway, +# as there's getauxval(), with a fallback to reading the link +# /proc/self/exe. +check_function_exists(dladdr LWAN_HAVE_DLADDR) + +if (NOT LWAN_HAVE_CLOCK_GETTIME AND ${CMAKE_SYSTEM_NAME} MATCHES "Linux") + list(APPEND ADDITIONAL_LIBRARIES rt) +endif () + + +# +# Ensure compiler is compatible with GNU11 standard +# +check_c_compiler_flag(-std=gnu11 LWAN_HAVE_STD_GNU11) +if (NOT LWAN_HAVE_STD_GNU11) + message(FATAL_ERROR "Compiler does not support -std=gnu11. Consider using a newer compiler") +endif() + + +# +# Check for GCC builtin functions +# +check_c_source_compiles("__attribute__((access(read_only, 1))) int main(char *p) { return 0; }" LWAN_HAVE_ACCESS_ATTRIBUTE) +check_c_source_compiles("int main(void) { __builtin_cpu_init(); }" LWAN_HAVE_BUILTIN_CPU_INIT) +check_c_source_compiles("int main(void) { __builtin_expect_with_probability(0, 0, 0); }" LWAN_HAVE_BUILTIN_EXPECT_PROBABILITY) +check_c_source_compiles("int main(void) { __builtin_clzll(0); }" LWAN_HAVE_BUILTIN_CLZLL) +check_c_source_compiles("int main(void) { __builtin_fpclassify(0, 0, 0, 0, 0, 0.0f); }" LWAN_HAVE_BUILTIN_FPCLASSIFY) +check_c_source_compiles("int main(void) { unsigned long long p; (void)__builtin_mul_overflow(0, 0, &p); }" LWAN_HAVE_BUILTIN_MUL_OVERFLOW) +check_c_source_compiles("int main(void) { unsigned long long p; (void)__builtin_add_overflow(0, 0, &p); }" LWAN_HAVE_BUILTIN_ADD_OVERFLOW) +check_c_source_compiles("int main(void) { _Static_assert(1, \"\"); }" LWAN_HAVE_STATIC_ASSERT) +check_c_source_compiles("#include +#include +#include +int main(void) { + setsockopt(0, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, NULL, 0); +}" LWAN_HAVE_SO_ATTACH_REUSEPORT_CBPF) +check_c_source_compiles("#include +#include +int main(void) { + setsockopt(0, SOL_SOCKET, SO_INCOMING_CPU, NULL, 0); +}" LWAN_HAVE_SO_INCOMING_CPU) +check_c_source_compiles("#include +int main(void) { + struct statfs sfs = {.f_type = 0}; +}" LWAN_HAVE_STATFS_F_TYPE) + +# +# Look for Valgrind header +# find_path(VALGRIND_INCLUDE_DIR valgrind.h /usr/include /usr/include/valgrind /usr/local/include /usr/local/include/valgrind) if (VALGRIND_INCLUDE_DIR) message(STATUS "Building with Valgrind support") - add_definitions("-DUSE_VALGRIND=1") + include_directories(${VALGRIND_INCLUDE_DIR}) + + set(LWAN_HAVE_VALGRIND 1) else () message(STATUS "Valgrind headers not found -- disabling valgrind support") endif() -set(CMAKE_C_FLAGS "-Wall -Wextra -Wshadow -Wconversion -std=gnu11") +option(MTUNE_NATIVE "Build with -mtune=native/-march=native" "ON") +if (MTUNE_NATIVE) + enable_c_flag_if_avail(-mtune=native C_FLAGS_REL LWAN_HAVE_MTUNE_NATIVE) + enable_c_flag_if_avail(-march=native C_FLAGS_REL LWAN_HAVE_MARCH_NATIVE) +endif () + +enable_c_flag_if_avail(-fstack-protector-explicit CMAKE_C_FLAGS + LWAN_HAVE_STACK_PROTECTOR_EXPLICIT) + +# +# Check if immediate binding and read-only global offset table flags +# can be used +# +if (APPLE) + enable_c_flag_if_avail(-Wl,-bind_at_load CMAKE_EXE_LINKER_FLAGS + LWAN_HAVE_IMMEDIATE_BINDING) +else () + enable_c_flag_if_avail(-Wl,-z,now CMAKE_EXE_LINKER_FLAGS + LWAN_HAVE_IMMEDIATE_BINDING) + enable_c_flag_if_avail(-Wl,-z,relro CMAKE_EXE_LINKER_FLAGS + LWAN_HAVE_READ_ONLY_GOT) + enable_c_flag_if_avail(-fno-plt CMAKE_C_FLAGS + LWAN_HAVE_NO_PLT) + enable_c_flag_if_avail(-Wl,-z,noexecstack CMAKE_EXE_LINKER_FLAGS + LWAN_HAVE_NOEXEC_STACK) +endif () + +if (${CMAKE_BUILD_TYPE} MATCHES "Rel") + enable_c_flag_if_avail(-falign-functions=32 C_FLAGS_REL LWAN_HAVE_ALIGN_FNS) + enable_c_flag_if_avail(-fno-semantic-interposition C_FLAGS_REL LWAN_HAVE_NO_SEMANTIC_INTERPOSITION) + enable_c_flag_if_avail(-malign-data=abi C_FLAGS_REL LWAN_HAVE_ALIGN_DATA) + enable_c_flag_if_avail(-fno-asynchronous-unwind-tables C_FLAGS_REL LWAN_HAVE_NO_ASYNC_UNWIND_TABLES) + + enable_c_flag_if_avail(-fPIC -flto C_FLAGS_REL LWAN_HAVE_LTO) + + enable_c_flag_if_avail(-ffat-lto-objects C_FLAGS_REL LWAN_HAVE_LTO_FAT_OBJS) + enable_c_flag_if_avail(-mcrc32 C_FLAGS_REL LWAN_HAVE_BUILTIN_IA32_CRC32) + + enable_c_flag_if_avail(-mtls-dialect=gnu2 C_FLAGS_REL LWAN_HAVE_GNU2_TLS_DIALECT) +endif () + +if (${CMAKE_BUILD_TYPE} MATCHES "Deb") + option(SANITIZER "Use sanitizer (undefined, address, thread, none)" "none") + + if (${SANITIZER} MATCHES "(undefined|ub|ubsan)") + try_sanitizer("undefined") + elseif (${SANITIZER} MATCHES "(address|memory)") + try_sanitizer("address") + elseif (${SANITIZER} MATCHES "(thread|race)") + try_sanitizer("thread") + else () + message(STATUS "Building without a sanitizer") + endif () +endif () + +# +# These warnings are only supported by GCC, and some only in newer versions. +# +enable_warning_if_supported(-Waggressive-loop-optimizations) +enable_warning_if_supported(-Warith-conversion) +enable_warning_if_supported(-Warray-bounds) +enable_warning_if_supported(-Wdouble-promotion) +enable_warning_if_supported(-Wduplicated-branches) +enable_warning_if_supported(-Wduplicated-cond) +enable_warning_if_supported(-Wformat-overflow) +enable_warning_if_supported(-Wlogical-not-parentheses) +enable_warning_if_supported(-Wlogical-op) +enable_warning_if_supported(-Wno-override-init) +enable_warning_if_supported(-Wno-unknown-warning-option) +enable_warning_if_supported(-Wno-unused-parameter) +enable_warning_if_supported(-Wrestrict) +enable_warning_if_supported(-Wstringop-overflow) +enable_warning_if_supported(-Wstringop-overread) +enable_warning_if_supported(-Wstringop-truncation) +enable_warning_if_supported(-Wunsequenced) +enable_warning_if_supported(-Wvla) + +# While a useful warning, this is giving false positives. +enable_warning_if_supported(-Wno-free-nonheap-object) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wshadow -Wconversion -std=gnu11") set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} ${C_FLAGS_REL}") set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} ${C_FLAGS_REL}") set(CMAKE_C_FLAGS_MINSIZEREL "${CMAKE_C_FLAGS_MINSIZEREL} ${C_FLAGS_REL}") add_definitions("-D_FILE_OFFSET_BITS=64") +add_definitions("-D_TIME_BITS=64") + +if (APPLE) + set(LWAN_COMMON_LIBS -Wl,-force_load lwan-static) +else () + set(LWAN_COMMON_LIBS -Wl,-whole-archive lwan-static -Wl,-no-whole-archive) +endif () + +if (NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^x86_64|amd64|aarch64") + set(LWAN_HAVE_LIBUCONTEXT 1) +endif () + +include_directories(src/lib) +include_directories(BEFORE src/lib/missing) + +# +# Generate lwan-build-config.h +# +add_definitions(-include ${CMAKE_BINARY_DIR}/lwan-build-config.h) +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/src/cmake/lwan-build-config.h.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/lwan-build-config.h" +) +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/lwan-build-config.h + DESTINATION "include/lwan" +) + + +# +# Generate pkg-config file +# +set(PKG_CONFIG_REQUIRES "") +set(PKG_CONFIG_LIBDIR "\${prefix}/lib") +set(PKG_CONFIG_INCLUDEDIR "\${prefix}/include/lwan") -add_subdirectory(common) +string (REPLACE ";" " " ADDITIONAL_LIBRARIES_STR "${ADDITIONAL_LIBRARIES}") +set(PKG_CONFIG_LIBS "-L\${libdir} -llwan ${ADDITIONAL_LIBRARIES_STR}") +unset(ADDITIONAL_LIBRARIES_STR) +set(PKG_CONFIG_CFLAGS "-I\${includedir}") + +execute_process( + COMMAND git log -1 --format=%h + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE PROJECT_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE +) +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/src/cmake/lwan.pc.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc" +) +install(FILES "${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pc" DESTINATION lib/pkgconfig) + + +# +# Set up testsuite and benchmark targets +# +find_package(Python3 COMPONENTS Interpreter) +if (LUA_FOUND AND Python3_Interpreter_FOUND) + add_custom_target(testsuite + COMMAND ${Python3_EXECUTABLE} + ${PROJECT_SOURCE_DIR}/src/scripts/testsuite.py -v ${CMAKE_BINARY_DIR} + DEPENDS testrunner techempower + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMENT "Running test suite.") + + add_custom_target(benchmark + COMMAND ${Python3_EXECUTABLE} + ${PROJECT_SOURCE_DIR}/src/scripts/benchmark.py + ${CMAKE_BINARY_DIR}/src/bin/testrunner/testrunner + DEPENDS testrunner weighttp + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMENT "Running benchmark.") +endif() -include_directories(common) -add_subdirectory(lwan) -add_subdirectory(freegeoip) -add_subdirectory(techempower) +add_subdirectory(src) diff --git a/Containerfile b/Containerfile new file mode 100644 index 000000000..c5092d4b4 --- /dev/null +++ b/Containerfile @@ -0,0 +1,15 @@ +FROM docker.io/library/alpine:3.14.2 AS build +RUN apk add --no-cache gcc make musl-dev cmake pkgconfig linux-headers \ + luajit-dev sqlite-dev zlib-dev brotli-dev zstd-dev +COPY . /lwan +WORKDIR /lwan/build +RUN cmake .. -DCMAKE_BUILD_TYPE=Release -DMTUNE_NATIVE=OFF +RUN make -j + +FROM docker.io/library/alpine:3.14.2 +RUN apk add --no-cache luajit sqlite zlib brotli zstd-dev +COPY --from=build /lwan/build/src/bin/lwan/lwan . +COPY --from=build /lwan/lwan.conf . +EXPOSE 8080 +VOLUME /wwwroot +ENTRYPOINT ["/lwan"] \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 120000 index 000000000..5240dc01e --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +Containerfile \ No newline at end of file diff --git a/README.md b/README.md index 96150c76f..6df8e4995 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,1089 @@ -lwan Web Server +Lwan Web Server =============== -Lwan is a **high-performance** & **scalable** web server for glibc/Linux -platforms. - -In development for almost 3 years, Lwan was until now a personal research -effort that focused mostly on building a **solid infrastructure** for -a lightweight and speedy web server: - - - Low memory footprint (~500KiB for 10k idle connections) - - Minimal memory allocations & copies - - Minimal system calls - - Hand-crafted HTTP request parser - - Files are served using the most efficient way according to their size - - No copies between kernel and userland for files larger than 16KiB - - Smaller files are sent using vectored I/O of memory-mapped buffers - - Header overhead is considered before compressing small files - - Mostly wait-free multi-threaded design - - Diminute code base with roughly 7200 lines of C code - -It is now transitioning into a fully working, capable HTTP server. It is -not, however, as feature-packed as other popular web servers. But it is -[free software](http://www.gnu.org/philosophy/free-sw.html), so scratching -your own itches and making Lwan hum the way you want it to is possible. - -Features include: - - - Mustache templating engine - - Used for directory listing & error messages - - Available for user-built handlers - - Easy to use API to create web applications or extend the web server - - Supports rebimboca da parafuseta - - Test suite written in Python tests the server as a black box - - No-nonsense configuration file syntax - - Supports a subset of HTTP/1.0 and HTTP/1.1 - - systemd socket activation - - IPv6 ready - -The [web site](http://lwan.ws) has more details, including a FAQ about the name of the project and security concerns. +Lwan is a **high-performance** & **scalable** web server. -Performance ------------ +The [project web site](https://lwan.ws/) contains more details. -It can achieve good performance, yielding about **320000 requests/second** -on a Core i7 laptop for requests without disk access. - -When disk I/O is required, for files up to 16KiB, it yields about -**290000 requests/second**; for larger files, this drops to **185000 -requests/second**, which isn't too shabby either. - -These results, of course, with keep-alive connections, and with weighttp -running on the same machine (and thus using resources that could be used -for the webserver itself). +Build status +------------ -Without keep-alive, these numbers drop around 6-fold. +| OS | Arch | Release | Debug | Static Analysis | Tests | +|-------------|--------|---------|-------|-----------------|------------| +| Linux | x86_64 | ![release](https://shield.lwan.ws/img/gycKbr/release "Release") | ![debug](https://shield.lwan.ws/img/gycKbr/debug "Debug") | ![static-analysis](https://shield.lwan.ws/img/gycKbr/clang-analyze "Static Analysis") ![coverity](https://scan.coverity.com/projects/375/badge.svg) [Report history](https://buildbot.lwan.ws/sa/) | ![tests](https://shield.lwan.ws/img/gycKbr/unit-tests "Test") [![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/lwan.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:lwan) | +| FreeBSD 14 | x86_64 | ![freebsd-release](https://shield.lwan.ws/img/gycKbr/release-freebsd "Release FreeBSD") | ![freebsd-debug](https://shield.lwan.ws/img/gycKbr/debug-freebsd "Debug FreeBSD") | | | +| OpenBSD 7.4 | x86_64 | ![openbsd-release](https://shield.lwan.ws/img/gycKbr/release-openbsd "Release OpenBSD") | ![openbsd-debug](https://shield.lwan.ws/img/gycKbr/debug-openbsd "Debug OpenBSD") | | ![openbsd-tests](https://shield.lwan.ws/img/gycKbr/openbsd-unit-tests "OpenBSD Tests") | -Portability ------------ +Installing +---------- -Although it uses [epoll](https://en.wikipedia.org/wiki/Epoll) and the -Linux variant of sendfile(), it is fairly portable to other event-based -pollers, like [kqueue](https://en.wikipedia.org/wiki/Kqueue). -An old version of lwan has been [successfully ported to -FreeBSD](https://github.com/rakuco/lwan/tree/kqueue-port). Eventually, -some event library such as [libev](http://libev.schmorp.de) or -[libevent](http://libevent.org) will be used to aid in portability. +You can either [build Lwan yourself](#Building), use a [container +image](#container-images), or grab a package from [your favorite +distribution](#lwan-in-the-wild). Building -------- -Before installing Lwan, ensure all dependencies are installed. All of them are common dependencies found in any GNU/Linux distribution; package names will be different, but it shouldn't be difficult to search using whatever package management tool that's used by your distribution. +Before installing Lwan, ensure all dependencies are installed. All of them +are common dependencies found in any GNU/Linux distribution; package names +will be different, but it shouldn't be difficult to search using whatever +package management tool that's used by your distribution. ### Required dependencies - - [CMake](http://cmake.org), at least version 2.8 - - [Python](http://python.org), at least version 2.6 (3.x is OK) + - [CMake](https://cmake.org/), at least version 2.8 - [ZLib](http://zlib.net) ### Optional dependencies The build system will look for these libraries and enable/link if available. - - [SQLite 3](http://sqlite.org) - [Lua 5.1](http://www.lua.org) or [LuaJIT 2.0](http://luajit.org) - - Client libraries for either [MySQL](https://dev.mysql.com) or [MariaDB](https://mariadb.org) - - [TCMalloc](https://code.google.com/p/gperftools/) - - [jemalloc](http://www.canonware.com/jemalloc) - [Valgrind](http://valgrind.org) + - [Brotli](https://github.com/google/brotli) + - Can be disabled by passing `-DENABLE_BROTLI=NO` + - [ZSTD](https://github.com/facebook/zstd) + - Can be disabled by passing `-DENABLE_ZSTD=NO` + - On Linux builds, if `-DENABLE_TLS=ON` (default) is passed: + - [mbedTLS](https://github.com/ARMmbed/mbedtls) + - Alternative memory allocators can be used by passing `-DUSE_ALTERNATIVE_MALLOC` to CMake with the following values: + - ["mimalloc"](https://github.com/microsoft/mimalloc) + - ["jemalloc"](http://jemalloc.net/) + - ["tcmalloc"](https://github.com/gperftools/gperftools) + - "auto": Autodetect from the list above, falling back to libc malloc if none found + - To run test suite: + - [Python](https://www.python.org/) (2.6+) with Requests + - [Lua 5.1](http://www.lua.org) + - To run benchmark: + - [Weighttp](https://github.com/lpereira/weighttp) -- bundled and built alongside Lwan for convenience + - [Matplotlib](https://github.com/matplotlib/matplotlib) + - To build TechEmpower benchmark suite: + - Client libraries for [MariaDB](https://mariadb.org) + - [SQLite 3](http://sqlite.org) + +### Common operating system package names -### Common distribution package names +#### Minimum to build + - ArchLinux: `pacman -S cmake zlib` + - FreeBSD: `pkg install cmake pkgconf` + - Ubuntu 14+: `apt-get update && apt-get install git cmake zlib1g-dev pkg-config` + - macOS: `brew install cmake` - - ArchLinux: `pacman -S cmake python zlib sqlite luajit libmariadbclient gperftools valgrind` - - Ubuntu 14: `apt-get update && apt-get install git cmake zlib1g-dev pkg-config lua5.1-dev libsqlite3-dev libmysqlclient-dev` +#### Build with all optional features + - ArchLinux: `pacman -S cmake zlib sqlite luajit mariadb-libs gperftools valgrind mbedtls` + - FreeBSD: `pkg install cmake pkgconf sqlite3 lua51` + - Ubuntu 14+: `apt-get update && apt-get install git cmake zlib1g-dev pkg-config lua5.1-dev libsqlite3-dev libmariadb-dev libmbedtls-dev` + - macOS: `brew install cmake mariadb-connector-c sqlite lua@5.1 pkg-config` ### Build commands +#### Clone the repository + ~$ git clone git://github.com/lpereira/lwan ~$ cd lwan + +#### Create the build directory + ~/lwan$ mkdir build ~/lwan$ cd build + +#### Select build type + +Selecting a *release* version (no debugging symbols, messages, enable some +optimizations, etc): + ~/lwan/build$ cmake .. -DCMAKE_BUILD_TYPE=Release + +If you'd like to enable optimizations but still use a debugger, use this instead: + + ~/lwan/build$ cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo + +To disable optimizations and build a more debugging-friendly version: + + ~/lwan/build$ cmake .. -DCMAKE_BUILD_TYPE=Debug + +#### Build Lwan + ~/lwan/build$ make -It is important to build outside of the tree; in-tree builds *are not supported*. +This will generate a few binaries: + + - `src/bin/lwan/lwan`: The main Lwan executable. May be executed with `--help` for guidance. + - `src/bin/testrunner/testrunner`: Contains code to execute the test suite (`src/scripts/testsuite.py`). + - `src/samples/freegeoip/freegeoip`: [FreeGeoIP sample implementation](https://freegeoip.lwan.ws). Requires SQLite. + - `src/samples/techempower/techempower`: Code for the TechEmpower Web Framework benchmark. Requires SQLite and MariaDB libraries. + - `src/samples/clock/clock`: [Clock sample](https://time.lwan.ws). Generates a never-ending animated GIF file that always shows the local time. + - `src/samples/forthsalon/forthsalon`: Generates a never-ending animated GIF from a program written in the [Forth Salon](https://forthsalon.appspot.com/) dialect of the [Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)) programming language. *In construction!* + - `src/samples/forthsalon/forth`: Test harness for the Forth dialect used in the `forthsalon` sample. + - `src/bin/tools/mimegen`: Builds the extension-MIME type table. Used during the build process. + - `src/bin/tools/bin2hex`: Generates a C file from a binary file, suitable for use with #include. Used during the build process. + - `src/bin/tools/configdump`: Dumps a configuration file using the configuration reader API. Used for testing. + - `src/bin/tools/weighttp`: Rewrite of the `weighttp` HTTP benchmarking tool. + - `src/bin/tools/statuslookupgen`: Generates a perfect hash table for HTTP status codes and their descriptions. Used during the build process. + +#### Remarks Passing `-DCMAKE_BUILD_TYPE=Release` will enable some compiler optimizations (such as [LTO](http://gcc.gnu.org/wiki/LinkTimeOptimization)) -and tune the code for current architecture. *Please use this version -when benchmarking*, as the default is the Debug build, which not only -logs all requests to the standard output, but does so while holding a -mutex. +and tune the code for current architecture. + +> [!IMPORTANT] +> +> *Please use the release build when benchmarking*. +> The default is the Debug build, which not only logs all requests to the +> standard output, but does so while holding a lock, severely holding down +> the server. The default build (i.e. not passing `-DCMAKE_BUILD_TYPE=Release`) will build -a version suitable for debugging purposes. This version can be used under -Valgrind, is built with Undefined Behavior Sanitizer, and includes debugging -messages that are stripped in the release version. Debugging messages are -printed while holding a mutex, and are printed for each and every request; -so do not use this version for benchmarking purposes. +a version suitable for debugging purposes. This version can be used under +Valgrind *(if its headers are present)* and includes debugging messages that +are stripped in the release version. Debugging messages are printed for +each and every request. + +On these builds, sanitizers can be enabled. To select which one to build Lwan +with, specify one of the following options to the CMake invocation line: + + - `-DSANITIZER=ubsan` selects the Undefined Behavior Sanitizer. + - `-DSANITIZER=address` selects the Address Sanitizer. + - `-DSANITIZER=thread` selects the Thread Sanitizer. + +Alternative memory allocators can be selected as well. Lwan currently +supports [TCMalloc](https://github.com/google/tcmalloc), +[mimalloc](https://github.com/microsoft/mimalloc), and +[jemalloc](http://jemalloc.net/) out of the box. To use either one of them, +pass `-DALTERNATIVE_MALLOC=name` to the CMake invocation line, using the +names provided in the "Optional dependencies" section. + +The `-DUSE_SYSLOG=ON` option can be passed to CMake to also log to the system log +in addition to the standard output. + +If you're building Lwan for a distribution, it might be wise to use the +`-DMTUNE_NATIVE=OFF` option, otherwise the generated binary may fail to +run on some computers. + +TLS support is enabled automatically in the presence of a suitable mbedTLS +installation on Linux systems with headers new enough to support kTLS, but +can be disabled by passing `-DENABLE_TLS=NO` to CMake. + +### Tests + + ~/lwan/build$ make testsuite + +This will compile the `testrunner` program and execute regression test suite +in `src/scripts/testsuite.py`. + +### Benchmark + + ~/lwan/build$ make benchmark + +This will compile `testrunner` and execute benchmark script +`src/scripts/benchmark.py`. + +### Coverage + +Lwan can also be built with the Coverage build type by specifying +`-DCMAKE_BUILD_TYPE=Coverage`. This enables the `generate-coverage` make +target, which will run `testrunner` to prepare a test coverage report with +[lcov](http://ltp.sourceforge.net/coverage/lcov.php). + +Every commit in this repository triggers the generation of this report, +and results are [publicly available](https://buildbot.lwan.ws/lcov/). Running ------- Set up the server by editing the provided `lwan.conf`; the format is -very simple and should be self-explanatory. +explained in details below. -Configuration files are loaded from the current directory. If no changes -are made to this file, running lwan will serve static files located in -the `./wwwroot` directory, and also provide a `Hello, World!` handler (which -serves as an example of how to use some of its internal APIs). +> [!NOTE] +> +> Lwan will try to find a configuration file based in the +> executable name in the current directory; `testrunner.conf` will be used +> for the `testrunner` binary, `lwan.conf` for the `lwan` binary, and so on. -Lwan will listen on port 8080 on all interfaces. +Configuration files are loaded from the current directory. If no changes +are made to this file, running Lwan will serve static files located in +the `./wwwroot` directory. Lwan will listen on port 8080 on all interfaces. Lwan will detect the number of CPUs, will increase the maximum number of open file descriptors and generally try its best to autodetect reasonable -settings for the environment it's running on. +settings for the environment it's running on. Many of these settings can +be tweaked in the configuration file, but it's usually a good idea to not +mess with them. -Optionally, the `lwan` binary can be used for one-shot static file serving -without any configuration file. Run it with `--help` for help on that. +> [!TIP] +> +> Optionally, the `lwan` binary can be used for one-shot +> static file serving without any configuration file. Run it with `--help` +> for help on that. -IRC Channel +Configuration File +------------------ + +### Format + +Lwan uses a familiar `key = value` configuration file syntax. Comments are +supported with the `#` character (similar to e.g. shell scripts, Python, +and Perl). Nested sections can be created with curly brackets. Sections +can be empty; in this case, curly brackets are optional. + +`some_key_name` is equivalent to `some key name` in configuration files (as +an implementation detail, code reading configuration options will only be +given the version with underscores). + +> [!TIP] +> +> Values can contain environment variables. Use the +> syntax `${VARIABLE_NAME}`. Default values can be specified with a colon +> (e.g. `${VARIABLE_NAME:foo}`, which evaluates to `${VARIABLE_NAME}` if +> it's set, or `foo` otherwise). + +``` +sound volume = 11 # This one is 1 louder + +playlist metal { + files = ''' + /multi/line/strings/are/supported.mp3 + /anything/inside/these/are/stored/verbatim.mp3 + ''' +} + +playlist chiptune { + files = """ + /if/it/starts/with/single/quotes/it/ends/with/single/quotes.mod + /but/it/can/use/double/quotes.s3m + """ +} +``` + +Some examples can be found in `lwan.conf` and `techempower.conf`. + +#### Constants + +Constants can be defined and reused throughout the configuration file by +specifying them in a `constants` section anywhere in the configuration +file. A constant will be available only after that section defines a +particular constant. Constants can be re-defined. If a constant isn't +defined, its value will be obtained from an environment variable. If +it's not defined in either one `constants` section, or in the environment, +Lwan will abort with an appropriate error message. + +``` +constants { + user_name = ${USER} + home_directory = ${HOME} + buffer_size = 1000000 +} +``` + +The same syntax for default values specified above is valid here (e.g. +specifying `user_name` to be `${USER:nobody}` will set `${user_name}` to +`nobody` if `${USER}` isn't set in the environment variable or isn't +another constant.) + +#### Value types + +| Type | Description | +|--------|-------------| +| `str` | Any kind of free-form text, usually application specific | +| `int` | Integer number. Range is application specific | +| `time` | Time interval. See table below for units | +| `bool` | Boolean value. See table below for valid values | + +#### Time Intervals + +Time fields can be specified using multipliers. Multiple can be specified, they're +just added together; for instance, "1M 1w" specifies "1 month and 1 week" +(37 days). The following table lists all known multipliers: + +| Multiplier | Description | +|------------|-------------| +| `s` | Seconds | +| `m` | Minutes | +| `h` | Hours | +| `d` | Days | +| `w` | 7-day Weeks | +| `M` | 30-day Months | +| `y` | 365-day Years | + +> [!NOTE] +> +> A number with a multiplier not in this table is ignored; a +> warning is issued while reading the configuration file. No spaces must +> exist between the number and its multiplier. + +#### Boolean Values + +| True Values | False Values | +|-------------|--------------| +| Any integer number different than 0 | 0 | +| `on` | `off` | +| `true` | `false` | +| `yes` | `no` | + +### Global Settings + +It's generally a good idea to let Lwan decide the best settings for your +environment. However, not every environment is the same, and not all uses +can be decided automatically, so some configuration options are provided. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `keep_alive_timeout` | `time` | `15` | Timeout to keep a connection alive | +| `quiet` | `bool` | `false` | Set to true to not print any debugging messages. Only effective in release builds. | +| `expires` | `time` | `1M 1w` | Value of the "Expires" header. Default is 1 month and 1 week | +| `threads` | `int` | `0` | Number of I/O threads. Default (0) is the number of online CPUs | +| `proxy_protocol` | `bool` | `false` | Enables the [PROXY protocol](https://www.haproxy.com/blog/haproxy/proxy-protocol/). Versions 1 and 2 are supported. Only enable this setting if using Lwan behind a proxy, and the proxy supports this protocol; otherwise, this allows anybody to spoof origin IP addresses | +| `max_post_data_size` | `int` | `40960` | Sets the maximum number of data size for POST requests, in bytes | +| `max_put_data_size` | `int` | `40960` | Sets the maximum number of data size for PUT requests, in bytes | +| `max_file_descriptors` | `int` | `524288` | Maximum number of file descriptors. Needs to be at least 10x `threads` | +| `request_buffer_size` | `int` | `4096` | Request buffer size length. If larger than the default of `4096`, it'll be dynamically allocated. | +| `allow_temp_files` | `str` | `""` | Use temporary files; set to `post` for POST requests, `put` for PUT requests, or `all` (equivalent to setting to `post put`) for both.| +| `error_template` | `str` | Default error template | Template for error codes. See variables below. | + +#### Variables for `error_template` + +| Variable | Type | Description | +|----------|------|-------------| +| `short_message` | `str` | Short error message (e.g. `Not found`) | +| `long_message` | `str` | Long error message (e.g. `The requested resource could not be found on this server`) | + +### Straitjacket + +Lwan can drop its privileges to a user in the system, and limit its +filesystem view with a chroot. While not bulletproof, this provides a +first layer of security in the case there's a bug in Lwan. + +In order to use this feature, declare a `straitjacket` (or `straightjacket`) +section, and set some options. This requires Lwan to be executed as `root`. + +Although this section can be written anywhere in the file (as long as +it is a top level declaration), if any directories are open, due to +e.g. instantiating the `serve_files` module, Lwan will refuse to +start. (This check is only performed on Linux as a safeguard for +malconfiguration.) + +> [!TIP] +> +> Declare a Straitjacket right before a `site` section +> in such a way that configuration files and private data (e.g. TLS keys) +> are out of reach of the server after initialization has taken place. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `user` | `str` | `NULL` | Drop privileges to this user name | +| `chroot` | `str` | `NULL` | Path to `chroot()` | +| `drop_capabilities` | `bool` | `true` | Drop all capabilities with capset(2) (under Linux), or pledge(2) (under OpenBSD). | + +### Headers + +If there's a need to specify custom headers for each response, one can declare +a `headers` section in the global scope. The order which this section appears +isn't important. + +For example, this declaration: + +``` +headers { + Server = Apache/1.0.0 or nginx/1.0.0 (at your option) + Some-Custom-Header = ${WITH_THIS_ENVIRONMENT_VARIABLE} +} +``` + +Will both override the `Server` header (`Server: lwan` won't be sent), and set +`Some-Custom-Header` with the value obtained from the environment variable +`$WITH_THIS_ENVIRONMENT_VARIABLE`. + +Some headers can't be overridden, as that would cause issues when sending their +actual values while servicing requests. These include but is not limited to: + + - `Date` + - `Expires` + - `WWW-Authenticate` + - `Connection` + - `Content-Type` + - `Transfer-Encoding` + - All `Access-Control-Allow-` headers + +> [!NOTE] +> +> Header names are also case-insensitive (and case-preserving). Overriding +> `SeRVeR` will override the `Server` header, but send it the way it was +> written in the configuration file. + +### Listeners + +Only two listeners are supported per Lwan process: the HTTP listener (`listener` +section), and the HTTPS listener (`tls_listener` section). Only one listener +of each type is allowed. + +> [!WARNING] +> +> TLS support is experimental. Although it is stable +> during initial testing, your mileage may vary. Only TLSv1.2 is supported +> at this point, but TLSv1.3 is planned. + +> [!NOTE] +> +> TLS support requires :penguin: Linux with the `tls.ko` +> module built-in or loaded. Support for other operating systems may be +> added in the future. FreeBSD seems possible, other operating systems +> do not seem to offer similar feature. For unsupported operating systems, +> using a TLS terminator proxy such as [Hitch](https://hitch-tls.org/) is a good +> option. + +For both `listener` and `tls_listener` sections, the only parameter is the +the interface address and port to listen on. The listener syntax is +`${ADDRESS}:${PORT}`, where `${ADDRESS}` can either be `*` (binding to all +interfaces), an IPv6 address (if surrounded by square brackets), an IPv4 +address, or a hostname. For instance, `listener localhost:9876` would +listen only in the `lo` interface, port `9876`. + +While a `listener` section takes no keys, a `tls_listener` section requires +two: `cert` and `key` (each pointing, respectively, to the location on disk +where the TLS certificate and private key files are located) and takes an +optional boolean `hsts` key, which controls if `Strict-Transport-Security` +headers will be sent on HTTPS responses. + +> [!TIP] +> +> To generate these keys for testing purposes, the +> OpenSSL command-line tool can be used like the following: +> `openssl req -nodes -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 7` + +> [!NOTE] +> +> It's recommended that a [Straitjacket](#Straitjacket) with a `chroot` option is declared +> right after a `tls_listener` section, in such a way that the paths to the +> certificate and key are out of reach from that point on. + +If systemd socket activation is used, `systemd` can be specified as a +parameter. (If multiple listeners from systemd are specified, +`systemd:FileDescriptorName` can be specified, where `FileDescriptorName` +follows the [conventions set in the `systemd.socket` documentation](https://www.freedesktop.org/software/systemd/man/systemd.socket.html).) + +Examples: + +``` +listener *:8080 # Listen on all interfaces, port 8080, HTTP + +tls_listener *:8081 { # Listen on all interfaces, port 8081, HTTPS + cert = /path/to/cert.pem + key = /path/to/key.pem +} + +# Use named systemd socket activation for HTTP listener +listener systemd:my-service-http.socket + +# Use named systemd socket activation for HTTPS listener +tls_listener systemd:my-service-https.socket { + ... +} +``` + +### Site + +A `site` section groups instances of modules and handlers that will respond to +requests to a given URL prefix. + +#### Routing URLs Using Modules or Handlers + +In order to route URLs, Lwan matches the largest common prefix from the request +URI with a set of prefixes specified in the listener section. How a request to +a particular prefix will be handled depends on which handler or module has been +declared in the listener section. Handlers and modules are similar internally; +handlers are merely functions and hold no state, and modules holds state +("instance"). Multiple instances of a module can appear in a listener section. + +There is no special syntax to attach a prefix to a handler or module; all the +configuration parser rules apply here. Use `${NAME} ${PREFIX}` to link the +`${PREFIX}` prefix path to either a handler named `${NAME}` (if `${NAME}` +begins with `&`, as with C's "address of" operator), or a module named +`${NAME}`. Empty sections can be used here. + +Each module will have its specific set of options, and they're listed in the +next sections. In addition to configuration options, a special `authorization` +section can be present in the declaration of a module instance. Handlers do +not take any configuration options, but may include the `authorization` +section. + +> [!TIP] +> +> Executing Lwan with the `--version` command-line +> argument will show a list of built-in modules and handlers. + +The following is some basic documentation for the modules shipped with Lwan. + +#### File Serving + +The `serve_files` module will serve static files, and automatically create +directory indices or serve pre-compressed files. It'll generally try its +best to serve files in the fastest way possible according to some heuristics. + + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `path` | `str` | `NULL` | Path to a directory containing files to be served | +| `index_path` | `str` | `index.html` | File name to serve as an index for a directory | +| `serve_precompressed_path` | `bool` | `true` | If $FILE.gz exists, is smaller and newer than $FILE, and the client accepts `gzip` encoding, transfer it | +| `auto_index` | `bool` | `true` | Generate a directory list automatically if no `index_path` file present. Otherwise, yields 404 | +| `auto_index_readme` | `bool` | `true` | Includes the contents of README files as part of the automatically generated directory index | +| `directory_list_template` | `str` | `NULL` | Path to a Mustache template for the directory list; by default, use an internal template | +| `read_ahead` | `int` | `131702` | Maximum amount of bytes to read ahead when caching open files. A value of `0` disables readahead. Readahead is performed by a low priority thread to not block the I/O threads while file extents are being read from the filesystem. | +| `cache_for` | `time` | `5s` | Time to keep file metadata (size, compressed contents, open file descriptor, etc.) in cache | + +> [!NOTE] +> +> Files smaller than 16KiB will be compressed in RAM for +> the duration specified in the `cache_for` setting. Lwan will always try +> to compress with deflate, and will optionally compress with Brotli and +> zstd (if Lwan has been built with proper support). +> +> In cases where compression wouldn't be worth the effort (e.g. adding the +> `Content-Encoding` header would result in a larger response than sending +> the uncompressed file, usually the case for very small files), Lwan won't +> spend time compressing a file. +> +> For files larger than 16KiB, Lwan will not attempt to compress them. In +> future versions, it might do this and send responses using +> chunked-encoding while the file is being compressed (up to a certain +> limit, of course), but for now, only precompressed files (see +> `serve_precompressed_path` setting in the table above) are considered. +> +> For all cases, Lwan might try using the gzipped version if that's found in +> the filesystem and the client requested this encoding. + +##### Variables for `directory_list_template` + +| Variable | Type | Description | +|----------|------|-------------| +| `rel_path` | `str` | Path relative to the root directory real path | +| `readme` | `str` | Contents of first readme file found (`readme`, `readme.txt`, `read.me`, `README.TXT`, `README`) | +| `file_list` | iterator | Iterates on file list | +| `file_list.zebra_class` | `str` | `odd` for odd items, or `even` or even items | +| `file_list.icon` | `str` | Path to the icon for the file type | +| `file_list.name` | `str` | File name (escaped) | +| `file_list.type` | `str` | File type (directory or regular file) | +| `file_list.size` | `int` | File size | +| `file_list.unit` | `str` | Unit for `file_size` | + +#### Lua + +The `lua` module will allow requests to be serviced by scripts written in +the [Lua](https://www.lua.org/) programming language. Although the +functionality provided by this module is quite spartan, it's able to run +frameworks such as [Sailor](https://github.com/lpereira/sailor-hello-lwan). + +Scripts can be served from files or embedded in the configuration file, and +the results of loading them, the standard Lua modules, and (optionally, if +using LuaJIT) optimizing the code will be cached for a while. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `default_type` | `str` | `text/plain` | Default MIME-Type for responses | +| `script_file` | `str` | `NULL` | Path to Lua script| +| `cache_period` | `time` | `15s` | Time to keep Lua state loaded in memory | +| `script` | `str` | `NULL` | Inline lua script | + +##### Writing request handlers + +> [!NOTE] +> +> Lua scripts can't use global variables, as they may be not +> only serviced by different threads, but the state will be available only +> for the amount of time specified in the `cache_period` configuration +> option. This is because each I/O thread in Lwan will create an instance +> of a Lua VM (i.e. one `lua_State` struct for every I/O thread), and each +> Lwan coroutine will spawn a Lua thread (with `lua_newthread()`) per +> request. + +There's no need to have one instance of the Lua module for each endpoint; a +single script, embedded in the configuration file or otherwise, can service +many different endpoints. Scripts are supposed to implement functions with +the following signature: `handle_${METHOD}_${ENDPOINT}(req)`, where +`${METHOD}` can be a HTTP method (i.e. `get`, `post`, `head`, etc.), and +`${ENDPOINT}` is the desired endpoint to be handled by that function. A generic +`handle(req)` function will be called if the specific version doesn't exist. + +> [!TIP] +> +> Use the `root` endpoint for a catchall. For example, +> the handler function `handle_get_root()` will be called if no other handler +> could be found for that request. If no catchall is specified, the server +> will return a `404 Not Found` error. + +The `req` parameter points to a metatable that contains methods to obtain +information from the request, or to set the response, as seen below: + + - `req:query_param(param)` returns the query parameter (from the query string) with the key `param`, or `nil` if not found + - `req:post_param(param)` returns the post parameter (only for `${POST}` handlers) with the key `param`, or `nil` if not found + - `req:set_response(str)` sets the response to the string `str` + - `req:say(str)` sends a response chunk (using chunked encoding in HTTP) + - `req:send_event(event, str)` sends an event (using server-sent events) + - `req:cookie(param)` returns the cookie named `param`, or `nil` is not found + - `req:set_headers(tbl)` sets the response headers from the table `tbl`; a header may be specified multiple times by using a table, rather than a string, in the table value (`{'foo'={'bar', 'baz'}}`); must be called before sending any response with `say()` or `send_event()` + - `req:header(name)` obtains the header from the request with the given name or `nil` if not found + - `req:sleep(ms)` pauses the current handler for the specified amount of milliseconds + - `req:ws_upgrade()` returns `1` if the connection could be upgraded to a WebSocket; `0` otherwise + - `req:ws_write_text(str)` sends `str` through the WebSocket-upgraded connection as text frame + - `req:ws_write_binary(str)` sends `str` through the WebSocket-upgraded connection as binary frame + - `req:ws_write(str)` sends `str` through the WebSocket-upgraded connection as text or binary frame, depending on content containing only ASCII characters or not + - `req:ws_read()` returns a string with the contents of the last WebSocket frame, or a number indicating an status (ENOTCONN/107 on Linux if it has been disconnected; EAGAIN/11 on Linux if nothing was available; ENOMSG/42 on Linux otherwise). The return value here might change in the future for something more Lua-like. + - `req:remote_address()` returns a string with the remote IP address. + - `req:path()` returns a string with the request path. + - `req:query_string()` returns a string with the query string (empty string if no query string present). + - `req:body()` returns the request body (POST/PUT requests). + - `req:request_id()` returns a string containing the request ID. + - `req:request_date()` returns the date as it'll be written in the `Date` response header. + - `req:is_https()` returns `true` if this request is serviced through HTTPS, `false` otherwise. + - `req:host()` returns the value of the `Host` header if present, otherwise `nil`. + - `req:http_version()` returns `HTTP/1.0` or `HTTP/1.1` depending on the request version. + - `req:http_method()` returns a string, in uppercase, with the HTTP method (e.g. `"GET"`). + - `req:http_headers()` returns a table with all headers and their values. + +Handler functions may return either `nil` (in which case, a `200 OK` response +is generated), or a number matching an HTTP status code. Attempting to return +an invalid HTTP status code or anything other than a number or `nil` will result +in a `500 Internal Server Error` response being thrown. + +##### Logging + +In addition to the metamethods in the `req` parameter, one can also log messages +with different logging levels by calling methods from `Lwan.log`: + + - `Lwan.log:warning(str)` + - `Lwan.log:info(str)` + - `Lwan.log:error(str)` + - `Lwan.log:critical(str)` (Will also abort Lwan! Use with caution) + - `Lwan.log:debug(str)` (Only available in debug builds; no-op otherwise) + +> [!NOTE] +> +> If Lwan is built with syslog support, these messages will also be sent to the +> system log, otherwise they'll be printed to the standard error. + + +#### Rewrite + +The `rewrite` module will match +[patterns](https://man.openbsd.org/patterns.7) in URLs and give the option +to either redirect to another URL, or rewrite the request in a way that Lwan +will handle the request as if it were made in that way originally. + +> [!NOTE] +> +> Forked from Lua 5.3.1, the regular expresion +> engine may not be as feature-packed as most general-purpose engines, but +> has been chosen specifically because it is a [deterministic finite +> automaton](https://en.wikipedia.org/wiki/Deterministic_finite_automaton) +> in an attempt to make some kinds of [denial of service +> attacks](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS) +> impossible. + +The new URL can be specified using a simple text substitution syntax, or use Lua scripts. + +> [!TIP] +> +> Lua scripts will contain the same metamethods +> available in the `req` metatable provided by the Lua module, so it can be +> quite powerful. + +Each instance of the rewrite module will require a `pattern` and the action +to execute when such pattern is matched. Patterns are evaluated in the +order they appear in the configuration file, and are specified using nested +sections in the configuration file. For instance, consider the following +example, where two patterns are specified: + +``` +rewrite /some/base/endpoint { + pattern posts/(%d+) { + # Matches /some/base/endpointposts/2600 and /some/base/endpoint/posts/2600 + rewrite_as = /cms/view-post?id=%1 + } + pattern imgur/(%a+)/(%g+) { + # Matches /some/base/endpointimgur/gif/mpT94Ld and /some/base/endpoint/imgur/gif/mpT94Ld + redirect_to = https://i.imgur.com/%2.%1 + } +} +``` + +This example defines two patterns, one providing a nicer URL that's hidden +from the user, and another providing a different way to obtain a direct link +to an image hosted on a popular image hosting service (i.e. requesting +`/some/base/endpoint/imgur/mp4/4kOZNYX` will redirect directly to a resource +in the Imgur service). + +The value of `rewrite_as` or `redirect_to` can be Lua scripts as well; in +which case, the option `expand_with_lua` must be set to `true`, and, instead +of using the simple text substitution syntax as the example above, a +function named `handle_rewrite(req, captures)` has to be defined instead. +The `req` parameter is documented in the Lua module section; the `captures` +parameter is a table containing all the captures, in order (i.e. ``captures[2]`` +is equivalent to ``%2`` in the simple text substitition syntax). This function +returns the new URL to redirect to. + +This module has no options by itself. Options are specified in each and +every pattern. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `rewrite_as` | `str` | `NULL` | Rewrite the URL following this pattern | +| `redirect_to` | `str` | `NULL` | Redirect to a new URL following this pattern | +| `expand_with_lua` | `bool` | `false` | Use Lua scripts to redirect to or rewrite a request | + +`redirect_to` and `rewrite_as` options are mutually exclusive, and one of +them must be specified at least. + +It's also possible to specify conditions to trigger a rewrite. To specify one, +open a `condition` block, specify the condition type, and then the parameters +for that condition to be evaluated. Multiple conditions can be set per rewrite +rule as long as there's one condition per type: + +|Condition |Can use subst. syntax|Section required|Parameters|Description| +|-------------------|---------------------|----------------|----------|-----------| +|`cookie` | Yes | Yes | A single `key` = `value`| Checks if request has cookie `key` has value `value` | +|`query` | Yes | Yes | A single `key` = `value`| Checks if request has query variable `key` has value `value` | +|`post` | Yes | Yes | A single `key` = `value`| Checks if request has post data `key` has value `value` | +|`header` | Yes | Yes | A single `key` = `value`| Checks if request header `key` has value `value` | +|`environment` | Yes | Yes | A single `key` = `value`| Checks if environment variable `key` has value `value` | +|`stat` | Yes | Yes | `path`, `is_dir`, `is_file` | Checks if `path` exists in the filesystem, and optionally checks if `is_dir` or `is_file` | +|`encoding` | No | Yes | `deflate`, `gzip`, `brotli`, `zstd`, `none` | Checks if client accepts responses in a determined encoding (e.g. `deflate = yes` for Deflate encoding) | +|`proxied` | No | No | Boolean | Checks if request has been proxied through PROXY protocol | +|`http_1.0` | No | No | Boolean | Checks if request is made with a HTTP/1.0 client | +|`is_https` | No | No | Boolean | Checks if request is made through HTTPS | +|`has_query_string` | No | No | Boolean | Checks if request has a query string (even if empty) | +|`method` | No | No | Method name | Checks if HTTP method is the one specified | +|`lua` | No | No | String | Runs Lua function `matches(req)` inside String and checks if it returns `true` or `false` | +|`backref` | No | Yes | A single `backref index` = `value` | Checks if the backref number matches the provided value | + +*Can use subst. syntax* refers to the ability to reference the matched +pattern using the same substitution syntax used for the `rewrite as` or +`redirect to` actions. For instance, `condition cookie { some-cookie-name = +foo-%1-bar }` will substitute `%1` with the first match from the pattern +this condition is related to. + +> [!NOTE] +> +> Conditions that do not require a section have to be written +> as a key; for instance, `condition has_query_string = yes`. + +For example, if one wants to send `site-dark-mode.css` if there is a +`style` cookie with the value `dark`, and send `site-light-mode.css` +otherwise, one can write: + +``` +pattern site.css { + rewrite as = /site-dark-mode.css + condition cookie { style = dark } +} +pattern site.css { + rewrite as = /site-light-mode.css +} +``` + +Another example: if one wants to send pre-compressed files +if they do exist in the filesystem and the user requested them: + +``` +pattern (%g+) { + condition encoding { brotli = yes } + condition stat { path = %1.brotli } + rewrite as = %1.brotli +} +pattern (%g+) { + condition encoding { gzip = yes } + condition stat { path = %1.gzip } + rewrite as = %1.gzip +} +pattern (%g+) { + condition encoding { zstd = yes } + condition stat { path = %1.zstd } + rewrite as = %1.zstd +} +pattern (%g+) { + condition encoding { deflate = yes } + condition stat { path = %1.deflate } + rewrite as = %1.deflate +} +``` + +> [!NOTE] +> +> In general, this is not necessary, as the file serving +> module will do this automatically and pick the smallest file available for +> the requested encoding, but this shows it's possible to have a similar +> feature by configuration alone. + +#### Redirect + +The `redirect` module will, as it says in the tin, generate a `301 +Moved permanently` (by default; the code can be changed, see below) +response, according to the options specified in its configuration. +Generally, the `rewrite` module should be used instead as it packs more +features; however, this module serves also as an example of how to +write Lwan modules (less than 100 lines of code). + +If the `to` option is not specified, it always generates a `500 +Internal Server Error` response. Specifying an invalid HTTP code, or a +code that Lwan doesn't know about (see `enum lwan_http_status`), will +produce a `301 Moved Permanently` response. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `to` | `str` | `NULL` | The location to redirect to | +| `code` | `int` | `301` | The HTTP code to perform a redirect | + +#### Response + +The `response` module will generate an artificial response of any HTTP code. +In addition to also serving as an example of how to write a Lwan module, +it can be used to carve out voids from other modules (e.g. generating a +`405 Not Allowed` response for files in `/.git`, if `/` is served with +the `serve_files` module). + +If the supplied `code` falls outside the response codes known by Lwan, +a `404 Not Found` error will be sent instead. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `code` | `int` | `999` | A HTTP response code | + +#### FastCGI + +The `fastcgi` module proxies requests between the HTTP client connecting to +Lwan and a [FastCGI](https://en.wikipedia.org/wiki/FastCGI) server +accessible by Lwan. This is useful, for instance, to serve pages from a +scripting language such as PHP. + +> [!NOTE] +> +> This is a preliminary version of this module, and +> as such, it's not well optimized, some features are missing, and +> some values provided to the environment are hardcoded. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `address` | `str` | | Address to connect to. Can be a file path (for Unix Domain Sockets), IPv4 address (`aaa.bbb.ccc.ddd:port`), or IPv6 address (`[...]:port`). | +| `script_path` | `str` | | Location where the CGI scripts are located. | +| `default_index` | `str` | `index.php` | Default script to execute if unspecified in the request URI. | + +### Authorization Section + +Authorization sections can be declared in any module instance or handler, +and provides a way to authorize the fulfillment of that request through +the standard HTTP authorization mechanism. In order to require authorization +to access a certain module instance or handler, declare an `authorization` +section with a `basic` parameter, and set one of its options. + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `realm` | `str` | `Lwan` | Realm for authorization. This is usually shown in the user/password UI in browsers | +| `password_file` | `str` | `NULL` | Path for a file containing username and passwords (in clear text). The file format is the same as the configuration file format used by Lwan | + +> [!WARNING] +> +> Not only passwords are stored in clear text in a file +> that should be accessible by the server, they'll be kept in memory for a few +> seconds. Avoid using this feature if possible. + +Hacking +------- + +Please read this section (and follow it) if you're planning on contributing +to Lwan. There's nothing unexpected here; this mostly follows the rules and +expectations of many other FOSS projects, but every one expects things a +little bit different from one another. + +### Coding Style + +Lwan tries to follow a consistent coding style throughout the project. If you're +considering contributing a patch to the project, please respect this style by trying +to match the style of the surrounding code. In general: + + - `global_variables_are_named_like_this`, even though they tend to be rare and should be marked as `static` (with rare exceptions) + - Local variables are usually shorter, e.g. `local_var`, `i`, `conn` + - Struct names are often as short as they're descriptive. `typedef` for structs are rarely used in Lwan + - Header files should use `#pragma once` instead of the usual include guard hackery + - Functions that are used between .c files but are not APIs to be exposed to liblwan should have their prototype added to `lwan-private.h` + - Functions should be short and sweet. Exceptions may apply + - Public functions should be prefixed with `lwan_` + - Public types should be prefixed with `lwan_` + - Private functions must be static, and can be named without the `lwan_` prefix + - Code is indented with 4 spaces; don't use tabs + - There's a suggested line break at column 80, but it's not enforced + - `/* Old C-style comments are preferred */` + - `clang-format` can be used to format the source code in an acceptable way; a `.clang-format` file is provided + +### Tests + +If modifying well-tested areas of the code (e.g. the event loop, HTTP parser, +etc.), please add a new integration test and make sure that, before you send a +pull request, all tests (including the new ones you've sent) are working. +Tests can be added by modifying `src/scripts/testsuite.py`, and executed by +either invoking that script directly from the source root, or executing the +`testsuite` build target. + +Some tests will only work on Linux, and won't be executed on other platforms. + +### Fuzz-testing + +Lwan is automatically fuzz-tested by +[OSS-Fuzz](https://github.com/google/oss-fuzz/). To fuzz-test locally, +though, one can [follow the instructions to test +locally](https://github.com/google/oss-fuzz/blob/master/docs/new_project_guide.md#testing-locally). + +Currently, there are fuzzing drivers for the request parsing code, the +configuration file parser, the template parser, and the Lua string pattern +matching library used in the rewrite module. + +Adding new fuzzers is trivial: + +- Fuzzers are implemented in C++ and the sources are placed in + `src/bin/fuzz`. +- Fuzzers should be named `${FUZZER_NAME}_fuzzer.cc`. Look at the OSS-Fuzz + documentation and other fuzzers on information about how to write these. +- These files are not compiled by the Lwan build system, but rather by the + build scripts used by OSS-Fuzz. To test your fuzzer, please follow the + instructions to test locally, which will build the fuzzer in the + environment they'll be executed in. +- A fuzzing corpus has to be provided in `src/fuzz/corpus`. Files have to + be named `corpus-${FUZZER_NAME}-${UNIQUE_ID}`. + +### Exporting APIs + +The shared object version of `liblwan` on ELF targets (e.g. Linux) will use +a symbol filter script to hide symbols that are considered private to the +library. Please edit `src/lib/liblwan.sym` to add new symbols that should +be exported to `liblwan.so`. + +### Using Git and Pull Requests + +Lwan tries to maintain a source history that's as flat as possible, devoid of +merge commits. This means that pull requests should be rebased on top of the +current master before they can be merged; sometimes this can be done +automatically by the GitHub interface, sometimes they need some manual work to +fix conflicts. It is appreciated if the contributor fixes these conflicts when +asked. + +It is advisable to push your changes to your fork on a branch-per-pull request, +rather than pushing to the `master` branch; the reason is explained below. + +Please ensure that Git is configured properly with your name (it doesn't really +matter if it is your legal name or a nickname, but it should be enough to credit +you) and a valid email address. There's no need to add `Signed-off-by` lines, +even though it's fine to send commits with them. + +If a change is requested in a pull request, you have two choices: + + - *Reply asking for clarification.* Maybe the intentions were not clear enough, +and whoever asked for changes didn't fully understand what you were trying to +achieve + - *Fix the issue.* When fixing issues found in pull requests, *please* use +[interactive rebases](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) to +squash or fixup commits; don't add your fixes on top of your tree. Do not create +another pull request just to accomodate the changes. After rewriting +the history locally, force-push to your PR branch; the PR will update automatically +with your changes. Rewriting the history of development branches is fine, and +force-pushing them is normal and expected + +It is not enforced, but it is recommended to create smaller commits. How +commits are split in Lwan is pretty much arbitrary, so please take a look at +the commit history to get an idea on how the division should be made. Git +offers a plethora of commands to achieve this result: the already mentioned +interactive rebase, the `-p` option to `git add`, and `git commit --amend` +are good examples. + +Commit messages should have one line of summary (~72 chars), followed by an +empty line, followed by paragraphs of 80-char lines explaining the change. The +paragraphs explaining the changes are usually not necessary if the summary +is good enough. Try to [write good commit messages](https://chris.beams.io/posts/git-commit/). + +### Licensing + +Lwan is licensed under the GNU General Public License, version 2, or (at your option), +any later version. Therefore: + + - Code must be either LGPLv2.1, GPLv2, a permissive "copyfree" license that is compatible +with GPLv2 (e.g. MIT, BSD 3-clause), or public domain code (e.g. CC0) + - Although the program can be distributed and used as if it were licensed as GPLv3, +its code must be compatible with GPLv2 as well; no new code can be licensed under versions +of GPL newer than 2 + - Likewise, code licensed under licenses compatible with GPLv3 but +incompatible with GPLv2 (e.g. Apache 2) are not suitable for inclusion in +Lwan + - Even if the license does not specify that credit should be given (e.g. CC0-licensed code), +please give credit to the original author for that piece of code + - Contrary to popular belief, it is possible to use a GPL'd piece of code on a server without +having to share the code for your application. It is only when the binary of that server is +shared that source must be available to whoever has that binary. Merely accessing a Lwan +server through HTTP does not qualify as having access to the binary program that's running +on the server + - When in doubt, don't take legal advice from a README file: please consult +a lawyer that understands free software licensing + +Portability ----------- -There is an IRC channel (`#lwan`) on [Freenode](http://freenode.net). A -standard IRC client can be used. A [web IRC gateway](http://webchat.freenode.net?channels=%23lwan&uio=d4) -is also available. +While Lwan was written originally for Linux, it has been ported to BSD +systems as well. The build system will detect the supported features +and build support library functions as appropriate. +For instance, [epoll](https://en.wikipedia.org/wiki/Epoll) has been +implemented on top of [kqueue](https://en.wikipedia.org/wiki/Kqueue), and +Linux-only syscalls and GNU extensions have been implemented for the +supported systems. [This blog post](https://tia.mat.br/posts/2018/06/28/include_next_and_portability.html) +explains the details and how `#include_next` is used. + +Performance +----------- + +It can achieve good performance, yielding about **320000 requests/second** +on a Core i7 laptop for requests without disk access, and without pipelining. + +When disk I/O is required, for files up to 16KiB, it yields about +**290000 requests/second**; for larger files, this drops to **185000 +requests/second**, which isn't too shabby either. + +These results, of course, with keep-alive connections, and with weighttp +running on the same machine (and thus using resources that could be used +for the webserver itself). + +Without keep-alive, these numbers drop around 6-fold. + +IRC Channel +----------- + +There is an IRC channel (`#lwan`) on [Libera](https://libera.chat). A +standard IRC client can be used. Lwan in the wild ---------------- Here's a non-definitive list of third-party stuff that uses Lwan and have -been seen in the wild. *Help build this list!* +been seen in the wild. *If you see mentions of Lwan in the media or +academia, however small it might be, please contact the author! It'll make +her day!* +* [This project uses Cython and Lwan](https://www.erp5.com/NXD-Blog.Multicore.Python.HTTP.Server) to make it possible to write handlers in Python. * [An experimental version of Node.js using Lwan](https://github.com/raadad/node-lwan) as its HTTP server is maintained by [@raadad](https://github.com/raadad). * The beginnings of a C++11 [web framework](https://github.com/vileda/wfpp) based on Lwan written by [@vileda](https://github.com/vileda). +* A more complete C++14 [web framework](https://github.com/matt-42/silicon) by [@matt-42](https://github.com/matt-42) offers Lwan as one of its backends. * A [word ladder sample program](https://github.com/sjnam/lwan-sgb-ladders) by [@sjnam](https://github.com/sjnam). [Demo](http://tbcoe.ddns.net/sgb/ladders?start=chaos&goal=order). * A [Shodan search](https://www.shodan.io/search?query=server%3A+lwan) listing some brave souls that expose Lwan to the public internet. +* This [write-up shows the use of Lwan on a Capture the Flag competition](https://medium.com/feedzaitech/pixels-camp-ctf-challenge-qualifiers-writeup-ac661f4af96a). Some other distribution channels were made available as well: -* A `Dockerfile` is maintained by [@jaxgeller](https://github.com/jaxgeller), and is [available from the Docker registry](https://registry.hub.docker.com/u/jaxgeller/lwan/). +* Container images are available from the [GitHub Container Registry](https://ghcr.io/lpereira/lwan). [More information below](#container-images). +* A `Dockerfile` is maintained by [@jaxgeller](https://github.com/jaxgeller), and is [available from the Docker registry](https://hub.docker.com/r/jaxgeller/lwan/). * A buildpack for Heroku is maintained by [@bherrera](https://github.com/bherrera), and is [available from its repo](https://github.com/bherrera/heroku-buildpack-lwan). * Lwan is also available as a package in [Biicode](http://docs.biicode.com/c++/examples/lwan.html). +* It's also available in some GNU/Linux distributions: + * [Arch Linux](https://aur.archlinux.org/packages/lwan-git/) + * [Ubuntu](https://launchpad.net/lwan-unofficial) + * [Alpine Linux](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/lwan) + * [NixOS](https://nixos.org/nixos/packages.html#lwan) +* It's also available as a package for the [Nanos unikernel](https://github.com/nanovms/nanos). Lwan has been also used as a benchmark: @@ -169,17 +1091,195 @@ Lwan has been also used as a benchmark: * Lwan is used as a benchmark by the [PyParallel](http://pyparallel.org/) [author](https://www.reddit.com/r/programming/comments/3jhv80/pyparallel_an_experimental_proofofconcept_fork_of/cur4tut). * [Kong](https://getkong.org/about/benchmark/) uses Lwan as the [backend API](https://gist.github.com/montanaflynn/01376991f0a3ad07059c) in its benchmark. * [TechEmpower Framework benchmarks](https://www.techempower.com/benchmarks/#section=data-r10&hw=peak&test=json) feature Lwan since round 10. +* [KrakenD](http://www.krakend.io) used Lwan for the REST API in all official [benchmarks](http://www.krakend.io/docs/benchmarks/aws/) +* [Effective System Call Aggregation (ESCA)](https://github.com/eecheng87/ESCA) project uses Lwan as one of the benchmarks; they claim that Lwan throughput improved by about 30% with their system call batching approach. + +Mentions in academic journals: + +* [A dynamic predictive race detector for C/C++ programs (in English, published 2017)](https://link.springer.com/article/10.1007/s11227-017-1996-8) uses Lwan as a "real world example". +* [High-precision Data Race Detection Method for Large Scale Programs (in Chinese, published 2021)](http://www.jos.org.cn/jos/article/abstract/6260) also uses Lwan as one of the case studies. +* [AGE: Automatic Performance Evaluation of API Gateways (in English, published 2023)](https://www.computer.org/csdl/proceedings-article/iscc/2023/10218286/1PYLvz6ihBm) mentions Lwan as part of its usage in the KrakenD benchmarks. +* [Canary: Practical Static Detection of Inter-thread Value-Flow Bugs](https://rainoftime.github.io/files/PLDI21Canary.pdf) used Lwan as one of the pieces of software that have been analyzed. +* [Uma análise comparativa de ferramentas de análise estática para deteção de erros de memória](https://arxiv.org/pdf/1807.08015.pdf) used Lwan as one of the evaluation softwares. + +Mentions in magazines: + +* [Linux-Magazin (Germany) mentions Lwan in their December/2021 issue](https://www.linux-magazin.de/ausgaben/2021/12/tooltipps/) +* [Raspberry Pi Geek (Germany) mentions Lwan in their October/November 2022 issue](https://www.discountmags.com/magazine/raspberry-pi-geek-october-6-2022-digital/in-this-issue/17) +* [LinuxUser (Germany) mentions Lwan in their October 2022 issue](https://www.linux-community.de/ausgaben/linuxuser/2022/10/aktuelle-software-im-kurztest-30/) + +Mentions in books: + +* [The Ascetic Programmer](https://asceticprogrammer.info/book) has a paragraph about Lwan. + +Some talks mentioning Lwan: + +* [Talk about Lwan](https://www.youtube.com/watch?v=cttY9FdCzUE) at Polyconf16, given by [@lpereira](https://github.com/lpereira). +* This [talk about Iron](https://michaelsproul.github.io/iron-talk/), a framework for Rust, mentions Lwan as an *insane C thing*. +* [University seminar presentation](https://github.com/cu-data-engineering-s15/syllabus/blob/master/student_lectures/LWAN.pdf) about Lwan. +* This [presentation about Sailor web framework](http://www.slideshare.net/EtieneDalcol/web-development-with-lua-bulgaria-web-summit) mentions Lwan. +* [Performance and Scale @ Istio Service Mesh](https://www.youtube.com/watch?v=G4F5aRFEXnU), presented at KubeCon Europe 2018, mentions (at the 7:30 mark) that Lwan is used on the server side for testing due to its performance and robustness. +* [A multi-core Python HTTP server (much) faster than Go (spoiler: Cython)](https://www.youtube.com/watch?v=mZ9cXOH6NYk) presented at PyConFR 2018 by J.-P. Smets mentions [Nexedi's work](https://www.nexedi.com/NXD-Blog.Multicore.Python.HTTP.Server) on using Lwan as a backend for Python services with Cython. Not really third-party, but alas: * The [author's blog](http://tia.mat.br). * The [project's webpage](http://lwan.ws). -Build status ------------- +Container Images +---------------- + +Lwan container images are available at +[ghcr.io/lpereira/lwan](https://ghcr.io/lpereira/lwan). Container runtimes +like [Docker](https://docker.io) or [Podman](https://podman.io) may be used +to build and run Lwan in a container. + +### Pull lwan images from GHCR +Container images are tagged with release version numbers, so a specific version of Lwan can be pulled. + + # latest version + docker pull ghcr.io/lpereira/lwan:latest + # pull a specific version + docker pull ghcr.io/lpereira/lwan:v0.3 + +### Build images locally +Clone the repository and use `Containerfile` (Dockerfile) to build Lwan with all optional dependencies enabled. + + podman build -t lwan . + +### Run your image +The image expects to find static content at `/wwwroot`, so a volume containing your content can be mounted. + + docker run --rm -p 8080:8080 -v ./www:/wwwroot lwan + +To bring your own `lwan.conf`, simply mount it at `/lwan.conf`. + + podman run --rm -p 8080:8080 -v ./lwan.conf:/lwan.conf lwan + +### Run image with socket activation on a Linux host with Podman + +Podman supports [socket activation of containers](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md#socket-activation-of-containers). +This example shows how to run lwan with socket activation and Podman on a Linux host. + +Requirements: Podman version 4.5.0 or higher. + +1. Create user _test_ + ``` + sudo useradd test + ``` +2. Start a login shell for the user _test_ + ``` + sudo machinectl shell test@ + ``` +3. Clone the lwan git repository to _~/lwan_ +4. Build the image + ``` + podman build -t lwan ~/lwan + ``` +5. Create directories + ``` + mkdir -p ~/.config/containers/systemd + mkdir -p ~/.config/systemd/user + ``` +6. Create the file _~/lwan.conf_ with the contents + ``` + listener systemd:my.socket + site { + serve_files / { + path = /web + } + } + ``` +7. Create the file _~/.config/systemd/user/my.socket_ with the contents + ``` + [Socket] + ListenStream=8080 + ``` +8. Create the file _~/.config/containers/systemd/my.container_ with the contents + ``` + [Unit] + After=my.socket + Requires=my.socket + + [Container] + Network=none + Image=localhost/lwan + Volume=/home/test/lwan.conf:/lwan.conf:Z + Volume=/home/test/web:/web:Z + ``` + The option `:Z` is needed on SELinux systems. + As __lwan__ only needs to communicate over the socket-activated socket, it's possible to use `Network=none`. See the article [How to limit container privilege with socket activation](https://www.redhat.com/sysadmin/socket-activation-podman). +9. Create the web directory and an example text file + ``` + mkdir ~/web + echo hello > ~/web/file.txt + ``` +10. Reload systemd configuration + ``` + systemctl --user daemon-reload + ``` +11. Start the socket + ``` + systemctl --user start my.socket + ``` +12. Download the example text file from the lwan web server + ``` + $ curl localhost:8080/file.txt + hello + ``` + +Lwan quotes +----------- + +These are some of the quotes found in the wild about Lwan. They're presented +in no particular order. Contributions are appreciated: + +> "Lwan is like a classic, according to the definition given by Italian +> -- writer Italo Calvino: you can read it again and again" [Antonio +> Piccolboni](https://asceticprogrammer.info/book) + +> "I read lwan's source code. Especially, the part of using coroutine was +> very impressive and it was more interesting than a good novel. Thank you +> for that." -- +> [@patagonia](https://twitter.com/hakman314/status/996617563470680064) + +> "For the server side, we're using Lwan, which can handle 100k+ reqs/s. +> It's supposed to be super robust and it's working well for us." -- +> [@fawadkhaliq](https://twitter.com/fawadkhaliq) + +> "Insane C thing" -- [Michael +> Sproul](https://michaelsproul.github.io/iron-talk/) + +> "The best performer is LWAN, a newcomer" -- [InfoQ](https://www.infoq.com/news/2015/04/web-frameworks-benchmark-2015/) + +> "I've never had a chance to thank you for Lwan. It inspired me a lot to +> develop [Zewo](https://github.com/Zewo/Zero)" -- +> [@paulofariarl](https://twitter.com/paulofariarl/status/707926806373003265) + +> "Let me say that lwan is a thing of beauty. I got sucked into reading the +> source code for pure entertainment, it's so good. *high five*" -- +> [@kwilczynski](https://twitter.com/kwilczynski/status/692881117003644929) + +> "mad science" -- [jwz](https://jwz.org/b/yjFZ) + +> "Nice work with Lwan! I haven't looked _that_ carefully yet but so far I +> like what I saw. You definitely have the right ideas." -- +> [@thinkingfish](https://twitter.com/thinkingfish/status/521574267612196864) + +> "Lwan is a work of art. Every time I read through it, I am almost always +> awe-struck." -- +> [@neurodrone](https://twitter.com/neurodrone/status/359296080283840513) + +> "For Round 10, Lwan has taken the crown" -- +> [TechEmpower](https://www.techempower.com/blog/2015/04/21/framework-benchmarks-round-10/) + +> "Jeez this is amazing. Just end to end, rock solid engineering. (...) But that sells this work short." +> [kjeetgill](https://news.ycombinator.com/item?id=17548983) + +> "I am only a spare time C coder myself and was surprised that I can follow the code. Nice!" +> [cntlzw](https://news.ycombinator.com/item?id=17550319) -| Release | Debug | Static Analysis | Unit Tests | -|---------|-------|-----------------|------------| -| ![release](http://buildbot.lwan.ws/buildstatusimage?builder=release&number=-1 "Release") | ![debug](http://buildbot.lwan.ws/buildstatusimage?builder=debug&number=-1 "Debug") | ![clang](http://buildbot.lwan.ws/buildstatusimage?builder=clang-analyze&number=-1 "Clang") ![coverity](https://scan.coverity.com/projects/375/badge.svg)| ![tests](http://buildbot.lwan.ws/buildstatusimage?builder=unit-tests&number=-1 "Tests") -| [Waterfall](http://buildbot.lwan.ws/waterfall?show=release) | [Waterfall](http://buildbot.lwan.ws/waterfall?show=debug) | [Waterfall](http://buildbot.lwan.ws/waterfall?show=clang-analyze) - [Reports](http://buildbot.lwan.ws/sa/) | [Waterfall](http://buildbot.lwan.ws/waterfall?show=unit-tests) | +> "Impressive all and all, even more for being written in (grokkable!) C. Nice work." +> [tpaschalis](https://news.ycombinator.com/item?id=17550961) +> "LWAN was a complete failure" [dermetfan](http://dermetfan.net/posts/zig-with-c-web-servers.html) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt deleted file mode 100644 index 877755765..000000000 --- a/common/CMakeLists.txt +++ /dev/null @@ -1,77 +0,0 @@ -set(SOURCES - base64.c - hash.c - int-to-str.c - list.c - lwan.c - lwan-cache.c - lwan-config.c - lwan-coro.c - lwan-http-authorize.c - lwan-io-wrappers.c - lwan-job.c - lwan-redirect.c - lwan-request.c - lwan-response.c - lwan-rewrite.c - lwan-serve-files.c - lwan-socket.c - lwan-status.c - lwan-tables.c - lwan-template.c - lwan-thread.c - lwan-trie.c - murmur3.c - patterns.c - reallocarray.c - realpathat.c - sd-daemon.c - strbuf.c -) - - -include(FindPkgConfig) -foreach (pc_file luajit lua lua51 lua5.1 lua-5.1) - if (${pc_file} STREQUAL "luajit") - pkg_check_modules(LUA luajit>=2.0 luajit<=2.0.999) - else () - pkg_check_modules(LUA ${pc_file}>=5.1.0 ${pc_file}<=5.1.999) - endif () - if (LUA_FOUND) - list(APPEND ADDITIONAL_LIBRARIES ${LUA_LIBRARIES}) - list(APPEND SOURCES lwan-lua.c) - include_directories(${LUA_INCLUDE_DIRS}) - add_definitions(-DHAVE_LUA) - break() - endif() -endforeach () -if (NOT LUA_FOUND) - message(STATUS "Disabling Lua support") -else () - message(STATUS "Building with Lua support using ${LUA_LIBRARIES}") -endif () - -find_package(PythonInterp) -if (NOT PYTHONINTERP_FOUND) - message(FATAL_ERROR "Could not find Python interpreter") -endif () - -add_library(lwan-common STATIC ${SOURCES}) - -add_custom_command( - OUTPUT ${CMAKE_BINARY_DIR}/mime-types.h - COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/generate-mime-types-table.py ${CMAKE_SOURCE_DIR}/tools/mime.types > ${CMAKE_BINARY_DIR}/mime-types.h - DEPENDS ${CMAKE_SOURCE_DIR}/tools/mime.types ${CMAKE_SOURCE_DIR}/tools/generate-mime-types-table.py -) -add_custom_target(generate_mime_types_table DEPENDS ${CMAKE_BINARY_DIR}/mime-types.h) -add_dependencies(lwan-common generate_mime_types_table) -include_directories(${CMAKE_BINARY_DIR}) - -set(ADDITIONAL_LIBRARIES ${ADDITIONAL_LIBRARIES} PARENT_SCOPE) - -INSTALL(TARGETS lwan-common - DESTINATION "lib" -) -INSTALL(FILES lwan.h lwan-coro.h lwan-trie.h lwan-status.h strbuf.h hash.h lwan-template.h lwan-serve-files.h - DESTINATION "include/lwan" -) diff --git a/common/base64.c b/common/base64.c deleted file mode 100644 index 1d21d7eba..000000000 --- a/common/base64.c +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Base64 encoding/decoding (RFC1341) - * Copyright (c) 2005-2011, Jouni Malinen - * - * This software may be distributed under the terms of the BSD license. - * See README for more details. - */ - -#include -#include - -#include "base64.h" - -static const unsigned char base64_table[65] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -/* - * Calculated using: - * memset(dtable, 0x80, 256); - * for (i = 0; i < sizeof(base64_table) - 1; i++) - * dtable[base64_table[i]] = (unsigned char) i; - * dtable['='] = 0; - */ -static const unsigned char base64_decode_table[256] = { - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x3e, 0x80, 0x80, 0x80, 0x3f, - 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x80, 0x80, - 0x80, 0x00, 0x80, 0x80, 0x80, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, - 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, - 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, - 0x31, 0x32, 0x33, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80 -}; - -/** - * base64_encode - Base64 encode - * @src: Data to be encoded - * @len: Length of the data to be encoded - * @out_len: Pointer to output length variable, or %NULL if not used - * Returns: Allocated buffer of out_len bytes of encoded data, - * or %NULL on failure - * - * Caller is responsible for freeing the returned buffer. Returned buffer is - * nul terminated to make it easier to use as a C string. The nul terminator is - * not included in out_len. - */ -unsigned char * base64_encode(const unsigned char *src, size_t len, - size_t *out_len) -{ - unsigned char *out, *pos; - const unsigned char *end, *in; - size_t olen; - int line_len; - - olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */ - olen += olen / 72; /* line feeds */ - olen++; /* nul termination */ - if (olen < len) - return NULL; /* integer overflow */ - out = malloc(olen); - if (out == NULL) - return NULL; - - end = src + len; - in = src; - pos = out; - line_len = 0; - while (end - in >= 3) { - *pos++ = base64_table[in[0] >> 2]; - *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; - *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)]; - *pos++ = base64_table[in[2] & 0x3f]; - in += 3; - line_len += 4; - if (line_len >= 72) { - *pos++ = '\n'; - line_len = 0; - } - } - - if (end - in) { - *pos++ = base64_table[in[0] >> 2]; - if (end - in == 1) { - *pos++ = base64_table[(in[0] & 0x03) << 4]; - *pos++ = '='; - } else { - *pos++ = base64_table[((in[0] & 0x03) << 4) | - (in[1] >> 4)]; - *pos++ = base64_table[(in[1] & 0x0f) << 2]; - } - *pos++ = '='; - line_len += 4; - } - - if (line_len) - *pos++ = '\n'; - - *pos = '\0'; - if (out_len) - *out_len = (size_t)(pos - out); - return out; -} - -/** - * base64_decode - Base64 decode - * @src: Data to be decoded - * @len: Length of the data to be decoded - * @out_len: Pointer to output length variable - * Returns: Allocated buffer of out_len bytes of decoded data, - * or %NULL on failure - * - * Caller is responsible for freeing the returned buffer. - */ -unsigned char * base64_decode(const unsigned char *src, size_t len, - size_t *out_len) -{ - unsigned char *out, *pos, block[4]; - size_t i, count, olen; - int pad = 0; - - count = 0; - for (i = 0; i < len; i++) { - if (base64_decode_table[src[i]] != 0x80) - count++; - } - - if (count == 0 || count % 4) - return NULL; - - olen = (count / 4 * 3) + 1; - pos = out = malloc(olen); - if (out == NULL) - return NULL; - - count = 0; - for (i = 0; i < len; i++) { - unsigned char tmp = base64_decode_table[src[i]]; - if (tmp == 0x80) - continue; - - if (src[i] == '=') - pad++; - block[count] = tmp; - count++; - if (count == 4) { - *pos++ = (unsigned char)((block[0] << 2) | (block[1] >> 4)); - *pos++ = (unsigned char)((block[1] << 4) | (block[2] >> 2)); - *pos++ = (unsigned char)((block[2] << 6) | block[3]); - count = 0; - if (pad) { - if (pad == 1) - pos--; - else if (pad == 2) - pos -= 2; - else { - /* Invalid padding */ - free(out); - return NULL; - } - break; - } - } - } - *pos = '\0'; - - *out_len = (size_t)(pos - out); - return out; -} diff --git a/common/base64.h b/common/base64.h deleted file mode 100644 index d7f5a3fcc..000000000 --- a/common/base64.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -unsigned char *base64_encode(const unsigned char *src, size_t len, - size_t *out_len); -unsigned char *base64_decode(const unsigned char *src, size_t len, - size_t *out_len); - diff --git a/common/hash.c b/common/hash.c deleted file mode 100644 index 40a048682..000000000 --- a/common/hash.c +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Based on libkmod-hash.c from libkmod - interface to kernel module operations - * Copyright (C) 2011-2012 ProFUSION embedded systems - * Copyright (C) 2013 Leandro Pereira - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "hash.h" -#include "murmur3.h" -#include "reallocarray.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -enum { - n_buckets = 512, - steps = 64, - default_odd_constant = 0x27d4eb2d -}; -static unsigned odd_constant = default_odd_constant; - -struct hash_entry { - const char *key; - const void *value; - unsigned hashval; -}; - -struct hash_bucket { - struct hash_entry *entries; - unsigned used; - unsigned total; -}; - -struct hash { - unsigned count; - unsigned (*hash_value)(const void *key); - int (*key_compare)(const void *k1, const void *k2); - void (*free_value)(void *value); - void (*free_key)(void *value); - struct hash_bucket buckets[]; -}; - -static unsigned get_random_unsigned(void) -{ - unsigned value; - -#ifdef SYS_getrandom - long int ret = syscall(SYS_getrandom, &value, sizeof(value), 0); - if (ret == sizeof(value)) - return value; -#endif - - int fd = open("/dev/urandom", O_CLOEXEC | O_RDONLY); - if (fd < 0) { - fd = open("/dev/random", O_CLOEXEC | O_RDONLY); - if (fd < 0) - return default_odd_constant; - } - if (read(fd, &value, sizeof(value)) != sizeof(value)) - value = default_odd_constant; - close(fd); - - return value; -} - -__attribute__((constructor)) -static void initialize_odd_constant(void) -{ - /* This constant is randomized in order to mitigate the DDoS attack - * described by Crosby and Wallach in UsenixSec2003. */ - odd_constant = get_random_unsigned() | 1; - murmur3_set_seed(odd_constant); -} - -static inline unsigned hash_int(const void *keyptr) -{ - /* http://www.concentric.net/~Ttwang/tech/inthash.htm */ - unsigned key = (unsigned)(long)keyptr; - unsigned c2 = odd_constant; - - key = (key ^ 61) ^ (key >> 16); - key += key << 3; - key ^= key >> 4; - key *= c2; - key ^= key >> 15; - return key; -} - -#if defined(HAVE_BUILTIN_CPU_INIT) && defined(HAVE_BUILTIN_IA32_CRC32) -static inline unsigned hash_crc32(const void *keyptr) -{ - unsigned hash = odd_constant; - const char *key = keyptr; - size_t len = strlen(key); - -#if __x86_64__ - while (len >= sizeof(uint64_t)) { - uint64_t data; - memcpy(&data, key, sizeof(data)); - hash = (unsigned)__builtin_ia32_crc32di(hash, data); - key += sizeof(uint64_t); - len -= sizeof(uint64_t); - } -#endif /* __x86_64__ */ - while (len >= sizeof(uint32_t)) { - uint32_t data; - memcpy(&data, key, sizeof(data)); - hash = __builtin_ia32_crc32si(hash, data); - key += sizeof(uint32_t); - len -= sizeof(uint32_t); - } - if (*key && *(key + 1)) { - uint16_t data; - memcpy(&data, key, sizeof(data)); - hash = __builtin_ia32_crc32hi(hash, data); - key += sizeof(uint16_t); - len--; - } - if (*key) - hash = __builtin_ia32_crc32qi(hash, (unsigned char)*key); - - return hash; -} -#endif - -static inline int hash_int_key_cmp(const void *k1, const void *k2) -{ - int a = (int)(intptr_t)k1; - int b = (int)(intptr_t)k2; - return (a > b) - (a < b); -} - -static struct hash *hash_internal_new( - unsigned (*hash_value)(const void *key), - int (*key_compare)(const void *k1, const void *k2), - void (*free_key)(void *value), - void (*free_value)(void *value)) -{ - struct hash *hash = calloc(1, sizeof(struct hash) + - n_buckets * sizeof(struct hash_bucket)); - if (hash == NULL) - return NULL; - hash->hash_value = hash_value; - hash->key_compare = key_compare; - hash->free_value = free_value; - hash->free_key = free_key; - return hash; -} - -struct hash *hash_int_new(void (*free_key)(void *value), - void (*free_value)(void *value)) -{ - return hash_internal_new(hash_int, - hash_int_key_cmp, - free_key, - free_value); -} - -struct hash *hash_str_new(void (*free_key)(void *value), - void (*free_value)(void *value)) -{ - unsigned (*hash_func)(const void *key); - -#if defined(HAVE_BUILTIN_CPU_INIT) && defined(HAVE_BUILTIN_IA32_CRC32) - __builtin_cpu_init(); - if (__builtin_cpu_supports("sse4.2")) { - hash_func = hash_crc32; - } else { - hash_func = murmur3_simple; - } -#else - hash_func = murmur3_simple; -#endif - - return hash_internal_new( - hash_func, - (int (*)(const void *, const void *))strcmp, - free_key, - free_value); -} - -void hash_free(struct hash *hash) -{ - struct hash_bucket *bucket, *bucket_end; - - if (hash == NULL) - return; - - bucket = hash->buckets; - bucket_end = bucket + n_buckets; - for (; bucket < bucket_end; bucket++) { - if (hash->free_value) { - struct hash_entry *entry, *entry_end; - entry = bucket->entries; - entry_end = entry + bucket->used; - for (; entry < entry_end; entry++) { - hash->free_value((void *)entry->value); - if (hash->free_key) - hash->free_key((void *)entry->key); - } - } - free(bucket->entries); - } - free(hash); -} - -/* - * add or replace key in hash map. - * - * none of key or value are copied, just references are remembered as is, - * make sure they are live while pair exists in hash! - */ -int hash_add(struct hash *hash, const void *key, const void *value) -{ - unsigned hashval = hash->hash_value(key); - unsigned pos = hashval & (n_buckets - 1); - struct hash_bucket *bucket = hash->buckets + pos; - struct hash_entry *entry, *entry_end; - - if (bucket->used + 1 >= bucket->total) { - unsigned new_total = bucket->total + steps; - struct hash_entry *tmp = reallocarray(bucket->entries, new_total, sizeof(*tmp)); - if (tmp == NULL) - return -errno; - bucket->entries = tmp; - bucket->total = new_total; - } - - entry = bucket->entries; - entry_end = entry + bucket->used; - for (; entry < entry_end; entry++) { - if (hashval != entry->hashval) - continue; - if (hash->key_compare(key, entry->key) != 0) - continue; - if (hash->free_value) - hash->free_value((void *)entry->value); - if (hash->free_key) - hash->free_key((void *)entry->key); - - entry->key = key; - entry->value = value; - return 0; - } - - entry->key = key; - entry->value = value; - entry->hashval = hashval; - bucket->used++; - hash->count++; - return 0; -} - -/* similar to hash_add(), but fails if key already exists */ -int hash_add_unique(struct hash *hash, const void *key, const void *value) -{ - unsigned hashval = hash->hash_value(key); - unsigned pos = hashval & (n_buckets - 1); - struct hash_bucket *bucket = hash->buckets + pos; - struct hash_entry *entry, *entry_end; - - if (bucket->used + 1 >= bucket->total) { - unsigned new_total = bucket->total + steps; - struct hash_entry *tmp = reallocarray(bucket->entries, new_total, sizeof(*tmp)); - if (tmp == NULL) - return -errno; - bucket->entries = tmp; - bucket->total = new_total; - } - - entry = bucket->entries; - entry_end = entry + bucket->used; - for (; entry < entry_end; entry++) { - if (hashval != entry->hashval) - continue; - if (hash->key_compare(key, entry->key) == 0) - return -EEXIST; - } - - entry->key = key; - entry->value = value; - entry->hashval = hashval; - bucket->used++; - hash->count++; - return 0; -} - -static inline struct hash_entry *hash_find_entry(const struct hash *hash, - const char *key, - unsigned hashval) -{ - unsigned pos = hashval & (n_buckets - 1); - const struct hash_bucket *bucket = hash->buckets + pos; - struct hash_entry *entry, *entry_end; - - entry = bucket->entries; - entry_end = entry + bucket->used; - for (; entry < entry_end; entry++) { - if (hashval != entry->hashval) - continue; - if (hash->key_compare(key, entry->key) == 0) - return entry; - } - - return NULL; -} - -void *hash_find(const struct hash *hash, const void *key) -{ - const struct hash_entry *entry; - - entry = hash_find_entry(hash, key, hash->hash_value(key)); - if (entry) - return (void *)entry->value; - return NULL; -} - -int hash_del(struct hash *hash, const void *key) -{ - unsigned hashval = hash->hash_value(key); - unsigned pos = hashval & (n_buckets - 1); - unsigned steps_used, steps_total; - struct hash_bucket *bucket = hash->buckets + pos; - struct hash_entry *entry, *entry_end; - - entry = hash_find_entry(hash, key, hashval); - if (entry == NULL) - return -ENOENT; - - if (hash->free_value) - hash->free_value((void *)entry->value); - if (hash->free_key) - hash->free_key((void *)entry->key); - - entry_end = bucket->entries + bucket->used; - memmove(entry, entry + 1, - (size_t)(entry_end - entry) * sizeof(struct hash_entry)); - - bucket->used--; - hash->count--; - - steps_used = bucket->used / steps; - steps_total = bucket->total / steps; - if (steps_used + 1 < steps_total) { - struct hash_entry *tmp = reallocarray(bucket->entries, steps_used + 1, - steps * sizeof(*tmp)); - if (tmp) { - bucket->entries = tmp; - bucket->total = (steps_used + 1) * steps; - } - } - - return 0; -} - -unsigned hash_get_count(const struct hash *hash) -{ - return hash->count; -} - -void hash_iter_init(const struct hash *hash, struct hash_iter *iter) -{ - iter->hash = hash; - iter->bucket = 0; - iter->entry = -1; -} - -bool hash_iter_next(struct hash_iter *iter, const void **key, - const void **value) -{ - const struct hash_bucket *b = iter->hash->buckets + iter->bucket; - const struct hash_entry *e; - - iter->entry++; - - if ((unsigned)iter->entry >= b->used) { - iter->entry = 0; - - for (iter->bucket++; iter->bucket < n_buckets; - iter->bucket++) { - b = iter->hash->buckets + iter->bucket; - - if (b->used > 0) - break; - } - - if (iter->bucket >= n_buckets) - return false; - } - - e = b->entries + iter->entry; - - if (value != NULL) - *value = e->value; - if (key != NULL) - *key = e->key; - - return true; -} diff --git a/common/hash.h b/common/hash.h deleted file mode 100644 index 3a99a1598..000000000 --- a/common/hash.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include - -struct hash; - -struct hash_iter { - const struct hash *hash; - unsigned int bucket; - int entry; -}; - -struct hash *hash_int_new(void (*free_key)(void *value), - void (*free_value)(void *value)); -struct hash *hash_str_new(void (*free_key)(void *value), - void (*free_value)(void *value)); -void hash_free(struct hash *hash); -int hash_add(struct hash *hash, const void *key, const void *value); -int hash_add_unique(struct hash *hash, const void *key, const void *value); -int hash_del(struct hash *hash, const void *key); -void *hash_find(const struct hash *hash, const void *key); -unsigned int hash_get_count(const struct hash *hash); -void hash_iter_init(const struct hash *hash, struct hash_iter *iter); -bool hash_iter_next(struct hash_iter *iter, const void **key, - const void **value); diff --git a/common/int-to-str.c b/common/int-to-str.c deleted file mode 100644 index 526763ee4..000000000 --- a/common/int-to-str.c +++ /dev/null @@ -1,75 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include "int-to-str.h" - -ALWAYS_INLINE char * -uint_to_string(size_t value, - char dst[static INT_TO_STR_BUFFER_SIZE], - size_t *length_out) -{ - /* - * Based on routine by A. Alexandrescu, licensed under CC0 - * https://creativecommons.org/publicdomain/zero/1.0/legalcode - */ - static const size_t length = INT_TO_STR_BUFFER_SIZE; - size_t next = length - 1; - static const char digits[201] = - "0001020304050607080910111213141516171819" - "2021222324252627282930313233343536373839" - "4041424344454647484950515253545556575859" - "6061626364656667686970717273747576777879" - "8081828384858687888990919293949596979899"; - dst[next--] = '\0'; - while (value >= 100) { - const uint32_t i = (uint32_t)((value % 100) * 2); - value /= 100; - dst[next] = digits[i + 1]; - dst[next - 1] = digits[i]; - next -= 2; - } - // Handle last 1-2 digits - if (value < 10) { - dst[next] = (char)('0' + (uint32_t)value); - *length_out = length - next - 1; - return dst + next; - } - uint32_t i = (uint32_t)value * 2; - dst[next] = digits[i + 1]; - dst[next - 1] = digits[i]; - *length_out = length - next; - return dst + next - 1; -} - -ALWAYS_INLINE char * -int_to_string(ssize_t value, - char dst[static INT_TO_STR_BUFFER_SIZE], - size_t *length_out) -{ - if (value < 0) { - char *p = uint_to_string((size_t) -value, dst, length_out); - *--p = '-'; - ++*length_out; - - return p; - } - - return uint_to_string((size_t) value, dst, length_out); -} diff --git a/common/list.c b/common/list.c deleted file mode 100644 index 9aa6009aa..000000000 --- a/common/list.c +++ /dev/null @@ -1,71 +0,0 @@ -/* - * list - double linked list routines - * Author: Rusty Russell - * - * The list header contains routines for manipulating double linked lists. - * It defines two types: struct list_head used for anchoring lists, and - * struct list_node which is usually embedded in the structure which is placed - * in the list. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to permit - * persons to whom the Software is furnished to do so, subject to the - * following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT - * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR - * THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include "list.h" - -static void *corrupt(const char *abortstr, - const struct list_node *head, - const struct list_node *node, - unsigned int count) -{ - if (abortstr) { - fprintf(stderr, - "%s: prev corrupt in node %p (%u) of %p\n", - abortstr, node, count, head); - abort(); - } - return NULL; -} - -struct list_node *list_check_node(const struct list_node *node, - const char *abortstr) -{ - const struct list_node *p, *n; - unsigned int count = 0; - - for (p = node, n = node->next; n != node; p = n, n = n->next) { - count++; - if (n->prev != p) - return corrupt(abortstr, node, n, count); - } - /* Check prev on head node. */ - if (node->prev != p) - return corrupt(abortstr, node, node, 0); - - return (struct list_node *)node; -} - -struct list_head *list_check(const struct list_head *h, const char *abortstr) -{ - if (!list_check_node(&h->n, abortstr)) - return NULL; - return (struct list_head *)h; -} diff --git a/common/lwan-cache.c b/common/lwan-cache.c deleted file mode 100644 index a1732c1d4..000000000 --- a/common/lwan-cache.c +++ /dev/null @@ -1,394 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include -#include -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-private.h" -#include "lwan-cache.h" -#include "hash.h" - -#define GET_AND_REF_TRIES 5 - -enum { - /* Entry flags */ - FLOATING = 1 << 0, - TEMPORARY = 1 << 1, - - /* Cache flags */ - SHUTTING_DOWN = 1 << 0 -}; - -struct cache_t { - struct { - struct hash *table; - pthread_rwlock_t lock; - } hash; - - struct { - struct list_head list; - pthread_rwlock_t lock; - } queue; - - struct { - CreateEntryCallback create_entry; - DestroyEntryCallback destroy_entry; - void *context; - } cb; - - struct { - time_t time_to_live; - clockid_t clock_id; - } settings; - - unsigned flags; - -#ifndef NDEBUG - struct { - unsigned hits; - unsigned misses; - unsigned evicted; - } stats; -#endif -}; - -static bool cache_pruner_job(void *data); - -static clockid_t detect_fastest_monotonic_clock(void) -{ -#ifdef CLOCK_MONOTONIC_COARSE - struct timespec ts; - - if (!clock_gettime(CLOCK_MONOTONIC_COARSE, &ts)) - return CLOCK_MONOTONIC_COARSE; -#endif - return CLOCK_MONOTONIC; -} - -static ALWAYS_INLINE void clock_monotonic_gettime(struct cache_t *cache, - struct timespec *ts) -{ - if (UNLIKELY(clock_gettime(cache->settings.clock_id, ts) < 0)) - lwan_status_perror("clock_gettime"); -} - -struct cache_t *cache_create(CreateEntryCallback create_entry_cb, - DestroyEntryCallback destroy_entry_cb, - void *cb_context, - time_t time_to_live) -{ - struct cache_t *cache; - - assert(create_entry_cb); - assert(destroy_entry_cb); - assert(time_to_live > 0); - - cache = calloc(1, sizeof(*cache)); - if (!cache) - return NULL; - - cache->hash.table = hash_str_new(free, NULL); - if (!cache->hash.table) - goto error_no_hash; - - if (pthread_rwlock_init(&cache->hash.lock, NULL)) - goto error_no_hash_lock; - if (pthread_rwlock_init(&cache->queue.lock, NULL)) - goto error_no_queue_lock; - - cache->cb.create_entry = create_entry_cb; - cache->cb.destroy_entry = destroy_entry_cb; - cache->cb.context = cb_context; - - cache->settings.clock_id = detect_fastest_monotonic_clock(); - cache->settings.time_to_live = time_to_live; - - list_head_init(&cache->queue.list); - - lwan_job_add(cache_pruner_job, cache); - - return cache; - -error_no_queue_lock: - pthread_rwlock_destroy(&cache->hash.lock); -error_no_hash_lock: - hash_free(cache->hash.table); -error_no_hash: - free(cache); - - return NULL; -} - -void cache_destroy(struct cache_t *cache) -{ - assert(cache); - -#ifndef NDEBUG - lwan_status_debug("Cache stats: %d hits, %d misses, %d evictions", - cache->stats.hits, cache->stats.misses, - cache->stats.evicted); -#endif - - lwan_job_del(cache_pruner_job, cache); - cache->flags |= SHUTTING_DOWN; - cache_pruner_job(cache); - pthread_rwlock_destroy(&cache->hash.lock); - pthread_rwlock_destroy(&cache->queue.lock); - hash_free(cache->hash.table); - free(cache); -} - -static ALWAYS_INLINE void convert_to_temporary(struct cache_entry_t *entry) -{ - entry->flags = TEMPORARY; -} - -struct cache_entry_t *cache_get_and_ref_entry(struct cache_t *cache, - const char *key, int *error) -{ - struct cache_entry_t *entry; - char *key_copy; - - assert(cache); - assert(error); - assert(key); - - *error = 0; - - /* If the lock can't be obtained, return an error to allow, for instance, - * yielding from the coroutine and trying to obtain the lock at a later - * time. */ - if (UNLIKELY(pthread_rwlock_tryrdlock(&cache->hash.lock) == EBUSY)) { - *error = EWOULDBLOCK; - return NULL; - } - /* Find the item in the hash table. If it's there, increment the reference - * and return it. */ - entry = hash_find(cache->hash.table, key); - if (LIKELY(entry)) { - ATOMIC_INC(entry->refs); - pthread_rwlock_unlock(&cache->hash.lock); -#ifndef NDEBUG - ATOMIC_INC(cache->stats.hits); -#endif - return entry; - } - - /* Unlock the cache so the item can be created. */ - pthread_rwlock_unlock(&cache->hash.lock); - -#ifndef NDEBUG - ATOMIC_INC(cache->stats.misses); -#endif - - key_copy = strdup(key); - if (UNLIKELY(!key_copy)) { - *error = ENOMEM; - return NULL; - } - - entry = cache->cb.create_entry(key, cache->cb.context); - if (!entry) { - free(key_copy); - return NULL; - } - - memset(entry, 0, sizeof(*entry)); - entry->key = key_copy; - entry->refs = 1; - - if (pthread_rwlock_trywrlock(&cache->hash.lock) == EBUSY) { - /* Couldn't obtain hash lock: instead of waiting, just return - * the recently-created item as a temporary item. Might result - * in starvation, though, so this might be changed back to - * pthread_rwlock_wrlock() again someday if this proves to be - * a problem. */ - convert_to_temporary(entry); - return entry; - } - - if (!hash_add_unique(cache->hash.table, entry->key, entry)) { - struct timespec time_to_die; - clock_monotonic_gettime(cache, &time_to_die); - entry->time_to_die = time_to_die.tv_sec + cache->settings.time_to_live; - - if (LIKELY(!pthread_rwlock_wrlock(&cache->queue.lock))) { - list_add_tail(&cache->queue.list, &entry->entries); - pthread_rwlock_unlock(&cache->queue.lock); - } else { - convert_to_temporary(entry); - } - } else { - /* Either there's another item with the same key (-EEXIST), or - * there was an error inside the hash table. In either case, - * just return a TEMPORARY entry so that it is destroyed the first - * time someone unrefs this entry. TEMPORARY entries are pretty much - * like FLOATING entries, but unreffing them do not use atomic - * operations. */ - convert_to_temporary(entry); - } - - pthread_rwlock_unlock(&cache->hash.lock); - return entry; -} - -void cache_entry_unref(struct cache_t *cache, struct cache_entry_t *entry) -{ - assert(entry); - - if (entry->flags & TEMPORARY) { - free(entry->key); - goto destroy_entry; - } - - if (ATOMIC_DEC(entry->refs)) - return; - - /* FLOATING entries without references won't be picked up by the pruner - * job, so destroy them right here. */ - if (entry->flags & FLOATING) { -destroy_entry: - /* FIXME: There's a race condition here: if the cache is destroyed - * while there are cache items floating around, this will dereference - * deallocated memory. */ - cache->cb.destroy_entry(entry, cache->cb.context); - } -} - -static bool cache_pruner_job(void *data) -{ - struct cache_t *cache = data; - struct cache_entry_t *node, *next; - struct timespec now; - bool shutting_down = cache->flags & SHUTTING_DOWN; - unsigned evicted = 0; - struct list_head queue; - - if (UNLIKELY(pthread_rwlock_trywrlock(&cache->queue.lock) == EBUSY)) - return false; - - /* If the queue is empty, there's nothing to do; unlock/return*/ - if (list_empty(&cache->queue.list)) { - if (UNLIKELY(pthread_rwlock_unlock(&cache->queue.lock))) - lwan_status_perror("pthread_rwlock_unlock"); - return false; - } - - /* There are things to do; assign cache queue to a local queue, - * initialize cache queue to an empty queue. Then unlock */ - list_head_init(&queue); - list_append_list(&queue, &cache->queue.list); - list_head_init(&cache->queue.list); - - if (UNLIKELY(pthread_rwlock_unlock(&cache->queue.lock))) { - lwan_status_perror("pthread_rwlock_unlock"); - goto end; - } - - clock_monotonic_gettime(cache, &now); - list_for_each_safe(&queue, node, next, entries) { - char *key = node->key; - - if (now.tv_sec < node->time_to_die && LIKELY(!shutting_down)) - break; - - list_del(&node->entries); - - if (UNLIKELY(pthread_rwlock_wrlock(&cache->hash.lock))) { - lwan_status_perror("pthread_rwlock_wrlock"); - continue; - } - - hash_del(cache->hash.table, key); - - if (UNLIKELY(pthread_rwlock_unlock(&cache->hash.lock))) - lwan_status_perror("pthread_rwlock_unlock"); - - if (ATOMIC_INC(node->refs) == 1) { - cache->cb.destroy_entry(node, cache->cb.context); - } else { - ATOMIC_BITWISE(&node->flags, or, FLOATING); - /* Decrement the reference and see if we were genuinely the last one - * holding it. If so, destroy the entry. */ - if (!ATOMIC_DEC(node->refs)) - cache->cb.destroy_entry(node, cache->cb.context); - } - - evicted++; - } - - /* If local queue has been entirely processed, there's no need to - * append items in the cache queue to it; just update statistics and - * return */ - if (list_empty(&queue)) - goto end; - - /* Prepend local, unprocessed queue, to the cache queue. Since the cache - * item TTL is constant, items created later will be destroyed later. */ - if (LIKELY(!pthread_rwlock_wrlock(&cache->queue.lock))) { - list_prepend_list(&cache->queue.list, &queue); - pthread_rwlock_unlock(&cache->queue.lock); - } else { - lwan_status_perror("pthread_rwlock_wrlock"); - } - -end: -#ifndef NDEBUG - ATOMIC_AAF(&cache->stats.evicted, evicted); -#endif - return evicted; -} - -struct cache_entry_t* -cache_coro_get_and_ref_entry(struct cache_t *cache, coro_t *coro, - const char *key) -{ - for (int tries = GET_AND_REF_TRIES; tries; tries--) { - int error; - struct cache_entry_t *ce = cache_get_and_ref_entry(cache, key, &error); - - if (LIKELY(ce)) { - /* - * This is deferred here so that, if the coroutine is killed - * after it has been yielded, this cache entry is properly - * freed. - */ - coro_defer2(coro, CORO_DEFER2(cache_entry_unref), cache, ce); - return ce; - } - - /* - * If the cache would block while reading its hash table, yield and - * try again. On any other error, just return NULL. - */ - if (error == EWOULDBLOCK) { - coro_yield(coro, CONN_CORO_MAY_RESUME); - } else { - break; - } - } - - return NULL; -} diff --git a/common/lwan-cache.h b/common/lwan-cache.h deleted file mode 100644 index 7396d8404..000000000 --- a/common/lwan-cache.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#pragma once - -#include - -#include "list.h" -#include "lwan-coro.h" - -struct cache_entry_t { - struct list_node entries; - char *key; - unsigned refs; - unsigned flags; - time_t time_to_die; -}; - -typedef struct cache_entry_t *(*CreateEntryCallback)( - const char *key, void *context); -typedef void (*DestroyEntryCallback)( - struct cache_entry_t *entry, void *context); - -struct cache_t; - -struct cache_t *cache_create(CreateEntryCallback create_entry_cb, - DestroyEntryCallback destroy_entry_cb, - void *cb_context, - time_t time_to_live); -void cache_destroy(struct cache_t *cache); - -struct cache_entry_t *cache_get_and_ref_entry(struct cache_t *cache, - const char *key, int *error); -void cache_entry_unref(struct cache_t *cache, struct cache_entry_t *entry); -struct cache_entry_t *cache_coro_get_and_ref_entry(struct cache_t *cache, - coro_t *coro, const char *key); diff --git a/common/lwan-config.c b/common/lwan-config.c deleted file mode 100644 index 2b8323433..000000000 --- a/common/lwan-config.c +++ /dev/null @@ -1,361 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "lwan-config.h" -#include "lwan-status.h" -#include "hash.h" - -unsigned int parse_time_period(const char *str, unsigned int default_value) -{ - unsigned int total = 0; - unsigned int period; - char multiplier; - - if (!str) - return default_value; - - while (*str && sscanf(str, "%u%c", &period, &multiplier) == 2) { - switch (multiplier) { - case 's': total += period; break; - case 'm': total += period * ONE_MINUTE; break; - case 'h': total += period * ONE_HOUR; break; - case 'd': total += period * ONE_DAY; break; - case 'w': total += period * ONE_WEEK; break; - case 'M': total += period * ONE_MONTH; break; - case 'y': total += period * ONE_YEAR; break; - default: - lwan_status_warning("Ignoring unknown multiplier: %c", - multiplier); - } - - str = (const char *)rawmemchr(str, multiplier) + 1; - } - - return total ? total : default_value; -} - -bool parse_bool(const char *value, bool default_value) -{ - if (!value) - return default_value; - - if (!strcmp(value, "true") || !strcmp(value, "1") - || !strcmp(value, "on") || !strcmp(value, "yes")) - return true; - - if (!strcmp(value, "false") || !strcmp(value, "0") - || !strcmp(value, "off") || !strcmp(value, "no")) - return false; - - return default_value; -} - -long parse_long(const char *value, long default_value) -{ - char *endptr; - long parsed; - - errno = 0; - parsed = strtol(value, &endptr, 0); - - if (errno != 0) - return default_value; - - if (*endptr != '\0' || value == endptr) - return default_value; - - return parsed; -} - -int parse_int(const char *value, int default_value) -{ - long long_value = parse_long(value, default_value); - - if ((long)(int)long_value != long_value) - return default_value; - - return (int)long_value; -} - -bool config_error(config_t *conf, const char *fmt, ...) -{ - va_list values; - int len; - char *output; - - if (conf->error_message) - return false; - - va_start(values, fmt); - len = vasprintf(&output, fmt, values); - va_end(values); - - if (len >= 0) { - conf->error_message = output; - return true; - } - - conf->error_message = NULL; - return false; -} - -static char *remove_comments(char *line) -{ - char *tmp = strrchr(line, '#'); - if (tmp) - *tmp = '\0'; - return line; -} - -static char *remove_trailing_spaces(char *line) -{ - char *end = rawmemchr(line, '\0'); - - for (end--; isspace(*end); end--); - *(end + 1) = '\0'; - - return line; -} - -static char *remove_leading_spaces(char *line) -{ - while (isspace(*line)) - line++; - return line; -} - -static char *find_line_end(char *line) -{ - if (*line == '\0') - return line; - return (char *)rawmemchr(line, '\0') - 1; -} - -static bool parse_section(char *line, config_line_t *l, char *bracket) -{ - char *name, *param; - char *space = strchr(line, ' '); - if (!space) - return false; - - *bracket = '\0'; - *space = '\0'; - name = remove_trailing_spaces(remove_leading_spaces(line)); - param = remove_trailing_spaces(remove_leading_spaces(space + 1)); - - l->section.name = name; - l->section.param = param; - l->type = CONFIG_LINE_TYPE_SECTION; - - return true; -} - -static char *replace_space_with_underscore(char *line) -{ - for (char *ptr = line; *ptr; ptr++) { - if (*ptr == ' ') - *ptr = '_'; - } - return line; -} - -static bool parse_line(char *line, config_line_t *l, char *equal) -{ - *equal = '\0'; - line = remove_leading_spaces(line); - line = remove_trailing_spaces(line); - - l->line.key = replace_space_with_underscore(line); - l->line.value = remove_leading_spaces(equal + 1); - l->type = CONFIG_LINE_TYPE_LINE; - - return true; -} - -static bool find_section_end(config_t *config, config_line_t *line, int recursion_level) -{ - if (recursion_level > 10) { - config_error(config, "Recursion level too deep"); - return false; - } - - while (config_read_line(config, line)) { - switch (line->type) { - case CONFIG_LINE_TYPE_LINE: - continue; - case CONFIG_LINE_TYPE_SECTION: - if (!find_section_end(config, line, recursion_level + 1)) - return false; - break; - case CONFIG_LINE_TYPE_SECTION_END: - return true; - } - } - - return false; -} - -bool config_skip_section(config_t *conf, config_line_t *line) -{ - if (conf->error_message) - return false; - if (line->type != CONFIG_LINE_TYPE_SECTION) - return false; - return find_section_end(conf, line, 0); -} - -bool config_isolate_section(config_t *current_conf, - config_line_t *current_line, config_t *isolated) -{ - long startpos, endpos; - bool r = false; - - *isolated = *current_conf; - - if (current_conf->error_message) - return false; - if (current_line->type != CONFIG_LINE_TYPE_SECTION) - return false; - - startpos = ftell(current_conf->file); - if (startpos < 0) - return false; - if (!find_section_end(current_conf, current_line, 0)) - goto resetpos; - endpos = ftell(current_conf->file); - if (endpos < 0) - goto resetpos; - - if (!config_open(isolated, current_conf->path)) - goto resetpos; - if (fseek(isolated->file, startpos, SEEK_SET) < 0) - goto resetpos; - - isolated->isolated.end = endpos; - r = true; - -resetpos: - if (fseek(current_conf->file, startpos, SEEK_SET) < 0) { - config_error(current_conf, "Could not reset file position"); - return false; - } - if (!r) - config_error(current_conf, "Unknown error while isolating section"); - return r; -} - -bool config_read_line(config_t *conf, config_line_t *l) -{ - char *line, *line_end; - - if (conf->error_message) - return false; - -retry: - if (!fgets(l->buffer, sizeof(l->buffer), conf->file)) - return false; - - if (conf->isolated.end > 0) { - long curpos = ftell(conf->file); - if (curpos < 0) { - config_error(conf, "Could not obtain file position"); - return false; - } - if (curpos >= conf->isolated.end) - return false; - } - conf->line++; - - line = remove_comments(l->buffer); - line = remove_leading_spaces(line); - line = remove_trailing_spaces(line); - line_end = find_line_end(line); - - if (*line_end == '{') { - if (!parse_section(line, l, line_end)) { - config_error(conf, "Malformed section opening"); - return false; - } - } else if (*line == '\0') { - goto retry; - } else if (*line == '}' && line == line_end) { - l->type = CONFIG_LINE_TYPE_SECTION_END; - } else { - char *equal = strchr(line, '='); - if (equal) { - if (!parse_line(line, l, equal)) { - config_error(conf, "Malformed key=value line"); - return false; - } - } else { - config_error(conf, "Expecting section or key=value"); - return false; - } - } - - return true; -} - -bool config_open(config_t *conf, const char *path) -{ - if (!conf) - return false; - if (!path) - return false; - - conf->file = fopen(path, "re"); - if (!conf->file) - return false; - - conf->path = strdup(path); - if (!conf->path) { - fclose(conf->file); - conf->file = NULL; - return false; - } - - conf->isolated.end = -1; - conf->line = 0; - conf->error_message = NULL; - - return true; -} - -void config_close(config_t *conf) -{ - if (!conf) - return; - if (!conf->file) - return; - fclose(conf->file); - free(conf->path); - free(conf->error_message); -} - diff --git a/common/lwan-coro.c b/common/lwan-coro.c deleted file mode 100644 index 29310c7a0..000000000 --- a/common/lwan-coro.c +++ /dev/null @@ -1,363 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-coro.h" - -#ifdef USE_VALGRIND -#include -#endif - -#define CORO_STACK_MIN ((3 * (PTHREAD_STACK_MIN)) / 2) - -static_assert(DEFAULT_BUFFER_SIZE < (CORO_STACK_MIN + PTHREAD_STACK_MIN), - "Request buffer fits inside coroutine stack"); - -typedef struct coro_defer_t_ coro_defer_t; - -struct coro_defer_t_ { - coro_defer_t *next; - void (*func)(); - void *data1; - void *data2; -}; - -struct coro_t_ { - coro_switcher_t *switcher; - coro_context_t context; - int yield_value; - -#if !defined(NDEBUG) && defined(USE_VALGRIND) - unsigned int vg_stack_id; -#endif - - coro_defer_t *defer; - void *data; - - bool ended; -}; - -static void coro_entry_point(coro_t *data, coro_function_t func); - -/* - * This swapcontext() implementation was obtained from glibc and modified - * slightly to not save/restore the floating point registers, unneeded - * registers, and signal mask. It is Copyright (C) 2001, 2002, 2003 Free - * Software Foundation, Inc and are distributed under GNU LGPL version 2.1 - * (or later). I'm not sure if I can distribute them inside a GPL program; - * they're straightforward so I'm assuming there won't be any problem; if - * there is, I'll just roll my own. - * -- Leandro - */ -#if defined(__x86_64__) -void coro_swapcontext(coro_context_t *current, coro_context_t *other) - __attribute__((noinline)); - asm( - ".text\n\t" - ".p2align 4\n\t" - ".globl coro_swapcontext\n\t" - "coro_swapcontext:\n\t" - "mov %rbx,0(%rdi)\n\t" - "mov %rbp,8(%rdi)\n\t" - "mov %r12,16(%rdi)\n\t" - "mov %r13,24(%rdi)\n\t" - "mov %r14,32(%rdi)\n\t" - "mov %r15,40(%rdi)\n\t" - "mov %rdi,48(%rdi)\n\t" - "mov %rsi,56(%rdi)\n\t" - "mov (%rsp),%rcx\n\t" - "mov %rcx,64(%rdi)\n\t" - "lea 0x8(%rsp),%rcx\n\t" - "mov %rcx,72(%rdi)\n\t" - "mov 72(%rsi),%rsp\n\t" - "mov 0(%rsi),%rbx\n\t" - "mov 8(%rsi),%rbp\n\t" - "mov 16(%rsi),%r12\n\t" - "mov 24(%rsi),%r13\n\t" - "mov 32(%rsi),%r14\n\t" - "mov 40(%rsi),%r15\n\t" - "mov 48(%rsi),%rdi\n\t" - "mov 64(%rsi),%rcx\n\t" - "mov 56(%rsi),%rsi\n\t" - "jmp *%rcx\n\t"); -#elif defined(__i386__) -void coro_swapcontext(coro_context_t *current, coro_context_t *other) - __attribute__((noinline)); - asm( - ".text\n\t" - ".p2align 16\n\t" - ".globl coro_swapcontext\n\t" - "coro_swapcontext:\n\t" - "movl 0x4(%esp),%eax\n\t" - "movl %ecx,0x1c(%eax)\n\t" /* ECX */ - "movl %ebx,0x0(%eax)\n\t" /* EBX */ - "movl %esi,0x4(%eax)\n\t" /* ESI */ - "movl %edi,0x8(%eax)\n\t" /* EDI */ - "movl %ebp,0xc(%eax)\n\t" /* EBP */ - "movl (%esp),%ecx\n\t" - "movl %ecx,0x14(%eax)\n\t" /* EIP */ - "leal 0x4(%esp),%ecx\n\t" - "movl %ecx,0x18(%eax)\n\t" /* ESP */ - "movl 8(%esp),%eax\n\t" - "movl 0x14(%eax),%ecx\n\t" /* EIP (1) */ - "movl 0x18(%eax),%esp\n\t" /* ESP */ - "pushl %ecx\n\t" /* EIP (2) */ - "movl 0x0(%eax),%ebx\n\t" /* EBX */ - "movl 0x4(%eax),%esi\n\t" /* ESI */ - "movl 0x8(%eax),%edi\n\t" /* EDI */ - "movl 0xc(%eax),%ebp\n\t" /* EBP */ - "movl 0x1c(%eax),%ecx\n\t" /* ECX */ - "ret\n\t"); -#else -#define coro_swapcontext(cur,oth) swapcontext(cur, oth) -#endif - -static void -coro_entry_point(coro_t *coro, coro_function_t func) -{ - int return_value = func(coro); - coro->ended = true; - coro_yield(coro, return_value); -} - -static void -coro_run_deferred(coro_t *coro) -{ - for (coro_defer_t *defer = coro->defer; defer;) { - coro_defer_t *tmp = defer; - defer->func(defer->data1, defer->data2); - defer = tmp->next; - free(tmp); - } - coro->defer = NULL; -} - -void -coro_reset(coro_t *coro, coro_function_t func, void *data) -{ - unsigned char *stack = (unsigned char *)(coro + 1); - - coro->ended = false; - coro->data = data; - - coro_run_deferred(coro); - -#if defined(__x86_64__) - coro->context[6 /* RDI */] = (uintptr_t) coro; - coro->context[7 /* RSI */] = (uintptr_t) func; - coro->context[8 /* RIP */] = (uintptr_t) coro_entry_point; - coro->context[9 /* RSP */] = (uintptr_t) stack + CORO_STACK_MIN; -#elif defined(__i386__) - /* Align stack and make room for two arguments */ - stack = (unsigned char *)((uintptr_t)(stack + CORO_STACK_MIN - - sizeof(uintptr_t) * 2) & 0xfffffff0); - - uintptr_t *argp = (uintptr_t *)stack; - *argp++ = 0; - *argp++ = (uintptr_t)coro; - *argp++ = (uintptr_t)func; - - coro->context[5 /* EIP */] = (uintptr_t) coro_entry_point; - coro->context[6 /* ESP */] = (uintptr_t) stack; -#else - getcontext(&coro->context); - - coro->context.uc_stack.ss_sp = stack; - coro->context.uc_stack.ss_size = CORO_STACK_MIN; - coro->context.uc_stack.ss_flags = 0; - coro->context.uc_link = NULL; - - makecontext(&coro->context, (void (*)())coro_entry_point, 2, coro, func); -#endif -} - -ALWAYS_INLINE coro_t * -coro_new(coro_switcher_t *switcher, coro_function_t function, void *data) -{ - coro_t *coro = malloc(sizeof(*coro) + CORO_STACK_MIN); - if (!coro) - return NULL; - - coro->switcher = switcher; - coro->defer = NULL; - coro_reset(coro, function, data); - -#if !defined(NDEBUG) && defined(USE_VALGRIND) - char *stack = (char *)(coro + 1); - coro->vg_stack_id = VALGRIND_STACK_REGISTER(stack, stack + CORO_STACK_MIN); -#endif - - return coro; -} - -ALWAYS_INLINE void * -coro_get_data(coro_t *coro) -{ - return LIKELY(coro) ? coro->data : NULL; -} - -ALWAYS_INLINE int -coro_resume(coro_t *coro) -{ - assert(coro); - assert(coro->ended == false); - -#if defined(__x86_64__) || defined(__i386__) - coro_swapcontext(&coro->switcher->caller, &coro->context); - if (!coro->ended) - memcpy(&coro->context, &coro->switcher->callee, - sizeof(coro->context)); -#else - coro_context_t prev_caller; - - memcpy(&prev_caller, &coro->switcher->caller, sizeof(prev_caller)); - coro_swapcontext(&coro->switcher->caller, &coro->context); - if (!coro->ended) { - memcpy(&coro->context, &coro->switcher->callee, - sizeof(coro->context)); - memcpy(&coro->switcher->caller, &prev_caller, - sizeof(coro->switcher->caller)); - } -#endif - - return coro->yield_value; -} - -ALWAYS_INLINE int -coro_resume_value(coro_t *coro, int value) -{ - assert(coro); - assert(coro->ended == false); - - coro->yield_value = value; - return coro_resume(coro); -} - -ALWAYS_INLINE int -coro_yield(coro_t *coro, int value) -{ - assert(coro); - coro->yield_value = value; - coro_swapcontext(&coro->switcher->callee, &coro->switcher->caller); - return coro->yield_value; -} - -void -coro_free(coro_t *coro) -{ - assert(coro); -#if !defined(NDEBUG) && defined(USE_VALGRIND) - VALGRIND_STACK_DEREGISTER(coro->vg_stack_id); -#endif - coro_run_deferred(coro); - free(coro); -} - -static void -coro_defer_any(coro_t *coro, void (*func)(), void *data1, void *data2) -{ - coro_defer_t *defer = malloc(sizeof(*defer)); - if (UNLIKELY(!defer)) - return; - - assert(func); - - /* Some uses require deferred statements are arranged in a stack. */ - defer->next = coro->defer; - defer->func = func; - defer->data1 = data1; - defer->data2 = data2; - coro->defer = defer; -} - -ALWAYS_INLINE void -coro_defer(coro_t *coro, void (*func)(void *data), void *data) -{ - coro_defer_any(coro, func, data, NULL); -} - -ALWAYS_INLINE void -coro_defer2(coro_t *coro, void (*func)(void *data1, void *data2), - void *data1, void *data2) -{ - coro_defer_any(coro, func, data1, data2); -} - -void * -coro_malloc_full(coro_t *coro, size_t size, void (*destroy_func)()) -{ - coro_defer_t *defer = malloc(sizeof(*defer) + size); - if (UNLIKELY(!defer)) - return NULL; - - defer->next = coro->defer; - defer->func = destroy_func; - defer->data1 = defer + 1; - defer->data2 = NULL; - - coro->defer = defer; - - return defer + 1; -} - -static void nothing() -{ -} - -inline void * -coro_malloc(coro_t *coro, size_t size) -{ - return coro_malloc_full(coro, size, nothing); -} - -char * -coro_strdup(coro_t *coro, const char *str) -{ - size_t len = strlen(str) + 1; - char *dup = coro_malloc(coro, len); - if (LIKELY(dup)) - memcpy(dup, str, len); - return dup; -} - -char * -coro_printf(coro_t *coro, const char *fmt, ...) -{ - va_list values; - int len; - char *tmp_str; - - va_start(values, fmt); - len = vasprintf(&tmp_str, fmt, values); - va_end(values); - - if (UNLIKELY(len < 0)) - return NULL; - - coro_defer(coro, CORO_DEFER(free), tmp_str); - return tmp_str; -} diff --git a/common/lwan-coro.h b/common/lwan-coro.h deleted file mode 100644 index d56081412..000000000 --- a/common/lwan-coro.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#pragma once - -#include -#if defined(__x86_64__) -#include -typedef uintptr_t coro_context_t[10]; -#elif defined(__i386__) -#include -typedef uintptr_t coro_context_t[7]; -#else -#include -typedef ucontext_t coro_context_t; -#endif - -typedef struct coro_t_ coro_t; -typedef struct coro_switcher_t_ coro_switcher_t; - -typedef int (*coro_function_t) (coro_t *coro); - -struct coro_switcher_t_ { - coro_context_t caller; - coro_context_t callee; -}; - -coro_t *coro_new(coro_switcher_t *switcher, coro_function_t function, void *data); -void coro_free(coro_t *coro); - -void coro_reset(coro_t *coro, coro_function_t func, void *data); - -int coro_resume(coro_t *coro); -int coro_resume_value(coro_t *coro, int value); -int coro_yield(coro_t *coro, int value); - -void *coro_get_data(coro_t *coro); - -void coro_defer(coro_t *coro, void (*func)(void *data), void *data); -void coro_defer2(coro_t *coro, void (*func)(void *data1, void *data2), - void *data1, void *data2); -void *coro_malloc(coro_t *coro, size_t sz); -void *coro_malloc_full(coro_t *coro, size_t size, void (*destroy_func)()); -char *coro_strdup(coro_t *coro, const char *str); -char *coro_printf(coro_t *coro, const char *fmt, ...); - -#define CORO_DEFER(fn) ((void (*)(void *))(fn)) -#define CORO_DEFER2(fn) ((void (*)(void *, void *))(fn)) - diff --git a/common/lwan-http-authorize.c b/common/lwan-http-authorize.c deleted file mode 100644 index 2e3e4766b..000000000 --- a/common/lwan-http-authorize.c +++ /dev/null @@ -1,214 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include -#include -#include - -#include "base64.h" -#include "lwan-cache.h" -#include "lwan-config.h" -#include "lwan-http-authorize.h" - -struct realm_password_file_t { - struct cache_entry_t base; - struct hash *entries; -}; - -static struct cache_t *realm_password_cache = NULL; - -static void fourty_two_and_free(void *str) -{ - if (LIKELY(str)) { - char *s = str; - while (*s) - *s++ = 42; - free(str); - } -} - -static struct cache_entry_t *create_realm_file( - const char *key, - void *context __attribute__((unused))) -{ - struct realm_password_file_t *rpf = malloc(sizeof(*rpf)); - config_t f; - config_line_t l; - - if (UNLIKELY(!rpf)) - return NULL; - - rpf->entries = hash_str_new(fourty_two_and_free, fourty_two_and_free); - if (UNLIKELY(!rpf->entries)) - goto error_no_close; - - if (!config_open(&f, key)) - goto error_no_close; - - while (config_read_line(&f, &l)) { - /* FIXME: Storing plain-text passwords in memory isn't a good idea. */ - switch (l.type) { - case CONFIG_LINE_TYPE_LINE: { - char *username = strdup(l.line.key); - if (!username) - goto error; - - char *password = strdup(l.line.value); - if (!password) { - free(username); - goto error; - } - - int err = hash_add_unique(rpf->entries, username, password); - if (LIKELY(!err)) - continue; - - free(username); - free(password); - - if (err == -EEXIST) { - lwan_status_warning( - "Username entry already exists, ignoring: \"%s\"", - l.line.key); - continue; - } - - goto error; - } - default: - config_error(&f, "Expected username = password"); - break; - } - } - - if (f.error_message) { - lwan_status_error("Error on password file \"%s\", line %d: %s", - key, f.line, f.error_message); - goto error; - } - - config_close(&f); - return (struct cache_entry_t *)rpf; - -error: - config_close(&f); -error_no_close: - hash_free(rpf->entries); - free(rpf); - return NULL; -} - -static void destroy_realm_file(struct cache_entry_t *entry, - void *context __attribute__((unused))) -{ - struct realm_password_file_t *rpf = (struct realm_password_file_t *)entry; - hash_free(rpf->entries); - free(rpf); -} - -bool -lwan_http_authorize_init(void) -{ - realm_password_cache = cache_create(create_realm_file, - destroy_realm_file, NULL, 60); - - return !!realm_password_cache; -} - -void -lwan_http_authorize_shutdown(void) -{ - cache_destroy(realm_password_cache); -} - -static bool -authorize(coro_t *coro, - lwan_value_t *authorization, - const char *password_file) -{ - struct realm_password_file_t *rpf; - unsigned char *decoded; - char *colon; - char *password; - char *looked_password; - size_t decoded_len; - bool password_ok = false; - - rpf = (struct realm_password_file_t *)cache_coro_get_and_ref_entry( - realm_password_cache, coro, password_file); - if (UNLIKELY(!rpf)) - return false; - - decoded = base64_decode((unsigned char *)authorization->value, - authorization->len, &decoded_len); - if (UNLIKELY(!decoded)) - return false; - - if (UNLIKELY(decoded_len >= sizeof(((config_line_t *)0)->buffer))) - goto out; - - colon = memchr(decoded, ':', decoded_len); - if (UNLIKELY(!colon)) - goto out; - - *colon = '\0'; - password = colon + 1; - - looked_password = hash_find(rpf->entries, decoded); - if (looked_password) - password_ok = !strcmp(password, looked_password); - -out: - free(decoded); - return password_ok; -} - -bool -lwan_http_authorize(lwan_request_t *request, - lwan_value_t *authorization, - const char *realm, - const char *password_file) -{ - static const char authenticate_tmpl[] = "Basic realm=\"%s\""; - static const size_t basic_len = sizeof("Basic ") - 1; - lwan_key_value_t *headers; - - if (!authorization->value) - goto unauthorized; - - if (UNLIKELY(strncmp(authorization->value, "Basic ", basic_len))) - goto unauthorized; - - authorization->value += basic_len; - authorization->len -= basic_len; - - if (authorize(request->conn->coro, authorization, password_file)) - return true; - -unauthorized: - headers = coro_malloc(request->conn->coro, 2 * sizeof(*headers)); - headers[0].key = "WWW-Authenticate"; - headers[0].value = coro_printf(request->conn->coro, - authenticate_tmpl, realm); - headers[1].key = headers[1].value = NULL; - - request->response.headers = headers; - return false; -} diff --git a/common/lwan-io-wrappers.c b/common/lwan-io-wrappers.c deleted file mode 100644 index 3451f0248..000000000 --- a/common/lwan-io-wrappers.c +++ /dev/null @@ -1,254 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-io-wrappers.h" - -static const int MAX_FAILED_TRIES = 5; -static const size_t BUFFER_SIZE = 1400; - -int -lwan_openat(lwan_request_t *request, - int dirfd, const char *pathname, int flags) -{ - for (int tries = MAX_FAILED_TRIES; tries; tries--) { - int fd = openat(dirfd, pathname, flags); - if (LIKELY(fd >= 0)) { - coro_defer(request->conn->coro, CORO_DEFER(close), (void *)(intptr_t)fd); - return fd; - } - - switch (errno) { - case EINTR: - case EMFILE: - case ENFILE: - case ENOMEM: - coro_yield(request->conn->coro, CONN_CORO_MAY_RESUME); - break; - default: - return -errno; - } - } - - return -ENFILE; -} - -ssize_t -lwan_writev(lwan_request_t *request, struct iovec *iov, int iov_count) -{ - ssize_t total_written = 0; - int curr_iov = 0; - - for (int tries = MAX_FAILED_TRIES; tries;) { - ssize_t written = writev(request->fd, iov + curr_iov, iov_count - curr_iov); - if (UNLIKELY(written < 0)) { - /* FIXME: Consider short writes as another try as well? */ - tries--; - - switch (errno) { - case EAGAIN: - case EINTR: - goto try_again; - default: - goto out; - } - } - - total_written += written; - - while (curr_iov < iov_count && written >= (ssize_t)iov[curr_iov].iov_len) { - written -= (ssize_t)iov[curr_iov].iov_len; - curr_iov++; - } - - if (curr_iov == iov_count) - return total_written; - - iov[curr_iov].iov_base = (char *)iov[curr_iov].iov_base + written; - iov[curr_iov].iov_len -= (size_t)written; - -try_again: - coro_yield(request->conn->coro, CONN_CORO_MAY_RESUME); - } - -out: - coro_yield(request->conn->coro, CONN_CORO_ABORT); - __builtin_unreachable(); -} - -ssize_t -lwan_write(lwan_request_t *request, const void *buf, size_t count) -{ - ssize_t total_written = 0; - - for (int tries = MAX_FAILED_TRIES; tries;) { - ssize_t written = write(request->fd, buf, count); - if (UNLIKELY(written < 0)) { - tries--; - - switch (errno) { - case EAGAIN: - case EINTR: - goto try_again; - default: - goto out; - } - } - - total_written += written; - if ((size_t)total_written == count) - return total_written; - if ((size_t)total_written < count) - buf = (char *)buf + written; - -try_again: - coro_yield(request->conn->coro, CONN_CORO_MAY_RESUME); - } - -out: - coro_yield(request->conn->coro, CONN_CORO_ABORT); - __builtin_unreachable(); -} - -ssize_t -lwan_send(lwan_request_t *request, const void *buf, size_t count, int flags) -{ - ssize_t total_sent = 0; - - for (int tries = MAX_FAILED_TRIES; tries;) { - ssize_t written = send(request->fd, buf, count, flags); - if (UNLIKELY(written < 0)) { - tries--; - - switch (errno) { - case EAGAIN: - case EINTR: - goto try_again; - default: - goto out; - } - } - - total_sent += written; - if ((size_t)total_sent == count) - return total_sent; - if ((size_t)total_sent < count) - buf = (char *)buf + written; - -try_again: - coro_yield(request->conn->coro, CONN_CORO_MAY_RESUME); - } - -out: - coro_yield(request->conn->coro, CONN_CORO_ABORT); - __builtin_unreachable(); -} - -static ALWAYS_INLINE ssize_t -sendfile_read_write(coro_t *coro, int in_fd, int out_fd, off_t offset, size_t count) -{ - /* FIXME: Use lwan_{read,write}() here */ - ssize_t total_bytes_written = 0; - /* This buffer is allocated on the heap in order to minimize stack usage - * inside the coroutine */ - char *buffer = coro_malloc(coro, BUFFER_SIZE); - - if (offset && lseek(in_fd, offset, SEEK_SET) < 0) { - lwan_status_perror("lseek"); - return -1; - } - - while (count > 0) { - ssize_t read_bytes = read(in_fd, buffer, BUFFER_SIZE); - if (read_bytes < 0) { - coro_yield(coro, CONN_CORO_ABORT); - __builtin_unreachable(); - } - - ssize_t bytes_written = write(out_fd, buffer, (size_t)read_bytes); - if (bytes_written < 0) { - coro_yield(coro, CONN_CORO_ABORT); - __builtin_unreachable(); - } - - total_bytes_written += bytes_written; - count -= (size_t)bytes_written; - coro_yield(coro, CONN_CORO_MAY_RESUME); - } - - return total_bytes_written; -} - -static ALWAYS_INLINE ssize_t -sendfile_linux_sendfile(coro_t *coro, int in_fd, int out_fd, off_t offset, size_t count) -{ - size_t total_written = 0; - size_t to_be_written = count; - - do { - ssize_t written = sendfile(out_fd, in_fd, &offset, to_be_written); - if (written < 0) { - switch (errno) { - case EAGAIN: - case EINTR: - coro_yield(coro, CONN_CORO_MAY_RESUME); - continue; - - default: - coro_yield(coro, CONN_CORO_ABORT); - __builtin_unreachable(); - } - } - - total_written += (size_t)written; - to_be_written -= (size_t)written; - - coro_yield(coro, CONN_CORO_MAY_RESUME); - } while (to_be_written > 0); - - return (ssize_t)total_written; -} - -ssize_t -lwan_sendfile(lwan_request_t *request, int in_fd, off_t offset, size_t count) -{ - if (count > BUFFER_SIZE * 5) { - if (UNLIKELY(posix_fadvise(in_fd, offset, (off_t)count, - POSIX_FADV_SEQUENTIAL) < 0)) - lwan_status_perror("posix_fadvise"); - } - - ssize_t written_bytes = sendfile_linux_sendfile( - request->conn->coro, in_fd, request->fd, offset, count); - - if (UNLIKELY(written_bytes < 0)) { - switch (errno) { - case ENOSYS: - case EINVAL: - return sendfile_read_write(request->conn->coro, in_fd, request->fd, offset, count); - } - } - return written_bytes; -} diff --git a/common/lwan-io-wrappers.h b/common/lwan-io-wrappers.h deleted file mode 100644 index 2715f7127..000000000 --- a/common/lwan-io-wrappers.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#pragma once - -#include -#include - -#include "lwan.h" - -int lwan_openat(lwan_request_t *request, int dirfd, const char *pathname, - int flags); -ssize_t lwan_writev(lwan_request_t *request, struct iovec *iov, - int iovcnt); -ssize_t lwan_write(lwan_request_t *request, const void *buffer, - size_t count); -ssize_t lwan_send(lwan_request_t *request, const void *buf, size_t count, - int flags); -ssize_t lwan_sendfile(lwan_request_t *request, int in_fd, - off_t offset, size_t count); - diff --git a/common/lwan-lua.c b/common/lwan-lua.c deleted file mode 100644 index 5c9a56b96..000000000 --- a/common/lwan-lua.c +++ /dev/null @@ -1,469 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-cache.h" -#include "lwan-config.h" -#include "lwan-lua.h" - -static const char *request_metatable_name = "Lwan.Request"; - -struct lwan_lua_priv_t { - char *default_type; - char *script_file; - pthread_key_t cache_key; - unsigned cache_period; -}; - -struct lwan_lua_state_t { - struct cache_entry_t base; - lua_State *L; -}; - -static ALWAYS_INLINE lwan_request_t *userdata_as_request(lua_State *L, int n) -{ - return *((lwan_request_t **)luaL_checkudata(L, n, request_metatable_name)); -} - -static int req_say_cb(lua_State *L) -{ - lwan_request_t *request = userdata_as_request(L, 1); - size_t response_str_len; - const char *response_str = lua_tolstring(L, -1, &response_str_len); - - strbuf_set_static(request->response.buffer, response_str, response_str_len); - lwan_response_send_chunk(request); - - return 0; -} - -static int req_send_event_cb(lua_State *L) -{ - lwan_request_t *request = userdata_as_request(L, 1); - size_t event_str_len; - const char *event_str = lua_tolstring(L, -1, &event_str_len); - const char *event_name = lua_tostring(L, -2); - - strbuf_set_static(request->response.buffer, event_str, event_str_len); - lwan_response_send_event(request, event_name); - - return 0; -} - -static int req_yield_cb(lua_State *L) -{ - return lua_yield(L, 0); -} - -static int req_set_response_cb(lua_State *L) -{ - lwan_request_t *request = userdata_as_request(L, 1); - size_t response_str_len; - const char *response_str = lua_tolstring(L, -1, &response_str_len); - - strbuf_set(request->response.buffer, response_str, response_str_len); - - return 0; -} - -static int request_param_getter(lua_State *L, - const char *(*getter)(lwan_request_t *req, const char *key)) -{ - lwan_request_t *request = userdata_as_request(L, 1); - const char *key_str = lua_tostring(L, -1); - - const char *value = getter(request, key_str); - if (!value) - lua_pushnil(L); - else - lua_pushstring(L, value); - - return 1; -} - -static int req_query_param_cb(lua_State *L) -{ - return request_param_getter(L, lwan_request_get_query_param); -} - -static int req_post_param_cb(lua_State *L) -{ - return request_param_getter(L, lwan_request_get_post_param); -} - -static int req_cookie_cb(lua_State *L) -{ - return request_param_getter(L, lwan_request_get_cookie); -} - -static int req_set_headers_cb(lua_State *L) -{ - const size_t max_headers = 16; - const int table_index = 2; - const int key_index = 1 + table_index; - const int value_index = 2 + table_index; - const int nested_value_index = value_index * 2 - table_index; - lwan_request_t *request = userdata_as_request(L, 1); - lwan_key_value_t *headers = coro_malloc(request->conn->coro, max_headers * sizeof(*headers)); - size_t n_headers = 0; - - if (!headers) { - lua_pushnil(L); - return 1; - } - - if (request->flags & RESPONSE_SENT_HEADERS) { - lua_pushnil(L); - return 1; - } - - if (!lua_istable(L, table_index)) { - lua_pushnil(L); - return 1; - } - - lua_pushnil(L); - while (n_headers < (max_headers - 1) && lua_next(L, table_index) != 0) { - if (!lua_isstring(L, key_index)) { - lua_pop(L, 1); - continue; - } - - if (lua_isstring(L, value_index)) { - headers[n_headers].key = coro_strdup(request->conn->coro, - lua_tostring(L, key_index)); - headers[n_headers].value = coro_strdup(request->conn->coro, - lua_tostring(L, value_index)); - - n_headers++; - } else if (lua_istable(L, value_index)) { - char *header_name = coro_strdup(request->conn->coro, - lua_tostring(L, key_index)); - - lua_pushnil(L); - while (n_headers < (max_headers - 1) && lua_next(L, value_index) != 0) { - if (lua_isstring(L, nested_value_index)) { - headers[n_headers].key = header_name; - headers[n_headers].value = coro_strdup(request->conn->coro, - lua_tostring(L, nested_value_index)); - - n_headers++; - } - - lua_pop(L, 1); - } - } - - lua_pop(L, 1); - } - if (n_headers == (max_headers - 1)) - lua_pop(L, 1); - - headers[n_headers].key = headers[n_headers].value = NULL; - request->response.headers = headers; - - lua_pushinteger(L, (lua_Integer)n_headers); - return 1; -} - -static const struct luaL_reg lwan_request_meta_regs[] = { - { "query_param", req_query_param_cb }, - { "post_param", req_post_param_cb }, - { "yield", req_yield_cb }, - { "set_response", req_set_response_cb }, - { "say", req_say_cb }, - { "send_event", req_send_event_cb }, - { "cookie", req_cookie_cb }, - { "set_headers", req_set_headers_cb }, - { NULL, NULL } -}; - -static struct cache_entry_t *state_create(const char *key __attribute__((unused)), - void *context) -{ - struct lwan_lua_priv_t *priv = context; - struct lwan_lua_state_t *state = malloc(sizeof(*state)); - - if (UNLIKELY(!state)) - return NULL; - - state->L = luaL_newstate(); - if (UNLIKELY(!state->L)) - goto free_state; - - luaL_openlibs(state->L); - - luaL_newmetatable(state->L, request_metatable_name); - luaL_register(state->L, NULL, lwan_request_meta_regs); - lua_setfield(state->L, -1, "__index"); - - if (UNLIKELY(luaL_dofile(state->L, priv->script_file) != 0)) { - lwan_status_error("Error opening Lua script %s", lua_tostring(state->L, -1)); - goto close_lua_state; - } - - return (struct cache_entry_t *)state; - -close_lua_state: - lua_close(state->L); -free_state: - free(state); - return NULL; -} - -static void state_destroy(struct cache_entry_t *entry, - void *context __attribute__((unused))) -{ - struct lwan_lua_state_t *state = (struct lwan_lua_state_t *)entry; - - lua_close(state->L); - free(state); -} - -static struct cache_t *get_or_create_cache(struct lwan_lua_priv_t *priv) -{ - struct cache_t *cache = pthread_getspecific(priv->cache_key); - if (UNLIKELY(!cache)) { - lwan_status_debug("Creating cache for this thread"); - cache = cache_create(state_create, state_destroy, priv, priv->cache_period); - if (UNLIKELY(!cache)) - lwan_status_error("Could not create cache"); - /* FIXME: This cache instance leaks: store it somewhere and - * free it on module shutdown */ - pthread_setspecific(priv->cache_key, cache); - } - return cache; -} - -static void unref_thread(void *data1, void *data2) -{ - lua_State *L = data1; - int thread_ref = (int)(intptr_t)data2; - luaL_unref(L, LUA_REGISTRYINDEX, thread_ref); -} - -static ALWAYS_INLINE const char *get_handle_prefix(lwan_request_t *request, size_t *len) -{ - if (request->flags & REQUEST_METHOD_GET) { - *len = sizeof("handle_get_"); - return "handle_get_"; - } - if (request->flags & REQUEST_METHOD_POST) { - *len = sizeof("handle_post_"); - return "handle_post_"; - } - if (request->flags & REQUEST_METHOD_HEAD) { - *len = sizeof("handle_head_"); - return "handle_head_"; - } - - return NULL; -} - -static bool get_handler_function(lua_State *L, lwan_request_t *request) -{ - size_t handle_prefix_len; - const char *handle_prefix = get_handle_prefix(request, &handle_prefix_len); - if (UNLIKELY(!handle_prefix)) - return false; - - char handler_name[128]; - char *method_name = mempcpy(handler_name, handle_prefix, handle_prefix_len); - - char *url; - size_t url_len; - if (request->url.len) { - url = strdupa(request->url.value); - for (char *c = url; *c; c++) { - if (*c == '/') { - *c = '\0'; - break; - } - if (UNLIKELY(!isalnum(*c) && *c != '_')) - return false; - } - url_len = strlen(url); - } else { - url = "root"; - url_len = 4; - } - - if (UNLIKELY((handle_prefix_len + url_len + 1) > sizeof(handler_name))) - return false; - memcpy(method_name - 1, url, url_len + 1); - - lua_getglobal(L, handler_name); - return lua_isfunction(L, -1); -} - -static void push_request(lua_State *L, lwan_request_t *request) -{ - lwan_request_t **userdata = lua_newuserdata(L, sizeof(lwan_request_t *)); - *userdata = request; - luaL_getmetatable(L, request_metatable_name); - lua_setmetatable(L, -2); -} - -static lua_State *push_newthread(lua_State *L, coro_t *coro) -{ - lua_State *L1 = lua_newthread(L); - if (UNLIKELY(!L1)) - return NULL; - - int thread_ref = luaL_ref(L, LUA_REGISTRYINDEX); - coro_defer2(coro, CORO_DEFER2(unref_thread), L, (void *)(intptr_t)thread_ref); - - return L1; -} - -static lwan_http_status_t -lua_handle_cb(lwan_request_t *request, - lwan_response_t *response, - void *data) -{ - struct lwan_lua_priv_t *priv = data; - - if (UNLIKELY(!priv)) - return HTTP_INTERNAL_ERROR; - - struct cache_t *cache = get_or_create_cache(priv); - if (UNLIKELY(!cache)) - return HTTP_INTERNAL_ERROR; - - struct lwan_lua_state_t *state = (struct lwan_lua_state_t *)cache_coro_get_and_ref_entry( - cache, request->conn->coro, ""); - if (UNLIKELY(!state)) - return HTTP_NOT_FOUND; - - lua_State *L = push_newthread(state->L, request->conn->coro); - if (UNLIKELY(!L)) - return HTTP_INTERNAL_ERROR; - - if (UNLIKELY(!get_handler_function(L, request))) - return HTTP_NOT_FOUND; - - int n_arguments = 1; - push_request(L, request); - response->mime_type = priv->default_type; - while (true) { - switch (lua_resume(L, n_arguments)) { - case LUA_YIELD: - coro_yield(request->conn->coro, CONN_CORO_MAY_RESUME); - n_arguments = 0; - break; - case 0: - return HTTP_OK; - default: - lwan_status_error("Error from Lua script: %s", lua_tostring(L, -1)); - return HTTP_INTERNAL_ERROR; - } - } -} - -static void *lua_init(void *data) -{ - struct lwan_lua_settings_t *settings = data; - struct lwan_lua_priv_t *priv; - - priv = malloc(sizeof(*priv)); - if (!priv) { - lwan_status_error("Could not allocate memory for private Lua struct"); - return NULL; - } - - priv->default_type = strdup( - settings->default_type ? settings->default_type : "text/plain"); - if (!priv->default_type) { - lwan_status_perror("strdup"); - goto out_no_default_type; - } - - if (!settings->script_file) { - lwan_status_error("No Lua script file provided"); - goto out_no_script_file; - } - priv->script_file = strdup(settings->script_file); - if (!priv->script_file) { - lwan_status_perror("strdup"); - goto out_no_script_file; - } - - if (pthread_key_create(&priv->cache_key, NULL)) { - lwan_status_perror("pthread_key_create"); - goto out_key_create; - } - - priv->cache_period = settings->cache_period; - - return priv; - -out_key_create: - free(priv->script_file); -out_no_script_file: - free(priv->default_type); -out_no_default_type: - free(priv); - return NULL; -} - -static void lua_shutdown(void *data) -{ - struct lwan_lua_priv_t *priv = data; - if (priv) { - pthread_key_delete(priv->cache_key); - free(priv->default_type); - free(priv->script_file); - free(priv); - } -} - -static void *lua_init_from_hash(const struct hash *hash) -{ - struct lwan_lua_settings_t settings = { - .default_type = hash_find(hash, "default_type"), - .script_file = hash_find(hash, "script_file"), - .cache_period = parse_time_period(hash_find(hash, "cache_period"), 15) - }; - return lua_init(&settings); -} - -const lwan_module_t *lwan_module_lua(void) -{ - static const lwan_module_t lua_module = { - .name = "lua", - .init = lua_init, - .init_from_hash = lua_init_from_hash, - .shutdown = lua_shutdown, - .handle = lua_handle_cb, - .flags = HANDLER_PARSE_QUERY_STRING - | HANDLER_REMOVE_LEADING_SLASH - | HANDLER_PARSE_COOKIES - }; - - return &lua_module; -} diff --git a/common/lwan-private.h b/common/lwan-private.h deleted file mode 100644 index 6cf547d92..000000000 --- a/common/lwan-private.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#pragma once - -#include "lwan.h" - -void lwan_response_init(void); -void lwan_response_shutdown(void); - -void lwan_socket_init(lwan_t *l); -void lwan_socket_shutdown(lwan_t *l); - -void lwan_thread_init(lwan_t *l); -void lwan_thread_shutdown(lwan_t *l); -void lwan_thread_add_client(lwan_thread_t *t, int fd); - -void lwan_status_init(lwan_t *l); -void lwan_status_shutdown(lwan_t *l); - -void lwan_job_thread_init(void); -void lwan_job_thread_shutdown(void); -void lwan_job_add(bool (*cb)(void *data), void *data); -void lwan_job_del(bool (*cb)(void *data), void *data); - -void lwan_tables_init(void); -void lwan_tables_shutdown(void); - -char *lwan_process_request(lwan_t *l, lwan_request_t *request, - lwan_value_t *buffer, char *next_request); - -#undef static_assert -#if HAVE_STATIC_ASSERT -#define static_assert(expr, msg) _Static_assert(expr, msg) -#else -#define static_assert(expr, msg) -#endif - diff --git a/common/lwan-redirect.c b/common/lwan-redirect.c deleted file mode 100644 index 6ff542b18..000000000 --- a/common/lwan-redirect.c +++ /dev/null @@ -1,74 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include - -#include "lwan.h" -#include "lwan-redirect.h" - -static lwan_http_status_t -redirect_handle_cb(lwan_request_t *request, - lwan_response_t *response, - void *data) -{ - if (UNLIKELY(!data)) - return HTTP_INTERNAL_ERROR; - - lwan_key_value_t *headers = coro_malloc(request->conn->coro, sizeof(*headers) * 2); - if (UNLIKELY(!headers)) - return HTTP_INTERNAL_ERROR; - - headers[0].key = "Location"; - headers[0].value = data; - headers[1].key = NULL; - headers[1].value = NULL; - - response->headers = headers; - - return HTTP_MOVED_PERMANENTLY; -} - -static void *redirect_init(void *data) -{ - struct lwan_redirect_settings_t *settings = data; - return (settings->to) ? strdup(settings->to) : NULL; -} - -static void *redirect_init_from_hash(const struct hash *hash) -{ - struct lwan_redirect_settings_t settings = { - .to = hash_find(hash, "to") - }; - return redirect_init(&settings); -} - -const lwan_module_t *lwan_module_redirect(void) -{ - static const lwan_module_t redirect_module = { - .name = "redirect", - .init = redirect_init, - .init_from_hash = redirect_init_from_hash, - .shutdown = free, - .handle = redirect_handle_cb, - .flags = 0 - }; - - return &redirect_module; -} diff --git a/common/lwan-request.c b/common/lwan-request.c deleted file mode 100644 index 315431e7c..000000000 --- a/common/lwan-request.c +++ /dev/null @@ -1,897 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012-2014 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-config.h" -#include "lwan-http-authorize.h" - -typedef enum { - FINALIZER_DONE, - FINALIZER_TRY_AGAIN, - FINALIZER_YIELD_TRY_AGAIN, - FINALIZER_ERROR_TOO_LARGE -} lwan_read_finalizer_t; - -struct request_parser_helper { - lwan_value_t *buffer; - char *next_request; /* For pipelined requests */ - lwan_value_t accept_encoding; - lwan_value_t if_modified_since; - lwan_value_t range; - lwan_value_t cookie; - - lwan_value_t query_string; - lwan_value_t fragment; - lwan_value_t content_length; - lwan_value_t post_data; - - lwan_value_t content_type; - lwan_value_t authorization; - - int urls_rewritten; - char connection; -}; - -static char decode_hex_digit(char ch) __attribute__((pure)); -static bool is_hex_digit(char ch) __attribute__((pure)); -static unsigned long has_zero_byte(unsigned long n) __attribute__((pure)); -static unsigned long is_space(char ch) __attribute__((pure)); -static char *ignore_leading_whitespace(char *buffer) __attribute__((pure)); -static lwan_request_flags_t get_http_method(const char *buffer) __attribute__((pure)); - -static ALWAYS_INLINE lwan_request_flags_t -get_http_method(const char *buffer) -{ - /* Note: keep in sync in identify_http_method() */ - enum { - HTTP_STR_GET = MULTICHAR_CONSTANT('G','E','T',' '), - HTTP_STR_HEAD = MULTICHAR_CONSTANT('H','E','A','D'), - HTTP_STR_POST = MULTICHAR_CONSTANT('P','O','S','T') - }; - - STRING_SWITCH(buffer) { - case HTTP_STR_GET: - return REQUEST_METHOD_GET; - case HTTP_STR_HEAD: - return REQUEST_METHOD_HEAD; - case HTTP_STR_POST: - return REQUEST_METHOD_POST; - } - - return 0; -} - -static ALWAYS_INLINE char * -identify_http_method(lwan_request_t *request, char *buffer) -{ - lwan_request_flags_t flags = get_http_method(buffer); - static const char sizes[] = { - [0] = 0, - [REQUEST_METHOD_GET] = sizeof("GET ") - 1, - [REQUEST_METHOD_HEAD] = sizeof("HEAD ") - 1, - [REQUEST_METHOD_POST] = sizeof("POST ") - 1, - }; - request->flags |= flags; - return buffer + sizes[flags]; -} - -static ALWAYS_INLINE char -decode_hex_digit(char ch) -{ - return (char)((ch <= '9') ? ch - '0' : (ch & 7) + 9); -} - -static ALWAYS_INLINE bool -is_hex_digit(char ch) -{ - return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'); -} - -static size_t -url_decode(char *str) -{ - if (UNLIKELY(!str)) - return 0; - - char *ch, *decoded; - for (decoded = ch = str; *ch; ch++) { - if (*ch == '%' && LIKELY(is_hex_digit(ch[1]) && is_hex_digit(ch[2]))) { - char tmp = (char)(decode_hex_digit(ch[1]) << 4 | decode_hex_digit(ch[2])); - if (UNLIKELY(!tmp)) - return 0; - *decoded++ = tmp; - ch += 2; - } else if (*ch == '+') { - *decoded++ = ' '; - } else { - *decoded++ = *ch; - } - } - - *decoded = '\0'; - return (size_t)(decoded - str); -} - -static int -key_value_compare_qsort_key(const void *a, const void *b) -{ - return strcmp(((lwan_key_value_t *)a)->key, ((lwan_key_value_t *)b)->key); -} - -static void -parse_key_values(lwan_request_t *request, - lwan_value_t *helper_value, lwan_key_value_t **base, size_t *len, - size_t (*decode_value)(char *value), const char separator) -{ - char *ptr = helper_value->value; - lwan_key_value_t kvs[256]; - size_t values = 0; - bool last = true; - - if (!helper_value->len) - return; - - do { - char *key, *value; - - while (*ptr && (*ptr == ' ' || *ptr == '\t' || *ptr == separator)) - ptr++; - if (UNLIKELY(*ptr == '\0')) - return; - - key = ptr; - value = strchr(ptr, '='); - if (UNLIKELY(!value)) - return; - *value = '\0'; - value++; - - ptr = strchr(value, separator); - if (ptr) { - *ptr = '\0'; - ptr++; - - last = false; - } else { - last = true; - } - - if (UNLIKELY(!decode_value(key) || !decode_value(value))) - return; - - kvs[values].key = key; - kvs[values].value = value; - - values++; - } while (!last && values < N_ELEMENTS(kvs)); - - kvs[values].key = kvs[values].value = NULL; - - lwan_key_value_t *kv = coro_malloc(request->conn->coro, - (1 + values) * sizeof(lwan_key_value_t)); - if (LIKELY(kv)) { - qsort(kvs, values, sizeof(lwan_key_value_t), key_value_compare_qsort_key); - *base = memcpy(kv, kvs, (1 + values) * sizeof(lwan_key_value_t)); - *len = values; - } -} - -static size_t -identity_decode(char *input __attribute__((unused))) -{ - return 1; -} - -static void -parse_cookies(lwan_request_t *request, struct request_parser_helper *helper) -{ - parse_key_values(request, &helper->cookie, &request->cookies.base, - &request->cookies.len, identity_decode, ';'); -} - -static void -parse_query_string(lwan_request_t *request, struct request_parser_helper *helper) -{ - parse_key_values(request, &helper->query_string, - &request->query_params.base, &request->query_params.len, - url_decode, '&'); -} - -static void -parse_post_data(lwan_request_t *request, struct request_parser_helper *helper) -{ - static const char content_type[] = "application/x-www-form-urlencoded"; - - if (helper->content_type.len != sizeof(content_type) - 1) - return; - if (UNLIKELY(strcmp(helper->content_type.value, content_type))) - return; - - parse_key_values(request, &helper->post_data, - &request->post_data.base, &request->post_data.len, - url_decode, '&'); -} - -static void -parse_fragment_and_query(lwan_request_t *request, - struct request_parser_helper *helper, const char *space) -{ - /* Most of the time, fragments are small -- so search backwards */ - char *fragment = memrchr(request->url.value, '#', request->url.len); - if (fragment) { - *fragment = '\0'; - helper->fragment.value = fragment + 1; - helper->fragment.len = (size_t)(space - fragment - 1); - request->url.len -= helper->fragment.len + 1; - } - - /* Most of the time, query string values are larger than the URL, so - search from the beginning */ - char *query_string = memchr(request->url.value, '?', request->url.len); - if (query_string) { - *query_string = '\0'; - helper->query_string.value = query_string + 1; - helper->query_string.len = (size_t)((fragment ? fragment : space) - query_string - 1); - request->url.len -= helper->query_string.len + 1; - } -} - -static char * -identify_http_path(lwan_request_t *request, char *buffer, - struct request_parser_helper *helper) -{ - static const size_t minimal_request_line_len = sizeof("/ HTTP/1.0") - 1; - - char *end_of_line = memchr(buffer, '\r', - (helper->buffer->len - (size_t)(buffer - helper->buffer->value))); - if (UNLIKELY(!end_of_line)) - return NULL; - if (UNLIKELY((size_t)(end_of_line - buffer) < minimal_request_line_len)) - return NULL; - *end_of_line = '\0'; - - char *space = end_of_line - sizeof("HTTP/X.X"); - if (UNLIKELY(*(space + 1) != 'H')) /* assume HTTP/X.Y */ - return NULL; - *space = '\0'; - - if (UNLIKELY(*(space + 6) != '1')) - return NULL; - - if (*(space + 8) == '0') - request->flags |= REQUEST_IS_HTTP_1_0; - - if (UNLIKELY(*buffer != '/')) - return NULL; - - request->url.value = buffer; - request->url.len = (size_t)(space - buffer); - - parse_fragment_and_query(request, helper, space); - - request->original_url = request->url; - - return end_of_line + 1; -} - -#define MATCH_HEADER(hdr) \ - do { \ - p += sizeof(hdr) - 1; \ - if (UNLIKELY(p >= buffer_end)) /* reached the end of header blocks */ \ - return NULL; \ - \ - if (UNLIKELY(string_as_int16(p) != HTTP_HDR_COLON_SPACE)) \ - goto did_not_match; \ - p += 2; \ - \ - char *end = strchr(p, '\r'); \ - if (UNLIKELY(!end)) \ - goto did_not_match; \ - \ - *end = '\0'; \ - value = p; \ - length = (size_t)(end - value); \ - \ - p = end + 1; \ - if (UNLIKELY(*p != '\n')) \ - goto did_not_match; \ - } while (0) - -#define CASE_HEADER(hdr_const,hdr_name) \ - case hdr_const: MATCH_HEADER(hdr_name); - -static char * -parse_headers(struct request_parser_helper *helper, char *buffer, char *buffer_end) -{ - enum { - HTTP_HDR_COLON_SPACE = MULTICHAR_CONSTANT_SMALL(':', ' '), - HTTP_HDR_REQUEST_END = MULTICHAR_CONSTANT_SMALL('\r','\n'), - HTTP_HDR_ENCODING = MULTICHAR_CONSTANT_L('-','E','n','c'), - HTTP_HDR_LENGTH = MULTICHAR_CONSTANT_L('-','L','e','n'), - HTTP_HDR_TYPE = MULTICHAR_CONSTANT_L('-','T','y','p'), - HTTP_HDR_ACCEPT = MULTICHAR_CONSTANT_L('A','c','c','e'), - HTTP_HDR_AUTHORIZATION = MULTICHAR_CONSTANT_L('A','u','t','h'), - HTTP_HDR_CONNECTION = MULTICHAR_CONSTANT_L('C','o','n','n'), - HTTP_HDR_CONTENT = MULTICHAR_CONSTANT_L('C','o','n','t'), - HTTP_HDR_COOKIE = MULTICHAR_CONSTANT_L('C','o','o','k'), - HTTP_HDR_IF_MODIFIED_SINCE = MULTICHAR_CONSTANT_L('I','f','-','M'), - HTTP_HDR_RANGE = MULTICHAR_CONSTANT_L('R','a','n','g') - }; - - for (char *p = buffer; *p; buffer = ++p) { - char *value; - size_t length; - -retry: - if ((p + sizeof(int32_t)) >= buffer_end) - break; - - STRING_SWITCH_SMALL(p) { - case HTTP_HDR_REQUEST_END: - *p = '\0'; - helper->next_request = p + sizeof("\r\n") - 1; - return p; - } - - STRING_SWITCH_L(p) { - CASE_HEADER(HTTP_HDR_ENCODING, "-Encoding") - helper->accept_encoding.value = value; - helper->accept_encoding.len = length; - break; - CASE_HEADER(HTTP_HDR_TYPE, "-Type") - helper->content_type.value = value; - helper->content_type.len = length; - break; - CASE_HEADER(HTTP_HDR_LENGTH, "-Length") - helper->content_length.value = value; - helper->content_length.len = length; - break; - case HTTP_HDR_ACCEPT: - p += sizeof("Accept") - 1; - goto retry; - CASE_HEADER(HTTP_HDR_AUTHORIZATION, "Authorization") - helper->authorization.value = value; - helper->authorization.len = length; - break; - CASE_HEADER(HTTP_HDR_CONNECTION, "Connection") - helper->connection = (*value | 0x20); - break; - case HTTP_HDR_CONTENT: - p += sizeof("Content") - 1; - goto retry; - CASE_HEADER(HTTP_HDR_COOKIE, "Cookie") - helper->cookie.value = value; - helper->cookie.len = length; - break; - CASE_HEADER(HTTP_HDR_IF_MODIFIED_SINCE, "If-Modified-Since") - helper->if_modified_since.value = value; - helper->if_modified_since.len = length; - break; - CASE_HEADER(HTTP_HDR_RANGE, "Range") - helper->range.value = value; - helper->range.len = length; - break; - } -did_not_match: - p = memchr(p, '\n', (size_t)(buffer_end - p)); - if (!p) - break; - } - - return buffer; -} - -#undef CASE_HEADER -#undef MATCH_HEADER - -static void -parse_if_modified_since(lwan_request_t *request, struct request_parser_helper *helper) -{ - if (UNLIKELY(!helper->if_modified_since.len)) - return; - - struct tm t; - char *processed = strptime(helper->if_modified_since.value, - "%a, %d %b %Y %H:%M:%S GMT", &t); - - if (UNLIKELY(!processed)) - return; - if (UNLIKELY(*processed)) - return; - - request->header.if_modified_since = timegm(&t); -} - -static void -parse_range(lwan_request_t *request, struct request_parser_helper *helper) -{ - if (UNLIKELY(helper->range.len <= (sizeof("bytes=") - 1))) - return; - - char *range = helper->range.value; - if (UNLIKELY(strncmp(range, "bytes=", sizeof("bytes=") - 1))) - return; - - range += sizeof("bytes=") - 1; - off_t from, to; - - if (sscanf(range, "%"PRIu64"-%"PRIu64, &from, &to) == 2) { - request->header.range.from = from; - request->header.range.to = to; - } else if (sscanf(range, "-%"PRIu64, &to) == 1) { - request->header.range.from = 0; - request->header.range.to = to; - } else if (sscanf(range, "%"PRIu64"-", &from) == 1) { - request->header.range.from = from; - request->header.range.to = -1; - } else { - request->header.range.from = -1; - request->header.range.to = -1; - } -} - -static void -parse_accept_encoding(lwan_request_t *request, struct request_parser_helper *helper) -{ - if (!helper->accept_encoding.len) - return; - - enum { - ENCODING_DEFL1 = MULTICHAR_CONSTANT('d','e','f','l'), - ENCODING_DEFL2 = MULTICHAR_CONSTANT(' ','d','e','f'), - ENCODING_GZIP1 = MULTICHAR_CONSTANT('g','z','i','p'), - ENCODING_GZIP2 = MULTICHAR_CONSTANT(' ','g','z','i') - }; - - for (char *p = helper->accept_encoding.value; p && *p; p++) { - STRING_SWITCH(p) { - case ENCODING_DEFL1: - case ENCODING_DEFL2: - request->flags |= REQUEST_ACCEPT_DEFLATE; - break; - case ENCODING_GZIP1: - case ENCODING_GZIP2: - request->flags |= REQUEST_ACCEPT_GZIP; - break; - } - - if (!(p = strchr(p, ','))) - break; - } -} - -static ALWAYS_INLINE unsigned long -has_zero_byte(unsigned long n) -{ - return ((n - 0x01010101UL) & ~n) & 0x80808080UL; -} - -static ALWAYS_INLINE unsigned long -is_space(char ch) -{ - return has_zero_byte((0x1010101UL * (unsigned long)ch) ^ 0x090a0d20UL); -} - -static ALWAYS_INLINE char * -ignore_leading_whitespace(char *buffer) -{ - while (*buffer && is_space(*buffer)) - buffer++; - return buffer; -} - -static ALWAYS_INLINE void -compute_keep_alive_flag(lwan_request_t *request, struct request_parser_helper *helper) -{ - bool is_keep_alive; - if (request->flags & REQUEST_IS_HTTP_1_0) - is_keep_alive = (helper->connection == 'k'); - else - is_keep_alive = (helper->connection != 'c'); - if (is_keep_alive) - request->conn->flags |= CONN_KEEP_ALIVE; - else - request->conn->flags &= ~CONN_KEEP_ALIVE; -} - -static lwan_http_status_t read_from_request_socket(lwan_request_t *request, - lwan_value_t *buffer, struct request_parser_helper *helper, const size_t buffer_size, - lwan_read_finalizer_t (*finalizer)(size_t total_read, size_t buffer_size, struct request_parser_helper *helper)) -{ - ssize_t n; - size_t total_read = 0; - int packets_remaining = 16; - - if (helper->next_request) { - buffer->len -= (size_t)(helper->next_request - buffer->value); - /* FIXME: This memmove() could be eventually removed if a better - * stucture were used for the request buffer. */ - memmove(buffer->value, helper->next_request, buffer->len); - total_read = buffer->len; - goto try_to_finalize; - } - - for (; packets_remaining > 0; packets_remaining--) { - n = read(request->fd, buffer->value + total_read, - (size_t)(buffer_size - total_read)); - /* Client has shutdown orderly, nothing else to do; kill coro */ - if (UNLIKELY(n == 0)) { - coro_yield(request->conn->coro, CONN_CORO_ABORT); - __builtin_unreachable(); - } - - if (UNLIKELY(n < 0)) { - switch (errno) { - case EAGAIN: - case EINTR: -yield_and_read_again: - request->conn->flags |= CONN_MUST_READ; - coro_yield(request->conn->coro, CONN_CORO_MAY_RESUME); - continue; - } - - /* Unexpected error before reading anything */ - if (UNLIKELY(!total_read)) - return HTTP_BAD_REQUEST; - - /* Unexpected error, kill coro */ - coro_yield(request->conn->coro, CONN_CORO_ABORT); - __builtin_unreachable(); - } - - total_read += (size_t)n; - buffer->len = (size_t)total_read; - -try_to_finalize: - switch (finalizer(total_read, buffer_size, helper)) { - case FINALIZER_DONE: - request->conn->flags &= ~CONN_MUST_READ; - buffer->value[buffer->len] = '\0'; - return HTTP_OK; - case FINALIZER_TRY_AGAIN: - continue; - case FINALIZER_YIELD_TRY_AGAIN: - goto yield_and_read_again; - case FINALIZER_ERROR_TOO_LARGE: - return HTTP_TOO_LARGE; - } - } - - /* - * packets_remaining reached zero: return a timeout error to avoid clients - * being intentionally slow and hogging the server. - * - * FIXME: What should be the best approach? Error with 408, or give some more - * time by fiddling with the connection's time to die? - */ - - return HTTP_TIMEOUT; -} - -static lwan_read_finalizer_t read_request_finalizer(size_t total_read, - size_t buffer_size, struct request_parser_helper *helper) -{ - if (UNLIKELY(total_read < 4)) - return FINALIZER_YIELD_TRY_AGAIN; - - if (UNLIKELY(total_read == buffer_size)) - return FINALIZER_ERROR_TOO_LARGE; - - if (LIKELY(helper->next_request)) { - helper->next_request = NULL; - return FINALIZER_DONE; - } - - if (LIKELY(!memcmp(helper->buffer->value + total_read - 4, "\r\n\r\n", 4))) - return FINALIZER_DONE; - - if (get_http_method(helper->buffer->value) == REQUEST_METHOD_POST) { - char *post_data_separator = strrchr(helper->buffer->value, '\n'); - if (post_data_separator) { - if (LIKELY(!memcmp(post_data_separator - 3, "\r\n\r", 3))) - return FINALIZER_DONE; - } - } - - return FINALIZER_TRY_AGAIN; -} - -static ALWAYS_INLINE lwan_http_status_t -read_request(lwan_request_t *request, struct request_parser_helper *helper) -{ - return read_from_request_socket(request, helper->buffer, helper, - DEFAULT_BUFFER_SIZE, read_request_finalizer); -} - -static lwan_http_status_t -read_post_data(lwan_request_t *request __attribute__((unused)), - struct request_parser_helper *helper, - char *buffer __attribute__((unused))) -{ - long parsed_length; - - if (UNLIKELY(!helper->next_request)) - return HTTP_BAD_REQUEST; - - if (UNLIKELY(!helper->content_length.value)) - return HTTP_BAD_REQUEST; - - parsed_length = parse_long(helper->content_length.value, DEFAULT_BUFFER_SIZE); - if (UNLIKELY(parsed_length > DEFAULT_BUFFER_SIZE)) - return HTTP_TOO_LARGE; - if (UNLIKELY(parsed_length < 0)) - return HTTP_BAD_REQUEST; - - size_t post_data_size = (size_t)parsed_length; - char *buffer_end = helper->buffer->value + helper->buffer->len; - size_t have = (size_t)(ptrdiff_t)(buffer_end - helper->next_request); - - if (have == post_data_size) { - helper->post_data.value = helper->next_request; - helper->post_data.len = post_data_size; - helper->next_request += post_data_size; - return HTTP_OK; - } - - if (post_data_size > have) - return HTTP_TOO_LARGE; - - return HTTP_NOT_IMPLEMENTED; -} - -static lwan_http_status_t -parse_http_request(lwan_request_t *request, struct request_parser_helper *helper) -{ - char *buffer; - - buffer = ignore_leading_whitespace(helper->buffer->value); - if (UNLIKELY(!*buffer)) - return HTTP_BAD_REQUEST; - - char *path = identify_http_method(request, buffer); - if (UNLIKELY(buffer == path)) - return HTTP_NOT_ALLOWED; - - buffer = identify_http_path(request, path, helper); - if (UNLIKELY(!buffer)) - return HTTP_BAD_REQUEST; - - buffer = parse_headers(helper, buffer, helper->buffer->value + helper->buffer->len); - if (UNLIKELY(!buffer)) - return HTTP_BAD_REQUEST; - - size_t decoded_len = url_decode(request->url.value); - if (UNLIKELY(!decoded_len)) - return HTTP_BAD_REQUEST; - request->original_url.len = request->url.len = decoded_len; - - compute_keep_alive_flag(request, helper); - - if (request->flags & REQUEST_METHOD_POST) { - lwan_http_status_t status = read_post_data(request, helper, buffer); - if (UNLIKELY(status != HTTP_OK)) - return status; - } - - return HTTP_OK; -} - -static lwan_http_status_t -prepare_for_response(lwan_url_map_t *url_map, - lwan_request_t *request, - struct request_parser_helper *helper) -{ - request->url.value += url_map->prefix_len; - request->url.len -= url_map->prefix_len; - - if (url_map->flags & HANDLER_PARSE_QUERY_STRING) - parse_query_string(request, helper); - - if (url_map->flags & HANDLER_PARSE_IF_MODIFIED_SINCE) - parse_if_modified_since(request, helper); - - if (url_map->flags & HANDLER_PARSE_RANGE) - parse_range(request, helper); - - if (url_map->flags & HANDLER_PARSE_ACCEPT_ENCODING) - parse_accept_encoding(request, helper); - - if (url_map->flags & HANDLER_PARSE_COOKIES) - parse_cookies(request, helper); - - if (request->flags & REQUEST_METHOD_POST) { - if (url_map->flags & HANDLER_PARSE_POST_DATA) - parse_post_data(request, helper); - else - return HTTP_NOT_ALLOWED; - } - - if (url_map->flags & HANDLER_MUST_AUTHORIZE) { - if (!lwan_http_authorize(request, - &helper->authorization, - url_map->authorization.realm, - url_map->authorization.password_file)) - return HTTP_NOT_AUTHORIZED; - } - - if (url_map->flags & HANDLER_REMOVE_LEADING_SLASH) { - while (*request->url.value == '/' && request->url.len > 0) { - ++request->url.value; - --request->url.len; - } - } - - return HTTP_OK; -} - -static bool -handle_rewrite(lwan_request_t *request, struct request_parser_helper *helper) -{ - request->flags &= ~RESPONSE_URL_REWRITTEN; - - parse_fragment_and_query(request, helper, - request->url.value + request->url.len); - - helper->urls_rewritten++; - if (UNLIKELY(helper->urls_rewritten > 4)) { - lwan_default_response(request, HTTP_INTERNAL_ERROR); - return false; - } - - return true; -} -char * -lwan_process_request(lwan_t *l, lwan_request_t *request, - lwan_value_t *buffer, char *next_request) -{ - lwan_http_status_t status; - lwan_url_map_t *url_map; - - struct request_parser_helper helper = { - .buffer = buffer, - .next_request = next_request - }; - - status = read_request(request, &helper); - if (UNLIKELY(status != HTTP_OK)) { - /* This request was bad, but maybe there's a good one in the - * pipeline. */ - if (status == HTTP_BAD_REQUEST && helper.next_request) - goto out; - - /* Response here can be: HTTP_TOO_LARGE, HTTP_BAD_REQUEST (without - * next request), or HTTP_TIMEOUT. Nothing to do, just abort the - * coroutine. */ - lwan_default_response(request, status); - coro_yield(request->conn->coro, CONN_CORO_ABORT); - __builtin_unreachable(); - } - - status = parse_http_request(request, &helper); - if (UNLIKELY(status != HTTP_OK)) { - lwan_default_response(request, status); - goto out; - } - -lookup_again: - url_map = lwan_trie_lookup_prefix(&l->url_map_trie, request->url.value); - if (UNLIKELY(!url_map)) { - lwan_default_response(request, HTTP_NOT_FOUND); - goto out; - } - - status = prepare_for_response(url_map, request, &helper); - if (UNLIKELY(status != HTTP_OK)) { - lwan_default_response(request, status); - goto out; - } - - status = url_map->handler(request, &request->response, url_map->data); - if (UNLIKELY(url_map->flags & HANDLER_CAN_REWRITE_URL)) { - if (request->flags & RESPONSE_URL_REWRITTEN) { - if (LIKELY(handle_rewrite(request, &helper))) - goto lookup_again; - goto out; - } - } - - lwan_response(request, status); - -out: - return helper.next_request; -} - -static const char * -value_array_bsearch(lwan_key_value_t *base, const size_t len, const char *key) -{ - if (UNLIKELY(!len)) - return NULL; - - size_t lower_bound = 0; - size_t upper_bound = len; - size_t key_len = strlen(key); - - while (lower_bound < upper_bound) { - /* lower_bound + upper_bound will never overflow */ - size_t idx = (lower_bound + upper_bound) / 2; - lwan_key_value_t *ptr = base + idx; - int cmp = strncmp(key, ptr->key, key_len); - if (LIKELY(!cmp)) - return ptr->value; - if (cmp > 0) - lower_bound = idx + 1; - else - upper_bound = idx; - } - - return NULL; -} - -const char * -lwan_request_get_query_param(lwan_request_t *request, const char *key) -{ - return value_array_bsearch(request->query_params.base, - request->query_params.len, key); -} - -const char * -lwan_request_get_post_param(lwan_request_t *request, const char *key) -{ - return value_array_bsearch(request->post_data.base, - request->post_data.len, key); -} - -const char * -lwan_request_get_cookie(lwan_request_t *request, const char *key) -{ - return value_array_bsearch(request->cookies.base, - request->cookies.len, key); -} - -ALWAYS_INLINE int -lwan_connection_get_fd(const lwan_t *lwan, const lwan_connection_t *conn) -{ - return (int)(ptrdiff_t)(conn - lwan->conns); -} - -const char * -lwan_request_get_remote_address(lwan_request_t *request, - char buffer[static INET6_ADDRSTRLEN]) -{ - struct sockaddr_storage sock_addr = { 0 }; - socklen_t sock_len = sizeof(struct sockaddr_storage); - if (UNLIKELY(getpeername(request->fd, (struct sockaddr *)&sock_addr, &sock_len) < 0)) - return NULL; - - if (sock_addr.ss_family == AF_INET) - return inet_ntop(AF_INET, &((struct sockaddr_in *)&sock_addr)->sin_addr, - buffer, INET6_ADDRSTRLEN); - return inet_ntop(AF_INET6, &((struct sockaddr_in6 *)&sock_addr)->sin6_addr, - buffer, INET6_ADDRSTRLEN); -} diff --git a/common/lwan-response.c b/common/lwan-response.c deleted file mode 100644 index 3a2a97d55..000000000 --- a/common/lwan-response.c +++ /dev/null @@ -1,453 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include - -#include "int-to-str.h" -#include "lwan.h" -#include "lwan-io-wrappers.h" -#include "lwan-template.h" - -static lwan_tpl_t *error_template = NULL; - -static const char *error_template_str = "" \ - "" \ - "" \ - "
" \ - "
" \ - "

{{short_message}}

" \ - "
" \ - "

{{long_message}}

" \ - "
" \ - "
" \ - "
" \ - "" \ - ""; - -struct error_template_t { - const char *short_message; - const char *long_message; -}; - -void -lwan_response_init(void) -{ - static lwan_var_descriptor_t error_descriptor[] = { - TPL_VAR_STR(struct error_template_t, short_message), - TPL_VAR_STR(struct error_template_t, long_message), - TPL_VAR_SENTINEL - }; - - assert(!error_template); - - lwan_status_debug("Initializing default response"); - - error_template = lwan_tpl_compile_string(error_template_str, error_descriptor); - if (UNLIKELY(!error_template)) - lwan_status_critical_perror("lwan_tpl_compile_string"); -} - -void -lwan_response_shutdown(void) -{ - lwan_status_debug("Shutting down response"); - assert(error_template); - lwan_tpl_free(error_template); -} - -#ifndef NDEBUG -static const char * -get_request_method(lwan_request_t *request) -{ - if (request->flags & REQUEST_METHOD_GET) - return "GET"; - if (request->flags & REQUEST_METHOD_HEAD) - return "HEAD"; - if (request->flags & REQUEST_METHOD_POST) - return "POST"; - return "UNKNOWN"; -} - -static void -log_request(lwan_request_t *request, lwan_http_status_t status) -{ - char ip_buffer[INET6_ADDRSTRLEN]; - - lwan_status_debug("%s [%s] \"%s %s HTTP/%s\" %d %s", - lwan_request_get_remote_address(request, ip_buffer), - request->conn->thread->date.date, - get_request_method(request), - request->original_url.value, - request->flags & REQUEST_IS_HTTP_1_0 ? "1.0" : "1.1", - status, - request->response.mime_type); -} -#else -#define log_request(...) -#endif - -void -lwan_response(lwan_request_t *request, lwan_http_status_t status) -{ - char headers[DEFAULT_HEADERS_SIZE]; - - if (request->flags & RESPONSE_CHUNKED_ENCODING) { - /* Send last, 0-sized chunk */ - if (UNLIKELY(!strbuf_reset_length(request->response.buffer))) - coro_yield(request->conn->coro, CONN_CORO_ABORT); - lwan_response_send_chunk(request); - log_request(request, status); - return; - } - - if (UNLIKELY(request->flags & RESPONSE_SENT_HEADERS)) { - lwan_status_debug("Headers already sent, ignoring call"); - return; - } - - /* Requests without a MIME Type are errors from handlers that - should just be handled by lwan_default_response(). */ - if (UNLIKELY(!request->response.mime_type)) { - lwan_default_response(request, status); - return; - } - - log_request(request, status); - - if (request->response.stream.callback) { - lwan_http_status_t callback_status; - - callback_status = request->response.stream.callback(request, - request->response.stream.data); - /* Reset it after it has been called to avoid eternal recursion on errors */ - request->response.stream.callback = NULL; - - if (callback_status >= HTTP_BAD_REQUEST) /* Status < 400: success */ - lwan_default_response(request, callback_status); - return; - } - - size_t header_len = lwan_prepare_response_header(request, status, headers, sizeof(headers)); - if (UNLIKELY(!header_len)) { - lwan_default_response(request, HTTP_INTERNAL_ERROR); - return; - } - - if (request->flags & REQUEST_METHOD_HEAD) { - lwan_write(request, headers, header_len); - return; - } - - struct iovec response_vec[] = { - { .iov_base = headers, .iov_len = header_len }, - { .iov_base = strbuf_get_buffer(request->response.buffer), .iov_len = strbuf_get_length(request->response.buffer) } - }; - - lwan_writev(request, response_vec, N_ELEMENTS(response_vec)); -} - -void -lwan_default_response(lwan_request_t *request, lwan_http_status_t status) -{ - request->response.mime_type = "text/html"; - - lwan_tpl_apply_with_buffer(error_template, request->response.buffer, - &(struct error_template_t) { - .short_message = lwan_http_status_as_string(status), - .long_message = lwan_http_status_as_descriptive_string(status) - }); - - lwan_response(request, status); -} - -#define RETURN_0_ON_OVERFLOW(len_) \ - if (UNLIKELY(p_headers + (len_) >= p_headers_end)) return 0 - -#define APPEND_STRING_LEN(const_str_,len_) \ - do { \ - RETURN_0_ON_OVERFLOW(len_); \ - p_headers = mempcpy(p_headers, (const_str_), (len_)); \ - } while(0) - -#define APPEND_STRING(str_) \ - do { \ - size_t len = strlen(str_); \ - APPEND_STRING_LEN((str_), len); \ - } while(0) - -#define APPEND_CHAR(value_) \ - do { \ - RETURN_0_ON_OVERFLOW(1); \ - *p_headers++ = (value_); \ - } while(0) - -#define APPEND_CHAR_NOCHECK(value_) \ - *p_headers++ = (value_) - -#define APPEND_UINT(value_) \ - do { \ - size_t len; \ - char *tmp = uint_to_string((value_), buffer, &len); \ - RETURN_0_ON_OVERFLOW(len); \ - APPEND_STRING_LEN(tmp, len); \ - } while(0) - -#define APPEND_CONSTANT(const_str_) \ - APPEND_STRING_LEN((const_str_), sizeof(const_str_) - 1) - -ALWAYS_INLINE size_t -lwan_prepare_response_header(lwan_request_t *request, lwan_http_status_t status, char headers[], size_t headers_buf_size) -{ - char *p_headers; - char *p_headers_end = headers + headers_buf_size; - char buffer[INT_TO_STR_BUFFER_SIZE]; - bool date_overridden = false; - bool expires_overridden = false; - - p_headers = headers; - - if (request->flags & REQUEST_IS_HTTP_1_0) - APPEND_CONSTANT("HTTP/1.0 "); - else - APPEND_CONSTANT("HTTP/1.1 "); - APPEND_STRING(lwan_http_status_as_string_with_code(status)); - - if (request->flags & RESPONSE_CHUNKED_ENCODING) { - APPEND_CONSTANT("\r\nTransfer-Encoding: chunked"); - } else if (request->flags & RESPONSE_NO_CONTENT_LENGTH) { - /* Do nothing. */ - } else { - APPEND_CONSTANT("\r\nContent-Length: "); - if (request->response.stream.callback) - APPEND_UINT(request->response.content_length); - else - APPEND_UINT(strbuf_get_length(request->response.buffer)); - } - - APPEND_CONSTANT("\r\nContent-Type: "); - APPEND_STRING(request->response.mime_type); - - if (request->conn->flags & CONN_KEEP_ALIVE) - APPEND_CONSTANT("\r\nConnection: keep-alive"); - else - APPEND_CONSTANT("\r\nConnection: close"); - - if ((status < HTTP_BAD_REQUEST && request->response.headers)) { - lwan_key_value_t *header; - - for (header = request->response.headers; header->key; header++) { - if (UNLIKELY(!strcmp(header->key, "Server"))) - continue; - if (UNLIKELY(!strcmp(header->key, "Date"))) - date_overridden = true; - if (UNLIKELY(!strcmp(header->key, "Expires"))) - expires_overridden = true; - - RETURN_0_ON_OVERFLOW(4); - APPEND_CHAR_NOCHECK('\r'); - APPEND_CHAR_NOCHECK('\n'); - APPEND_STRING(header->key); - APPEND_CHAR_NOCHECK(':'); - APPEND_CHAR_NOCHECK(' '); - APPEND_STRING(header->value); - } - } else if (status == HTTP_NOT_AUTHORIZED) { - lwan_key_value_t *header; - - for (header = request->response.headers; header->key; header++) { - if (!strcmp(header->key, "WWW-Authenticate")) { - APPEND_CONSTANT("\r\nWWW-Authenticate: "); - APPEND_STRING(header->value); - break; - } - } - } - - if (LIKELY(!date_overridden)) { - APPEND_CONSTANT("\r\nDate: "); - APPEND_STRING_LEN(request->conn->thread->date.date, 29); - } - - if (LIKELY(!expires_overridden)) { - APPEND_CONSTANT("\r\nExpires: "); - APPEND_STRING_LEN(request->conn->thread->date.expires, 29); - } - - APPEND_CONSTANT("\r\nServer: lwan\r\n\r\n\0"); - - return (size_t)(p_headers - headers - 1); -} - -#undef APPEND_CHAR -#undef APPEND_CHAR_NOCHECK -#undef APPEND_CONSTANT -#undef APPEND_STRING -#undef APPEND_STRING_LEN -#undef APPEND_UINT -#undef RETURN_0_ON_OVERFLOW - -bool -lwan_response_set_chunked(lwan_request_t *request, lwan_http_status_t status) -{ - char buffer[DEFAULT_BUFFER_SIZE]; - size_t buffer_len; - - if (request->flags & RESPONSE_SENT_HEADERS) - return false; - - request->flags |= RESPONSE_CHUNKED_ENCODING; - buffer_len = lwan_prepare_response_header(request, status, - buffer, DEFAULT_BUFFER_SIZE); - if (UNLIKELY(!buffer_len)) - return false; - - request->flags |= RESPONSE_SENT_HEADERS; - lwan_send(request, buffer, buffer_len, MSG_MORE); - - return true; -} - -void -lwan_response_send_chunk(lwan_request_t *request) -{ - if (!(request->flags & RESPONSE_SENT_HEADERS)) { - if (UNLIKELY(!lwan_response_set_chunked(request, HTTP_OK))) - return; - } - - size_t buffer_len = strbuf_get_length(request->response.buffer); - if (UNLIKELY(!buffer_len)) { - static const char last_chunk[] = "0\r\n\r\n"; - lwan_send(request, last_chunk, sizeof(last_chunk) - 1, 0); - return; - } - - char chunk_size[3 * sizeof(size_t) + 2]; - int converted_len = snprintf(chunk_size, sizeof(chunk_size), "%zx\r\n", buffer_len); - if (UNLIKELY(converted_len < 0 || (size_t)converted_len >= sizeof(chunk_size))) - goto abort_coro; - size_t chunk_size_len = (size_t)converted_len; - - struct iovec chunk_vec[] = { - { .iov_base = chunk_size, .iov_len = chunk_size_len }, - { .iov_base = strbuf_get_buffer(request->response.buffer), .iov_len = buffer_len }, - { .iov_base = "\r\n", .iov_len = 2 } - }; - - lwan_writev(request, chunk_vec, N_ELEMENTS(chunk_vec)); - - if (LIKELY(strbuf_reset_length(request->response.buffer))) { - coro_yield(request->conn->coro, CONN_CORO_MAY_RESUME); - return; - } - -abort_coro: - coro_yield(request->conn->coro, CONN_CORO_ABORT); - __builtin_unreachable(); -} - -bool -lwan_response_set_event_stream(lwan_request_t *request, - lwan_http_status_t status) -{ - char buffer[DEFAULT_BUFFER_SIZE]; - size_t buffer_len; - - if (request->flags & RESPONSE_SENT_HEADERS) - return false; - - request->response.mime_type = "text/event-stream"; - request->flags |= RESPONSE_NO_CONTENT_LENGTH; - buffer_len = lwan_prepare_response_header(request, status, - buffer, DEFAULT_BUFFER_SIZE); - if (UNLIKELY(!buffer_len)) - return false; - - request->flags |= RESPONSE_SENT_HEADERS; - lwan_send(request, buffer, buffer_len, MSG_MORE); - - return true; -} - -void -lwan_response_send_event(lwan_request_t *request, const char *event) -{ - if (!(request->flags & RESPONSE_SENT_HEADERS)) { - if (UNLIKELY(!lwan_response_set_event_stream(request, HTTP_OK))) - return; - } - - struct iovec vec[6]; - int last = 0; - - if (event) { - vec[last].iov_base = "event: "; - vec[last].iov_len = sizeof("event: ") - 1; - last++; - - vec[last].iov_base = (char *)event; - vec[last].iov_len = strlen(event); - last++; - - vec[last].iov_base = "\r\n"; - vec[last].iov_len = 2; - last++; - } - - size_t buffer_len = strbuf_get_length(request->response.buffer); - if (buffer_len) { - vec[last].iov_base = "data: "; - vec[last].iov_len = sizeof("data: ") - 1; - last++; - - vec[last].iov_base = strbuf_get_buffer(request->response.buffer); - vec[last].iov_len = buffer_len; - last++; - - } - - vec[last].iov_base = "\r\n\r\n"; - vec[last].iov_len = 4; - last++; - - lwan_writev(request, vec, last); - - if (UNLIKELY(!strbuf_reset_length(request->response.buffer))) { - coro_yield(request->conn->coro, CONN_CORO_ABORT); - __builtin_unreachable(); - } - - coro_yield(request->conn->coro, CONN_CORO_MAY_RESUME); -} diff --git a/common/lwan-rewrite.c b/common/lwan-rewrite.c deleted file mode 100644 index 1cbc54028..000000000 --- a/common/lwan-rewrite.c +++ /dev/null @@ -1,312 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2015 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-rewrite.h" -#include "list.h" -#include "patterns.h" - -struct private_data { - struct list_head patterns; -}; - -struct pattern { - struct list_node list; - char *pattern; - char *expand; - lwan_http_status_t (*handle)(lwan_request_t *request, const char *url); -}; - -struct str_builder { - char *buffer; - size_t size, len; -}; - -static lwan_http_status_t -module_redirect_to(lwan_request_t *request, const char *url) -{ - lwan_key_value_t *headers = coro_malloc(request->conn->coro, sizeof(*headers) * 2); - if (UNLIKELY(!headers)) - return HTTP_INTERNAL_ERROR; - - headers[0].key = "Location"; - headers[0].value = coro_strdup(request->conn->coro, url); - if (UNLIKELY(!headers[0].value)) - return HTTP_INTERNAL_ERROR; - - headers[1].key = NULL; - headers[1].value = NULL; - request->response.headers = headers; - - return HTTP_MOVED_PERMANENTLY; -} - -static lwan_http_status_t -module_rewrite_as(lwan_request_t *request, const char *url) -{ - request->url.value = coro_strdup(request->conn->coro, url); - if (UNLIKELY(!request->url.value)) - return HTTP_INTERNAL_ERROR; - - request->url.len = strlen(request->url.value); - request->original_url = request->url; - request->flags |= RESPONSE_URL_REWRITTEN; - - return HTTP_OK; -} - -static bool -append_str(struct str_builder *builder, const char *src, size_t src_len) -{ - size_t total_size = builder->len + src_len + 1 /* for the \0 */; - char *dest; - - if (total_size > builder->size) - return false; - - dest = mempcpy(builder->buffer + builder->len, src, src_len); - *dest = '\0'; - builder->len = total_size - 1; - - return true; -} - -static const char * -expand(const char *pattern, const char *orig, char buffer[static PATH_MAX], - struct str_find *sf, int captures) -{ - struct str_builder builder = { .buffer = buffer, .size = PATH_MAX }; - char *ptr; - - ptr = strchr(pattern, '%'); - if (!ptr) - return pattern; - - do { - size_t index_len = strspn(ptr + 1, "0123456789"); - - if (ptr > pattern) { - if (UNLIKELY(!append_str(&builder, pattern, (size_t)(ptr - pattern)))) - return NULL; - - pattern += ptr - pattern; - } - - if (LIKELY(index_len > 0)) { - int index = parse_int(strndupa(ptr + 1, index_len), -1); - - if (UNLIKELY(index < 0 || index > captures)) - return NULL; - - if (UNLIKELY(!append_str(&builder, orig + sf[index].sm_so, - (size_t)(sf[index].sm_eo - sf[index].sm_so)))) - return NULL; - - ptr += index_len; - pattern += index_len; - } else if (UNLIKELY(!append_str(&builder, "%", 1))) { - return NULL; - } - - pattern++; - } while ((ptr = strchr(ptr + 1, '%'))); - - if (*pattern && !append_str(&builder, pattern, strlen(pattern))) - return NULL; - - if (UNLIKELY(!builder.len)) - return NULL; - - return builder.buffer; -} - -static lwan_http_status_t -module_handle_cb(lwan_request_t *request, - lwan_response_t *response __attribute__((unused)), - void *data) -{ - const char *url = request->url.value; - char final_url[PATH_MAX]; - struct private_data *pd = data; - struct pattern *p; - - if (UNLIKELY(!pd)) - return HTTP_INTERNAL_ERROR; - - list_for_each(&pd->patterns, p, list) { - struct str_find sf[MAXCAPTURES]; - const char *errmsg, *expanded; - int captures; - - captures = str_find(url, p->pattern, sf, MAXCAPTURES, &errmsg); - if (captures <= 0) - continue; - - expanded = expand(p->expand, url, final_url, sf, captures); - if (LIKELY(expanded)) - return p->handle(request, expanded); - - return HTTP_INTERNAL_ERROR; - } - - return HTTP_NOT_FOUND; -} - -static void * -module_init(void *data __attribute__((unused))) -{ - struct private_data *pd = malloc(sizeof(*pd)); - - if (!pd) - return NULL; - - list_head_init(&pd->patterns); - return pd; -} - -static void -module_shutdown(void *data) -{ - struct private_data *pd = data; - struct pattern *iter, *next; - - list_for_each_safe(&pd->patterns, iter, next, list) { - free(iter->pattern); - free(iter->expand); - free(iter); - } - free(pd); -} - -static void * -module_init_from_hash(const struct hash *hash __attribute__((unused))) -{ - return module_init(NULL); -} - -static bool -module_parse_conf_pattern(struct private_data *pd, config_t *config, config_line_t *line) -{ - struct pattern *pattern; - char *redirect_to = NULL, *rewrite_as = NULL; - - pattern = calloc(1, sizeof(*pattern)); - if (!pattern) - goto out_no_free; - - pattern->pattern = strdup(line->section.param); - if (!pattern->pattern) - goto out; - - while (config_read_line(config, line)) { - switch (line->type) { - case CONFIG_LINE_TYPE_LINE: - if (!strcmp(line->line.key, "redirect_to")) { - redirect_to = strdup(line->line.value); - if (!redirect_to) - goto out; - } else if (!strcmp(line->line.key, "rewrite_as")) { - rewrite_as = strdup(line->line.value); - if (!rewrite_as) - goto out; - } else { - config_error(config, "Unexpected key: %s", line->line.key); - goto out; - } - break; - case CONFIG_LINE_TYPE_SECTION: - config_error(config, "Unexpected section: %s", line->section.name); - break; - case CONFIG_LINE_TYPE_SECTION_END: - if (redirect_to && rewrite_as) { - config_error(config, "`redirect to` and `rewrite as` are mutually exclusive"); - goto out; - } - if (redirect_to) { - pattern->expand = redirect_to; - pattern->handle = module_redirect_to; - } else if (rewrite_as) { - pattern->expand = rewrite_as; - pattern->handle = module_rewrite_as; - } else { - config_error(config, "either `redirect to` or `rewrite as` are required"); - goto out; - } - list_add_tail(&pd->patterns, &pattern->list); - return true; - } - } - -out: - free(pattern->pattern); - free(redirect_to); - free(rewrite_as); - free(pattern); -out_no_free: - config_error(config, "Could not copy pattern"); - return false; -} - -static bool -module_parse_conf(void *data, config_t *config) -{ - struct private_data *pd = data; - config_line_t line; - - while (config_read_line(config, &line)) { - switch (line.type) { - case CONFIG_LINE_TYPE_LINE: - config_error(config, "Unknown option: %s", line.line.key); - break; - case CONFIG_LINE_TYPE_SECTION: - if (!strcmp(line.section.name, "pattern")) { - module_parse_conf_pattern(pd, config, &line); - } else { - config_error(config, "Unknown section: %s", line.section.name); - } - break; - case CONFIG_LINE_TYPE_SECTION_END: - break; - } - } - - return !config->error_message; -} - -const lwan_module_t * -lwan_module_rewrite(void) -{ - static const lwan_module_t rewrite_module = { - .name = "rewrite", - .init = module_init, - .init_from_hash = module_init_from_hash, - .parse_conf = module_parse_conf, - .shutdown = module_shutdown, - .handle = module_handle_cb, - .flags = HANDLER_CAN_REWRITE_URL - }; - - return &rewrite_module; -} diff --git a/common/lwan-serve-files.c b/common/lwan-serve-files.c deleted file mode 100644 index 081bedd55..000000000 --- a/common/lwan-serve-files.c +++ /dev/null @@ -1,1019 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-cache.h" -#include "lwan-config.h" -#include "lwan-io-wrappers.h" -#include "lwan-serve-files.h" -#include "lwan-template.h" -#include "realpathat.h" -#include "hash.h" - -static const char *compression_none = NULL; -static const char *compression_gzip = "gzip"; -static const char *compression_deflate = "deflate"; - -typedef struct serve_files_priv_t_ serve_files_priv_t; -typedef struct file_cache_entry_t_ file_cache_entry_t; -typedef struct cache_funcs_t_ cache_funcs_t; -typedef struct mmap_cache_data_t_ mmap_cache_data_t; -typedef struct sendfile_cache_data_t_ sendfile_cache_data_t; -typedef struct dir_list_cache_data_t_ dir_list_cache_data_t; -typedef struct redir_cache_data_t_ redir_cache_data_t; - -struct serve_files_priv_t_ { - struct cache_t *cache; - - struct { - char *path; - size_t path_len; - int fd; - } root; - - int open_mode; - const char *index_html; - - lwan_tpl_t *directory_list_tpl; - - bool serve_precompressed_files; -}; - -struct cache_funcs_t_ { - lwan_http_status_t (*serve)(lwan_request_t *request, - void *data); - bool (*init)(file_cache_entry_t *ce, - serve_files_priv_t *priv, - const char *full_path, - struct stat *st); - void (*free)(void *data); - size_t struct_size; -}; - -struct mmap_cache_data_t_ { - struct { - void *contents; - /* zlib expects unsigned longs instead of size_t */ - unsigned long size; - } compressed, uncompressed; -}; - -struct sendfile_cache_data_t_ { - /* - * FIXME Investigate if keeping files open and dup()ing them - * is faster than openat()ing. This won't scale as well, - * but might be a good alternative for popular files. - */ - - struct { - char *filename; - size_t size; - } compressed, uncompressed; -}; - -struct dir_list_cache_data_t_ { - strbuf_t *rendered; -}; - -struct redir_cache_data_t_ { - char *redir_to; -}; - -struct file_cache_entry_t_ { - struct cache_entry_t base; - - struct { - char string[31]; - time_t integer; - } last_modified; - - const char *mime_type; - const cache_funcs_t *funcs; -}; - -struct file_list_t { - const char *full_path; - const char *rel_path; - struct { - lwan_tpl_list_generator_t generator; - - const char *icon; - const char *icon_alt; - const char *name; - const char *type; - - int size; - const char *unit; - } file_list; -}; - -static int directory_list_generator(coro_t *coro); - -static bool mmap_init(file_cache_entry_t *ce, serve_files_priv_t *priv, - const char *full_path, struct stat *st); -static void mmap_free(void *data); -static lwan_http_status_t mmap_serve(lwan_request_t *request, void *data); -static bool sendfile_init(file_cache_entry_t *ce, serve_files_priv_t *priv, - const char *full_path, struct stat *st); -static void sendfile_free(void *data); -static lwan_http_status_t sendfile_serve(lwan_request_t *request, void *data); -static bool dirlist_init(file_cache_entry_t *ce, serve_files_priv_t *priv, - const char *full_path, struct stat *st); -static void dirlist_free(void *data); -static lwan_http_status_t dirlist_serve(lwan_request_t *request, void *data); -static bool redir_init(file_cache_entry_t *ce, serve_files_priv_t *priv, - const char *full_path, struct stat *st); -static void redir_free(void *data); -static lwan_http_status_t redir_serve(lwan_request_t *request, void *data); - - -static const cache_funcs_t mmap_funcs = { - .init = mmap_init, - .free = mmap_free, - .serve = mmap_serve, - .struct_size = sizeof(mmap_cache_data_t) -}; - -static const cache_funcs_t sendfile_funcs = { - .init = sendfile_init, - .free = sendfile_free, - .serve = sendfile_serve, - .struct_size = sizeof(sendfile_cache_data_t) -}; - -static const cache_funcs_t dirlist_funcs = { - .init = dirlist_init, - .free = dirlist_free, - .serve = dirlist_serve, - .struct_size = sizeof(dir_list_cache_data_t) -}; - -static const cache_funcs_t redir_funcs = { - .init = redir_init, - .free = redir_free, - .serve = redir_serve, - .struct_size = sizeof(redir_cache_data_t) -}; - -static const lwan_var_descriptor_t file_list_desc[] = { - TPL_VAR_STR_ESCAPE(struct file_list_t, full_path), - TPL_VAR_STR_ESCAPE(struct file_list_t, rel_path), - TPL_VAR_SEQUENCE(struct file_list_t, file_list, directory_list_generator, ( - (const lwan_var_descriptor_t[]) { - TPL_VAR_STR(struct file_list_t, file_list.icon), - TPL_VAR_STR(struct file_list_t, file_list.icon_alt), - TPL_VAR_STR(struct file_list_t, file_list.name), - TPL_VAR_STR(struct file_list_t, file_list.type), - TPL_VAR_INT(struct file_list_t, file_list.size), - TPL_VAR_STR(struct file_list_t, file_list.unit), - TPL_VAR_SENTINEL - } - )), - TPL_VAR_SENTINEL -}; - -static const char *directory_list_tpl_str = "\n" - "\n" - " Index of {{rel_path}}\n" - "\n" - "\n" - "

Index of {{rel_path}}

\n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "{{#file_list}}" - " \n" - " \n" - " \n" - " \n" - " \n" - " \n" - "{{/file_list}}" - "{{^#file_list}}" - " \n" - " \n" - " \n" - "{{/file_list}}" - "
 File nameTypeSize
Parent directory
\"{{file_list.icon_alt}}\"{{{file_list.name}}}{{file_list.type}}{{file_list.size}}{{file_list.unit}}
Empty directory.
\n" - "\n" - "\n"; - -static int -directory_list_generator(coro_t *coro) -{ - DIR *dir; - struct dirent entry, *buffer; - struct file_list_t *fl = coro_get_data(coro); - int fd; - - dir = opendir(fl->full_path); - if (!dir) - return 0; - - fd = dirfd(dir); - if (fd < 0) - goto out; - - while (!readdir_r(dir, &entry, &buffer)) { - struct stat st; - - if (!buffer) - break; - - if (entry.d_name[0] == '.') - continue; - - if (fstatat(fd, entry.d_name, &st, 0) < 0) - continue; - - if (S_ISDIR(st.st_mode)) { - fl->file_list.icon = "folder"; - fl->file_list.icon_alt = "DIR"; - fl->file_list.type = "directory"; - } else if (S_ISREG(st.st_mode)) { - fl->file_list.icon = "file"; - fl->file_list.icon_alt = "FILE"; - fl->file_list.type = lwan_determine_mime_type_for_file_name(entry.d_name); - } else { - continue; - } - - if (st.st_size < 1024) { - fl->file_list.size = (int)st.st_size; - fl->file_list.unit = "B"; - } else if (st.st_size < 1024 * 1024) { - fl->file_list.size = (int)(st.st_size / 1024); - fl->file_list.unit = "KiB"; - } else if (st.st_size < 1024 * 1024 * 1024) { - fl->file_list.size = (int)(st.st_size / (1024 * 1024)); - fl->file_list.unit = "MiB"; - } else { - fl->file_list.size = (int)(st.st_size / (1024 * 1024 * 1024)); - fl->file_list.unit = "GiB"; - } - - fl->file_list.name = entry.d_name; - - if (coro_yield(coro, 1)) - break; - } - -out: - closedir(dir); - return 0; -} - -static ALWAYS_INLINE bool -is_compression_worthy(const size_t compressed_sz, const size_t uncompressed_sz) -{ - /* FIXME: gzip encoding is also supported but not considered here */ - static const size_t deflated_header_size = sizeof("Content-Encoding: deflate\r\n") - 1; - return ((compressed_sz + deflated_header_size) < uncompressed_sz); -} - -static void -compress_cached_entry(mmap_cache_data_t *md) -{ - md->compressed.size = compressBound(md->uncompressed.size); - - if (UNLIKELY(!(md->compressed.contents = malloc(md->compressed.size)))) - goto error_zero_out; - - if (UNLIKELY(compress(md->compressed.contents, &md->compressed.size, - md->uncompressed.contents, md->uncompressed.size) != Z_OK)) - goto error_free_compressed; - - if (is_compression_worthy(md->compressed.size, md->uncompressed.size)) - return; - -error_free_compressed: - free(md->compressed.contents); - md->compressed.contents = NULL; -error_zero_out: - md->compressed.size = 0; -} - -static bool -mmap_init(file_cache_entry_t *ce, - serve_files_priv_t *priv, - const char *full_path, - struct stat *st) -{ - mmap_cache_data_t *md = (mmap_cache_data_t *)(ce + 1); - int file_fd; - bool success; - - file_fd = openat(priv->root.fd, full_path + priv->root.path_len + 1, - priv->open_mode); - if (UNLIKELY(file_fd < 0)) - return false; - - md->uncompressed.contents = mmap(NULL, (size_t)st->st_size, PROT_READ, - MAP_SHARED, file_fd, 0); - if (UNLIKELY(md->uncompressed.contents == MAP_FAILED)) { - success = false; - goto close_file; - } - - if (UNLIKELY(madvise(md->uncompressed.contents, (size_t)st->st_size, - MADV_WILLNEED) < 0)) - lwan_status_perror("madvise"); - - md->uncompressed.size = (size_t)st->st_size; - compress_cached_entry(md); - - ce->mime_type = lwan_determine_mime_type_for_file_name( - full_path + priv->root.path_len); - - success = true; - -close_file: - close(file_fd); - - return success; -} - -static bool -sendfile_init(file_cache_entry_t *ce, - serve_files_priv_t *priv, - const char *full_path, - struct stat *st) -{ - sendfile_cache_data_t *sd = (sendfile_cache_data_t *)(ce + 1); - struct stat compressed_st; - - ce->mime_type = lwan_determine_mime_type_for_file_name( - full_path + priv->root.path_len); - - if (UNLIKELY(!priv->serve_precompressed_files)) - goto only_uncompressed; - - /* Try to serve a compressed file using sendfile() if $FILENAME.gz exists */ - int len = asprintf(&sd->compressed.filename, "%s.gz", full_path + priv->root.path_len + 1); - if (UNLIKELY(len < 0 || len >= PATH_MAX)) - goto only_uncompressed; - - int ret = fstatat(priv->root.fd, sd->compressed.filename, &compressed_st, 0); - if (LIKELY(ret >= 0 && compressed_st.st_mtime >= st->st_mtime && - is_compression_worthy((size_t)compressed_st.st_size, (size_t)st->st_size))) { - sd->compressed.size = (size_t)compressed_st.st_size; - } else { - free(sd->compressed.filename); - -only_uncompressed: - sd->compressed.filename = NULL; - sd->compressed.size = 0; - } - - /* Regardless of the existence of $FILENAME.gz, store the full path */ - sd->uncompressed.size = (size_t)st->st_size; - sd->uncompressed.filename = strdup(full_path + priv->root.path_len + 1); - if (UNLIKELY(!sd->uncompressed.filename)) { - free(sd->compressed.filename); - return false; - } - - return true; -} - -static bool -dirlist_init(file_cache_entry_t *ce, - serve_files_priv_t *priv, - const char *full_path, - struct stat *st __attribute__((unused))) -{ - dir_list_cache_data_t *dd = (dir_list_cache_data_t *)(ce + 1); - struct file_list_t vars = { - .full_path = full_path, - .rel_path = full_path + priv->root.path_len - }; - - dd->rendered = lwan_tpl_apply(priv->directory_list_tpl, &vars); - ce->mime_type = "text/html"; - - return !!dd->rendered; -} - -static bool -redir_init(file_cache_entry_t *ce, - serve_files_priv_t *priv, - const char *full_path, - struct stat *st __attribute__((unused))) -{ - redir_cache_data_t *rd = (redir_cache_data_t *)(ce + 1); - - if (asprintf(&rd->redir_to, "%s/", full_path + priv->root.path_len) < 0) - return false; - - ce->mime_type = "text/plain"; - return true; -} - -static const cache_funcs_t * -get_funcs(serve_files_priv_t *priv, const char *key, char *full_path, - struct stat *st) -{ - char index_html_path_buf[PATH_MAX]; - char *index_html_path = index_html_path_buf; - - if (S_ISDIR(st->st_mode)) { - /* It is a directory. It might be the root directory (empty key), or - * something else. In either case, tack priv->index_html to the - * path. */ - if (*key == '\0') { - index_html_path = (char *)priv->index_html; - } else { - /* Redirect /path to /path/. This is to help cases where there's - * something like , so that actually - * /path/../foo.png is served instead of /path../foo.png. */ - const char *key_end = rawmemchr(key, '\0'); - if (*(key_end - 1) != '/') - return &redir_funcs; - - int ret = snprintf(index_html_path, PATH_MAX, "%s%s", key, priv->index_html); - if (UNLIKELY(ret < 0 || ret >= PATH_MAX)) - return NULL; - } - - /* See if it exists. */ - if (fstatat(priv->root.fd, index_html_path, st, 0) < 0) { - if (UNLIKELY(errno != ENOENT)) - return NULL; - - /* If it doesn't, we want to generate a directory list. */ - return &dirlist_funcs; - } - - /* If it does, we want its full path. */ - - /* FIXME: Use strlcpy() here instead of calling strlen()? */ - if (UNLIKELY(priv->root.path_len + 1 /* slash */ + - strlen(index_html_path) + 1 >= PATH_MAX)) - return NULL; - - full_path[priv->root.path_len] = '/'; - strncpy(full_path + priv->root.path_len + 1, index_html_path, - PATH_MAX - priv->root.path_len - 1); - } - - /* It's not a directory: choose the fastest way to serve the file - * judging by its size. */ - if (st->st_size < 16384) - return &mmap_funcs; - - return &sendfile_funcs; -} - -static file_cache_entry_t * -create_cache_entry_from_funcs(serve_files_priv_t *priv, const char *full_path, - struct stat *st, const cache_funcs_t *funcs) -{ - file_cache_entry_t *fce; - - fce = malloc(sizeof(*fce) + funcs->struct_size); - if (UNLIKELY(!fce)) - return NULL; - - if (LIKELY(funcs->init(fce, priv, full_path, st))) { - fce->funcs = funcs; - return fce; - } - - free(fce); - - if (funcs != &mmap_funcs) - return NULL; - - return create_cache_entry_from_funcs(priv, full_path, st, &sendfile_funcs); -} - -static struct cache_entry_t * -create_cache_entry(const char *key, void *context) -{ - serve_files_priv_t *priv = context; - file_cache_entry_t *fce; - struct stat st; - const cache_funcs_t *funcs; - char full_path[PATH_MAX]; - - if (UNLIKELY(!realpathat2(priv->root.fd, priv->root.path, - key, full_path, &st))) - return NULL; - - if (UNLIKELY(!(st.st_mode & (S_IRUSR | S_IRGRP | S_IROTH)))) - return NULL; - - if (UNLIKELY(strncmp(full_path, priv->root.path, priv->root.path_len))) - return NULL; - - funcs = get_funcs(priv, key, full_path, &st); - if (UNLIKELY(!funcs)) - return NULL; - - fce = create_cache_entry_from_funcs(priv, full_path, &st, funcs); - if (UNLIKELY(!fce)) - return NULL; - - lwan_format_rfc_time(st.st_mtime, fce->last_modified.string); - fce->last_modified.integer = st.st_mtime; - - return (struct cache_entry_t *)fce; -} - -static void -mmap_free(void *data) -{ - mmap_cache_data_t *md = data; - - munmap(md->uncompressed.contents, md->uncompressed.size); - free(md->compressed.contents); -} - -static void -sendfile_free(void *data) -{ - sendfile_cache_data_t *sd = data; - - free(sd->compressed.filename); - free(sd->uncompressed.filename); -} - -static void -dirlist_free(void *data) -{ - dir_list_cache_data_t *dd = data; - - strbuf_free(dd->rendered); -} - -static void -redir_free(void *data) -{ - redir_cache_data_t *rd = data; - - free(rd->redir_to); -} - -static void -destroy_cache_entry(struct cache_entry_t *entry, void *context __attribute__((unused))) -{ - file_cache_entry_t *fce = (file_cache_entry_t *)entry; - - fce->funcs->free(fce + 1); - free(fce); -} - -static int -try_open_directory(const char *path, int *open_mode) -{ - int fd; - - *open_mode = O_RDONLY | O_NOATIME | O_NONBLOCK | O_CLOEXEC; - - fd = open(path, *open_mode | O_DIRECTORY); - if (fd < 0) { - /* O_NOATIME only works for directories owned by the process owner */ - *open_mode &= ~O_NOATIME; - - fd = open(path, *open_mode | O_DIRECTORY); - if (fd < 0) { - /* Although unlikely, this might fail */ - *open_mode &= ~O_NONBLOCK; - - fd = open(path, *open_mode | O_DIRECTORY); - } - } - - if (fd < 0) - return -1; - - /* Passing O_PATH masks all modes except O_CLOEXEC | O_DIRECTORY | - * O_NOFOLLOW. The open(2) calls above detects if O_NOATIME and - * O_NONBLOCK can be used. Close and open the directory again when - * modes have been properly detected with O_PATH. */ - close(fd); - return open(path, *open_mode | O_DIRECTORY | O_PATH); -} - -static void * -serve_files_init(void *args) -{ - struct lwan_serve_files_settings_t *settings = args; - char *canonical_root; - int root_fd; - serve_files_priv_t *priv; - int open_mode; - - if (!settings->root_path) { - lwan_status_error("root_path not specified"); - return NULL; - } - - canonical_root = realpath(settings->root_path, NULL); - if (!canonical_root) { - lwan_status_perror("Could not obtain real path of \"%s\"", - settings->root_path); - goto out_realpath; - } - - root_fd = try_open_directory(canonical_root, &open_mode); - if (root_fd < 0) { - lwan_status_perror("Could not open directory \"%s\"", - canonical_root); - goto out_open; - } - - priv = malloc(sizeof(*priv)); - if (!priv) { - lwan_status_perror("malloc"); - goto out_malloc; - } - - priv->cache = cache_create(create_cache_entry, destroy_cache_entry, - priv, 5); - if (!priv->cache) { - lwan_status_error("Couldn't create cache"); - goto out_cache_create; - } - - priv->directory_list_tpl = lwan_tpl_compile_string( - directory_list_tpl_str, file_list_desc); - if (!priv->directory_list_tpl) { - lwan_status_error("Could not compile directory list template"); - goto out_tpl_compile; - } - - priv->root.path = canonical_root; - priv->root.path_len = strlen(canonical_root); - priv->root.fd = root_fd; - priv->open_mode = open_mode; - priv->index_html = settings->index_html ? settings->index_html : "index.html"; - priv->serve_precompressed_files = settings->serve_precompressed_files; - - return priv; - -out_tpl_compile: - cache_destroy(priv->cache); -out_cache_create: - free(priv); -out_malloc: - close(root_fd); -out_open: - free(canonical_root); -out_realpath: - return NULL; -} - -static void * -serve_files_init_from_hash(const struct hash *hash) -{ - struct lwan_serve_files_settings_t settings = { - .root_path = hash_find(hash, "path"), - .index_html = hash_find(hash, "index_path"), - .serve_precompressed_files = - parse_bool(hash_find(hash, "serve_precompressed_files"), true) - }; - return serve_files_init(&settings); -} - -static void -serve_files_shutdown(void *data) -{ - serve_files_priv_t *priv = data; - - if (!priv) { - lwan_status_warning("Nothing to shutdown"); - return; - } - - lwan_tpl_free(priv->directory_list_tpl); - cache_destroy(priv->cache); - close(priv->root.fd); - free(priv->root.path); - free(priv); -} - -static ALWAYS_INLINE bool -client_has_fresh_content(lwan_request_t *request, time_t mtime) -{ - return request->header.if_modified_since && mtime <= request->header.if_modified_since; -} - -static size_t -prepare_headers(lwan_request_t *request, - lwan_http_status_t return_status, - file_cache_entry_t *fce, - size_t size, - const char *compression_type, - char *header_buf, - size_t header_buf_size) -{ - lwan_key_value_t headers[3] = { - [0] = { .key = "Last-Modified", .value = fce->last_modified.string }, - }; - - request->response.headers = headers; - request->response.content_length = size; - - if (compression_type) { - headers[1] = (lwan_key_value_t) { - .key = "Content-Encoding", - .value = (char *)compression_type - }; - } - - return lwan_prepare_response_header(request, return_status, - header_buf, header_buf_size); -} - -static ALWAYS_INLINE lwan_http_status_t -compute_range(lwan_request_t *request, off_t *from, off_t *to, off_t size) -{ - off_t f, t; - - f = request->header.range.from; - t = request->header.range.to; - - /* - * No Range: header present: both t and f are -1 - */ - if (LIKELY(t <= 0 && f <= 0)) { - *from = 0; - *to = size; - return HTTP_OK; - } - - /* - * To goes beyond from or To and From are the same: this is unsatisfiable. - */ - if (UNLIKELY(t >= f)) - return HTTP_RANGE_UNSATISFIABLE; - - /* - * Range goes beyond the size of the file - */ - if (UNLIKELY(f >= size || t >= size)) - return HTTP_RANGE_UNSATISFIABLE; - - /* - * t < 0 means ranges from f to the file size - */ - if (t < 0) - t = size - f; - else - t -= f; - - /* - * If for some reason the previous calculations yields something - * less than zero, the range is unsatisfiable. - */ - if (UNLIKELY(t <= 0)) - return HTTP_RANGE_UNSATISFIABLE; - - *from = f; - *to = t; - - return HTTP_PARTIAL_CONTENT; -} - -static lwan_http_status_t -sendfile_serve(lwan_request_t *request, void *data) -{ - file_cache_entry_t *fce = data; - sendfile_cache_data_t *sd = (sendfile_cache_data_t *)(fce + 1); - char headers[DEFAULT_BUFFER_SIZE]; - size_t header_len; - lwan_http_status_t return_status; - off_t from, to; - const char *compressed; - char *filename; - size_t size; - - if (sd->compressed.size && (request->flags & REQUEST_ACCEPT_GZIP)) { - from = 0; - to = (off_t)sd->compressed.size; - - compressed = compression_gzip; - filename = sd->compressed.filename; - size = sd->compressed.size; - - return_status = HTTP_OK; - } else { - return_status = compute_range(request, &from, &to, (off_t)sd->uncompressed.size); - if (UNLIKELY(return_status == HTTP_RANGE_UNSATISFIABLE)) - return HTTP_RANGE_UNSATISFIABLE; - - compressed = compression_none; - filename = sd->uncompressed.filename; - size = sd->uncompressed.size; - } - - if (client_has_fresh_content(request, fce->last_modified.integer)) - return_status = HTTP_NOT_MODIFIED; - - header_len = prepare_headers(request, return_status, fce, size, - compressed, headers, DEFAULT_HEADERS_SIZE); - if (UNLIKELY(!header_len)) - return HTTP_INTERNAL_ERROR; - - if (request->flags & REQUEST_METHOD_HEAD || return_status == HTTP_NOT_MODIFIED) { - lwan_write(request, headers, header_len); - } else { - serve_files_priv_t *priv = request->response.stream.priv; - /* - * lwan_openat() will yield from the coroutine if openat() - * can't open the file due to not having free file descriptors - * around. This will happen just a handful of times. - * The file will be automatically closed whenever this - * coroutine is freed. - */ - int file_fd = lwan_openat(request, priv->root.fd, filename, priv->open_mode); - if (UNLIKELY(file_fd < 0)) { - switch (file_fd) { - case -EACCES: - return HTTP_FORBIDDEN; - case -ENFILE: - return HTTP_UNAVAILABLE; - default: - return HTTP_NOT_FOUND; - } - } - - lwan_send(request, headers, header_len, MSG_MORE); - lwan_sendfile(request, file_fd, from, (size_t)to); - } - - return return_status; -} - -static lwan_http_status_t -serve_contents_and_size(lwan_request_t *request, file_cache_entry_t *fce, - const char *compression_type, void *contents, size_t size) -{ - char headers[DEFAULT_BUFFER_SIZE]; - size_t header_len; - lwan_http_status_t return_status = HTTP_OK; - - if (client_has_fresh_content(request, fce->last_modified.integer)) - return_status = HTTP_NOT_MODIFIED; - - header_len = prepare_headers(request, return_status, - fce, size, compression_type, - headers, DEFAULT_HEADERS_SIZE); - if (UNLIKELY(!header_len)) - return HTTP_INTERNAL_ERROR; - - if (request->flags & REQUEST_METHOD_HEAD || return_status == HTTP_NOT_MODIFIED) { - lwan_write(request, headers, header_len); - } else { - struct iovec response_vec[] = { - { .iov_base = headers, .iov_len = header_len }, - { .iov_base = contents, .iov_len = size } - }; - - lwan_writev(request, response_vec, N_ELEMENTS(response_vec)); - } - - return return_status; -} - -static lwan_http_status_t -mmap_serve(lwan_request_t *request, void *data) -{ - file_cache_entry_t *fce = data; - mmap_cache_data_t *md = (mmap_cache_data_t *)(fce + 1); - void *contents; - size_t size; - const char *compressed; - - if (md->compressed.size && (request->flags & REQUEST_ACCEPT_DEFLATE)) { - contents = md->compressed.contents; - size = md->compressed.size; - compressed = compression_deflate; - } else { - contents = md->uncompressed.contents; - size = md->uncompressed.size; - compressed = compression_none; - } - - return serve_contents_and_size(request, fce, compressed, contents, size); -} - -static lwan_http_status_t -dirlist_serve(lwan_request_t *request, void *data) -{ - file_cache_entry_t *fce = data; - dir_list_cache_data_t *dd = (dir_list_cache_data_t *)(fce + 1); - - return serve_contents_and_size(request, fce, compression_none, - strbuf_get_buffer(dd->rendered), strbuf_get_length(dd->rendered)); -} - -static lwan_http_status_t -redir_serve(lwan_request_t *request, void *data) -{ - file_cache_entry_t *fce = data; - redir_cache_data_t *rd = (redir_cache_data_t *)(fce + 1); - char header_buf[DEFAULT_BUFFER_SIZE]; - size_t header_buf_size; - lwan_key_value_t headers[2] = { - [0] = { .key = "Location", .value = rd->redir_to }, - }; - - request->response.headers = headers; - request->response.content_length = strlen(rd->redir_to); - - header_buf_size = lwan_prepare_response_header(request, - HTTP_MOVED_PERMANENTLY, header_buf, DEFAULT_BUFFER_SIZE); - if (UNLIKELY(!header_buf_size)) - return HTTP_INTERNAL_ERROR; - - struct iovec response_vec[] = { - { .iov_base = header_buf, .iov_len = header_buf_size }, - { .iov_base = rd->redir_to, .iov_len = request->response.content_length }, - }; - - lwan_writev(request, response_vec, N_ELEMENTS(response_vec)); - - return HTTP_MOVED_PERMANENTLY; -} - -static lwan_http_status_t -serve_files_handle_cb(lwan_request_t *request, lwan_response_t *response, void *data) -{ - lwan_http_status_t return_status = HTTP_NOT_FOUND; - serve_files_priv_t *priv = data; - struct cache_entry_t *ce; - - if (UNLIKELY(!priv)) { - return_status = HTTP_INTERNAL_ERROR; - goto fail; - } - - ce = cache_coro_get_and_ref_entry(priv->cache, request->conn->coro, - request->url.value); - if (LIKELY(ce)) { - file_cache_entry_t *fce = (file_cache_entry_t *)ce; - response->mime_type = fce->mime_type; - response->stream.callback = fce->funcs->serve; - response->stream.data = ce; - response->stream.priv = priv; - - return HTTP_OK; - } - -fail: - response->stream.callback = NULL; - return return_status; -} - -const lwan_module_t *lwan_module_serve_files(void) -{ - static const lwan_module_t serve_files = { - .name = "serve_files", - .init = serve_files_init, - .init_from_hash = serve_files_init_from_hash, - .shutdown = serve_files_shutdown, - .handle = serve_files_handle_cb, - .flags = HANDLER_REMOVE_LEADING_SLASH - | HANDLER_PARSE_IF_MODIFIED_SINCE - | HANDLER_PARSE_RANGE - | HANDLER_PARSE_ACCEPT_ENCODING - }; - - return &serve_files; -} diff --git a/common/lwan-socket.c b/common/lwan-socket.c deleted file mode 100644 index 190b06c62..000000000 --- a/common/lwan-socket.c +++ /dev/null @@ -1,256 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012, 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "lwan.h" -#include "sd-daemon.h" -#include "int-to-str.h" - - -static int -get_backlog_size(void) -{ -#ifdef SOMAXCONN - int backlog = SOMAXCONN; -#else - int backlog = 128; -#endif - FILE *somaxconn; - - somaxconn = fopen("/proc/sys/net/core/somaxconn", "re"); - if (somaxconn) { - int tmp; - if (fscanf(somaxconn, "%d", &tmp) == 1) - backlog = tmp; - fclose(somaxconn); - } - - return backlog; -} - -static int -setup_socket_from_systemd(void) -{ - int fd = SD_LISTEN_FDS_START; - - if (!sd_is_socket_inet(fd, AF_UNSPEC, SOCK_STREAM, 1, 0)) - lwan_status_critical("Passed file descriptor is not a " - "listening TCP socket"); - - int flags = fcntl(fd, F_GETFD); - if (flags < 0) - lwan_status_critical_perror("Could not obtain socket flags"); - if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) - lwan_status_critical_perror("Could not set socket flags"); - - return fd; -} - -static sa_family_t -parse_listener_ipv4(char *listener, char **node, char **port) -{ - char *colon = strrchr(listener, ':'); - if (!colon) { - *port = "8080"; - if (!strchr(listener, '.')) { - /* 8080 */ - *node = "0.0.0.0"; - } else { - /* 127.0.0.1 */ - *node = listener; - } - } else { - /* - * 127.0.0.1:8080 - * localhost:8080 - */ - *colon = '\0'; - *node = listener; - *port = colon + 1; - - if (!strcmp(*node, "*")) { - /* *:8080 */ - *node = "0.0.0.0"; - } - } - - return AF_INET; -} - -static sa_family_t -parse_listener_ipv6(char *listener, char **node, char **port) -{ - char *last_colon = strrchr(listener, ':'); - if (!last_colon) - return AF_UNSPEC; - - if (*(last_colon - 1) == ']') { - /* [::]:8080 */ - *(last_colon - 1) = '\0'; - *node = listener + 1; - *port = last_colon + 1; - } else { - /* [::1] */ - listener[strlen(listener) - 1] = '\0'; - *node = listener + 1; - *port = "8080"; - } - - return AF_INET6; -} - -static sa_family_t -parse_listener(char *listener, char **node, char **port) -{ - if (*listener == '[') - return parse_listener_ipv6(listener, node, port); - return parse_listener_ipv4(listener, node, port); -} - -static int -listen_addrinfo(int fd, const struct addrinfo *addr) -{ - if (listen(fd, get_backlog_size()) < 0) - lwan_status_critical_perror("listen"); - - char host_buf[NI_MAXHOST], serv_buf[NI_MAXSERV]; - int ret = getnameinfo(addr->ai_addr, addr->ai_addrlen, host_buf, sizeof(host_buf), - serv_buf, sizeof(serv_buf), NI_NUMERICHOST | NI_NUMERICSERV); - if (ret) - lwan_status_critical("getnameinfo: %s", gai_strerror(ret)); - - if (addr->ai_family == AF_INET6) - lwan_status_info("Listening on http://[%s]:%s", host_buf, serv_buf); - else - lwan_status_info("Listening on http://%s:%s", host_buf, serv_buf); - - return fd; -} - -#define SET_SOCKET_OPTION(_domain,_option,_param,_size) \ - do { \ - if (setsockopt(fd, (_domain), (_option), (_param), (_size)) < 0) \ - lwan_status_critical_perror("setsockopt"); \ - } while(0) - -#define SET_SOCKET_OPTION_MAY_FAIL(_domain,_option,_param,_size) \ - do { \ - if (setsockopt(fd, (_domain), (_option), (_param), (_size)) < 0) \ - lwan_status_warning("%s not supported by the kernel", \ - #_option); \ - } while(0) - -#ifndef SO_REUSEPORT -#define SO_REUSEPORT 15 -#endif - -static int -bind_and_listen_addrinfos(struct addrinfo *addrs, bool reuse_port) -{ - const struct addrinfo *addr; - - /* Try each address until we bind one successfully. */ - for (addr = addrs; addr; addr = addr->ai_next) { - int fd = socket(addr->ai_family, - addr->ai_socktype | SOCK_CLOEXEC, addr->ai_protocol); - if (fd < 0) - continue; - - SET_SOCKET_OPTION(SOL_SOCKET, SO_REUSEADDR, (int[]){ 1 }, sizeof(int)); - SET_SOCKET_OPTION_MAY_FAIL(SOL_SOCKET, SO_REUSEPORT, - (int[]){ reuse_port }, sizeof(int)); - - if (!bind(fd, addr->ai_addr, addr->ai_addrlen)) - return listen_addrinfo(fd, addr); - - close(fd); - } - - lwan_status_critical("Could not bind socket"); -} - -static int -setup_socket_normally(lwan_t *l) -{ - char *node, *port; - char *listener = strdupa(l->config.listener); - sa_family_t family = parse_listener(listener, &node, &port); - if (family == AF_UNSPEC) - lwan_status_critical("Could not parse listener: %s", l->config.listener); - - struct addrinfo *addrs; - struct addrinfo hints = { - .ai_family = family, - .ai_socktype = SOCK_STREAM, - .ai_flags = AI_PASSIVE - }; - - int ret = getaddrinfo(node, port, &hints, &addrs); - if (ret) - lwan_status_critical("getaddrinfo: %s", gai_strerror(ret)); - - int fd = bind_and_listen_addrinfos(addrs, l->config.reuse_port); - freeaddrinfo(addrs); - return fd; -} - -#ifndef TCP_FASTOPEN -#define TCP_FASTOPEN 23 -#endif - -void -lwan_socket_init(lwan_t *l) -{ - int fd, n; - - lwan_status_debug("Initializing sockets"); - - n = sd_listen_fds(1); - if (n > 1) { - lwan_status_critical("Too many file descriptors received"); - } else if (n == 1) { - fd = setup_socket_from_systemd(); - } else { - fd = setup_socket_normally(l); - } - - SET_SOCKET_OPTION(SOL_SOCKET, SO_LINGER, - (&(struct linger){ .l_onoff = 1, .l_linger = 1 }), sizeof(struct linger)); - - SET_SOCKET_OPTION_MAY_FAIL(SOL_TCP, TCP_FASTOPEN, - (int[]){ 5 }, sizeof(int)); - SET_SOCKET_OPTION_MAY_FAIL(SOL_TCP, TCP_QUICKACK, - (int[]){ 0 }, sizeof(int)); - - l->main_socket = fd; -} - -#undef SET_SOCKET_OPTION -#undef SET_SOCKET_OPTION_MAY_FAIL diff --git a/common/lwan-status.c b/common/lwan-status.c deleted file mode 100644 index 5f63878f0..000000000 --- a/common/lwan-status.c +++ /dev/null @@ -1,199 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include - -#ifndef NDEBUG -#include -#include -#endif - -#include "lwan.h" - -typedef enum { - STATUS_INFO = 1<<0, - STATUS_WARNING = 1<<1, - STATUS_ERROR = 1<<2, - STATUS_PERROR = 1<<3, - STATUS_CRITICAL = 1<<4, - STATUS_DEBUG = 1<<5, -} lwan_status_type_t; - -static volatile bool quiet = false; -static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; - -void -lwan_status_init(lwan_t *l) -{ -#ifdef NDEBUG - quiet = l->config.quiet; -#else - quiet = false; - (void) l; -#endif -} - -void -lwan_status_shutdown(lwan_t *l __attribute__((unused))) -{ -} - -static const char * -get_color_start_for_type(lwan_status_type_t type, size_t *len_out) -{ - const char *retval; - - if (type & STATUS_INFO) - retval = "\033[36m"; - else if (type & STATUS_WARNING) - retval = "\033[33m"; - else if (type & STATUS_CRITICAL) - retval = "\033[31;1m"; - else if (type & STATUS_DEBUG) - retval = "\033[37m"; - else if (type & STATUS_PERROR) - retval = "\033[35m"; - else - retval = "\033[32m"; - - *len_out = strlen(retval); - - return retval; -} - -static const char * -get_color_end_for_type(lwan_status_type_t type __attribute__((unused)), - size_t *len_out) -{ - static const char *retval = "\033[0m"; - *len_out = strlen(retval); - return retval; -} - -static void -#ifdef NDEBUG -status_out_msg(lwan_status_type_t type, const char *msg, size_t msg_len) -#else -status_out_msg(const char *file, const int line, const char *func, - lwan_status_type_t type, const char *msg, size_t msg_len) -#endif -{ - int error_number = errno; /* Make sure no library call below modifies errno */ - size_t start_len, end_len; - const char *start_color = get_color_start_for_type(type, &start_len); - const char *end_color = get_color_end_for_type(type, &end_len); - - if (UNLIKELY(pthread_mutex_lock(&mutex) < 0)) - perror("pthread_mutex_lock"); - -#ifndef NDEBUG - fprintf(stdout, "\033[32;1m%ld\033[0m", syscall(SYS_gettid)); - fprintf(stdout, " \033[3m%s:%d\033[0m", basename(file), line); - fprintf(stdout, " \033[33m%s()\033[0m", func); - fprintf(stdout, " "); -#endif - - fwrite(start_color, start_len, 1, stdout); - fwrite(msg, msg_len, 1, stdout); - - if (type & STATUS_PERROR) { - char buffer[512]; - char *error_msg = strerror_r(error_number, buffer, sizeof(buffer) - 1); - fputc(':', stdout); - fputc(' ', stdout); - fwrite(error_msg, strlen(error_msg), 1, stdout); - } - - fputc('.', stdout); - fwrite(end_color, end_len, 1, stdout); - fputc('\n', stdout); - - if (UNLIKELY(pthread_mutex_unlock(&mutex) < 0)) - perror("pthread_mutex_unlock"); -} - -static void -#ifdef NDEBUG -status_out(lwan_status_type_t type, const char *fmt, va_list values) -#else -status_out(const char *file, const int line, const char *func, - lwan_status_type_t type, const char *fmt, va_list values) -#endif -{ - char *output; - int len; - - len = vasprintf(&output, fmt, values); - if (len >= 0) { -#ifdef NDEBUG - status_out_msg(type, output, (size_t)len); -#else - status_out_msg(file, line, func, type, output, (size_t)len); -#endif - free(output); - } -} - -#ifdef NDEBUG -#define IMPLEMENT_FUNCTION(fn_name_, type_) \ - void \ - lwan_status_##fn_name_(const char *fmt, ...) \ - { \ - if (!quiet) { \ - va_list values; \ - va_start(values, fmt); \ - status_out(type_, fmt, values); \ - va_end(values); \ - } \ - if ((type_) & STATUS_CRITICAL) abort(); \ - } -#else -#define IMPLEMENT_FUNCTION(fn_name_, type_) \ - void \ - lwan_status_##fn_name_##_debug(const char *file, \ - const int line, const char *func, \ - const char *fmt, ...) \ - { \ - if (!quiet) { \ - va_list values; \ - va_start(values, fmt); \ - status_out(file, line, func, type_, fmt, values); \ - va_end(values); \ - } \ - if ((type_) & STATUS_CRITICAL) abort(); \ - } - -IMPLEMENT_FUNCTION(debug, STATUS_DEBUG) -#endif - -IMPLEMENT_FUNCTION(info, STATUS_INFO) -IMPLEMENT_FUNCTION(warning, STATUS_WARNING) -IMPLEMENT_FUNCTION(error, STATUS_ERROR) -IMPLEMENT_FUNCTION(perror, STATUS_PERROR) - -IMPLEMENT_FUNCTION(critical, STATUS_CRITICAL) -IMPLEMENT_FUNCTION(critical_perror, STATUS_CRITICAL | STATUS_PERROR) - -#undef IMPLEMENT_FUNCTION diff --git a/common/lwan-tables.c b/common/lwan-tables.c deleted file mode 100644 index b20669aac..000000000 --- a/common/lwan-tables.c +++ /dev/null @@ -1,167 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012, 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include - -#include "lwan.h" -#include "mime-types.h" - - -static unsigned char uncompressed_mime_entries[MIME_UNCOMPRESSED_LEN]; -static struct mime_entry mime_entries[MIME_ENTRIES]; -static bool mime_entries_initialized = false; - -void -lwan_tables_init(void) -{ - if (mime_entries_initialized) - return; - - lwan_status_debug("Uncompressing MIME type table"); - uLongf uncompressed_length = MIME_UNCOMPRESSED_LEN; - int ret = uncompress((Bytef*)uncompressed_mime_entries, - &uncompressed_length, (const Bytef*)mime_entries_compressed, - MIME_COMPRESSED_LEN); - if (ret != Z_OK) - lwan_status_critical( - "Error while uncompressing table: zlib error %d", ret); - - if (uncompressed_length != MIME_UNCOMPRESSED_LEN) - lwan_status_critical("Expected uncompressed length %d, got %ld", - MIME_UNCOMPRESSED_LEN, uncompressed_length); - - unsigned char *ptr = uncompressed_mime_entries; - for (size_t i = 0; i < MIME_ENTRIES; i++) { - mime_entries[i].extension = (char*)ptr; - ptr = (unsigned char *)rawmemchr(ptr + 1, '\0') + 1; - mime_entries[i].type = (char*)ptr; - ptr = (unsigned char *)rawmemchr(ptr + 1, '\0') + 1; - } - - mime_entries_initialized = true; -} - -void -lwan_tables_shutdown(void) -{ -} - -static int -compare_mime_entry(const void *a, const void *b) -{ - const struct mime_entry *me1 = a; - const struct mime_entry *me2 = b; - return strcmp(me1->extension, me2->extension); -} - -const char * -lwan_determine_mime_type_for_file_name(const char *file_name) -{ - char *last_dot = strrchr(file_name, '.'); - if (UNLIKELY(!last_dot)) - goto fallback; - - enum { - EXT_JPG = MULTICHAR_CONSTANT_L('.','j','p','g'), - EXT_PNG = MULTICHAR_CONSTANT_L('.','p','n','g'), - EXT_HTM = MULTICHAR_CONSTANT_L('.','h','t','m'), - EXT_CSS = MULTICHAR_CONSTANT_L('.','c','s','s'), - EXT_TXT = MULTICHAR_CONSTANT_L('.','t','x','t'), - EXT_JS = MULTICHAR_CONSTANT_L('.','j','s',0), - }; - - STRING_SWITCH_L(last_dot) { - case EXT_CSS: return "text/css"; - case EXT_HTM: return "text/html"; - case EXT_JPG: return "image/jpeg"; - case EXT_JS: return "application/javascript"; - case EXT_PNG: return "image/png"; - case EXT_TXT: return "text/plain"; - } - - if (LIKELY(*last_dot)) { - struct mime_entry *entry, key = { .extension = last_dot + 1 }; - - entry = bsearch(&key, mime_entries, MIME_ENTRIES, - sizeof(struct mime_entry), compare_mime_entry); - if (LIKELY(entry)) - return entry->type; - } - -fallback: - return "application/octet-stream"; -} - -const char * -lwan_http_status_as_string_with_code(lwan_http_status_t status) -{ - switch (status) { - case HTTP_OK: return "200 OK"; - case HTTP_PARTIAL_CONTENT: return "206 Partial content"; - case HTTP_MOVED_PERMANENTLY: return "301 Moved permanently"; - case HTTP_NOT_MODIFIED: return "304 Not modified"; - case HTTP_BAD_REQUEST: return "400 Bad request"; - case HTTP_NOT_AUTHORIZED: return "401 Not authorized"; - case HTTP_FORBIDDEN: return "403 Forbidden"; - case HTTP_NOT_FOUND: return "404 Not found"; - case HTTP_NOT_ALLOWED: return "405 Not allowed"; - case HTTP_TIMEOUT: return "408 Request timeout"; - case HTTP_TOO_LARGE: return "413 Request too large"; - case HTTP_RANGE_UNSATISFIABLE: return "416 Requested range unsatisfiable"; - case HTTP_I_AM_A_TEAPOT: return "418 I'm a teapot"; - case HTTP_INTERNAL_ERROR: return "500 Internal server error"; - case HTTP_NOT_IMPLEMENTED: return "501 Not implemented"; - case HTTP_UNAVAILABLE: return "503 Service unavailable"; - } - return "999 Invalid"; -} - -ALWAYS_INLINE const char * -lwan_http_status_as_string(lwan_http_status_t status) -{ - return lwan_http_status_as_string_with_code(status) + 4; -} - -const char * -lwan_http_status_as_descriptive_string(lwan_http_status_t status) -{ - switch (status) { - case HTTP_OK: return "Success!"; - case HTTP_PARTIAL_CONTENT: return "Delivering part of requested resource."; - case HTTP_MOVED_PERMANENTLY: return "This content has moved to another place."; - case HTTP_NOT_MODIFIED: return "The content has not changed since previous request."; - case HTTP_BAD_REQUEST: return "The client has issued a bad request."; - case HTTP_NOT_AUTHORIZED: return "Client has no authorization to access this resource."; - case HTTP_FORBIDDEN: return "Access to this resource has been denied."; - case HTTP_NOT_FOUND: return "The requested resource could not be found on this server."; - case HTTP_NOT_ALLOWED: return "The requested method is not allowed by this server."; - case HTTP_TIMEOUT: return "Client did not produce a request within expected timeframe."; - case HTTP_TOO_LARGE: return "The request entity is too large."; - case HTTP_RANGE_UNSATISFIABLE: return "The server can't supply the requested portion of the requested resource."; - case HTTP_I_AM_A_TEAPOT: return "Client requested to brew coffee but device is a teapot."; - case HTTP_INTERNAL_ERROR: return "The server encountered an internal error that couldn't be recovered from."; - case HTTP_NOT_IMPLEMENTED: return "Server lacks the ability to fulfil the request."; - case HTTP_UNAVAILABLE: return "The server is either overloaded or down for maintenance."; - } - return "Invalid"; -} diff --git a/common/lwan-template.c b/common/lwan-template.c deleted file mode 100644 index 92e880fe1..000000000 --- a/common/lwan-template.c +++ /dev/null @@ -1,1353 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -/* - * Ideas from Mustache logic-less templates: http://mustache.github.com/ - * Lexer+parser implemented using ideas from Rob Pike's talk "Lexical Scanning - * in Go" (https://www.youtube.com/watch?v=HxaD_trXwRE). - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "hash.h" -#include "int-to-str.h" -#include "list.h" -#include "lwan-template.h" -#include "strbuf.h" -#include "reallocarray.h" - -enum action { - ACTION_APPEND, - ACTION_APPEND_CHAR, - ACTION_VARIABLE, - ACTION_VARIABLE_STR, - ACTION_VARIABLE_STR_ESCAPE, - ACTION_START_ITER, - ACTION_END_ITER, - ACTION_IF_VARIABLE_NOT_EMPTY, - ACTION_END_IF_VARIABLE_NOT_EMPTY, - ACTION_APPLY_TPL, - ACTION_LAST -}; - -enum flags { - FLAGS_ALL = -1, - FLAGS_NEGATE = 1<<0, - FLAGS_QUOTE = 1<<1, - FLAGS_NO_FREE = 1<<2, -}; - -enum lexeme_type { - LEXEME_ERROR, - LEXEME_EOF, - LEXEME_IDENTIFIER, - LEXEME_LEFT_META, - LEXEME_HASH, - LEXEME_RIGHT_META, - LEXEME_TEXT, - LEXEME_SLASH, - LEXEME_QUESTION_MARK, - LEXEME_HAT, - LEXEME_GREATER_THAN, - LEXEME_OPEN_CURLY_BRACE, - LEXEME_CLOSE_CURLY_BRACE, - TOTAL_LEXEMES -}; - -static const char *lexeme_type_str[TOTAL_LEXEMES] = { - [LEXEME_ERROR] = "ERROR", - [LEXEME_EOF] = "EOF", - [LEXEME_IDENTIFIER] = "IDENTIFIER", - [LEXEME_LEFT_META] = "LEFT_META", - [LEXEME_HASH] = "HASH", - [LEXEME_RIGHT_META] = "RIGHT_META", - [LEXEME_TEXT] = "TEXT", - [LEXEME_SLASH] = "SLASH", - [LEXEME_QUESTION_MARK] = "QUESTION_MARK", - [LEXEME_HAT] = "HAT", - [LEXEME_GREATER_THAN] = "GREATER_THAN", - [LEXEME_OPEN_CURLY_BRACE] = "LEXEME_OPEN_CURLY_BRACE", - [LEXEME_CLOSE_CURLY_BRACE] = "LEXEME_CLOSE_CURLY_BRACE" -}; - -struct chunk { - enum action action; - void *data; - enum flags flags; -}; - -struct lwan_tpl_t_ { - struct chunk *chunks; - size_t minimum_size; -}; - -struct symtab { - struct hash *hash; - struct symtab *next; -}; - -struct lexeme { - enum lexeme_type type; - struct { - const char *value; - size_t len; - } value; -}; - -struct lexer { - void *(*state)(struct lexer *); - const char *start, *pos, *end; - - struct { - struct lexeme lexemes[4]; - size_t first; - size_t last; - size_t population; - } ring_buffer; -}; - -struct parser { - lwan_tpl_t *tpl; - const lwan_var_descriptor_t *descriptor; - struct symtab *symtab; - struct lexer lexer; - enum flags flags; - struct list_head stack; - struct { - struct chunk *data; - size_t used, reserved; - } chunks; -}; - -struct stacked_lexeme { - struct list_node stack; - struct lexeme lexeme; -}; - -struct chunk_descriptor { - struct chunk *chunk; - lwan_var_descriptor_t *descriptor; -}; - -static const size_t array_increment_step = 16; - -static const char left_meta[] = "{{"; -static const char right_meta[] = "}}"; -static_assert(sizeof(left_meta) == sizeof(right_meta), - "right_meta and left_meta are the same length"); - -static void *lex_inside_action(struct lexer *lexer); -static void *lex_identifier(struct lexer *lexer); -static void *lex_left_meta(struct lexer *lexer); -static void *lex_right_meta(struct lexer *lexer); -static void *lex_text(struct lexer *lexer); - -static void *parser_meta(struct parser *parser, struct lexeme *lexeme); -static void *parser_text(struct parser *parser, struct lexeme *lexeme); -static void *parser_iter(struct parser *parser, struct lexeme *lexeme); -static void *parser_slash(struct parser *parser, struct lexeme *lexeme); -static void *parser_end_iter(struct parser *parser, struct lexeme *lexeme); -static void *parser_end_var_not_empty(struct parser *parser, struct lexeme *lexeme); -static void *parser_slash(struct parser *parser, struct lexeme *lexeme); -static void *parser_iter(struct parser *parser, struct lexeme *lexeme); -static void *parser_negate_iter(struct parser *parser, struct lexeme *lexeme); -static void *parser_meta(struct parser *parser, struct lexeme *lexeme); -static void *parser_text(struct parser *parser, struct lexeme *lexeme); - -static void *error_vlexeme(struct lexeme *lexeme, const char *msg, va_list ap) - __attribute__((format(printf, 2, 0))); -static void *error_lexeme(struct lexeme *lexeme, const char *msg, ...) - __attribute__((format(printf, 2, 3))); -static void *lex_error(struct lexer *lexer, const char *msg, ...) - __attribute__((format(printf, 2, 3))); - -static lwan_var_descriptor_t * -symtab_lookup(struct parser *parser, const char *var_name) -{ - for (struct symtab *tab = parser->symtab; tab; tab = tab->next) { - lwan_var_descriptor_t *var = hash_find(tab->hash, var_name); - if (var) - return var; - } - - return NULL; -} - -static int -symtab_push(struct parser *parser, const lwan_var_descriptor_t *descriptor) -{ - struct symtab *tab; - - if (!descriptor) - return -ENODEV; - - tab = malloc(sizeof(*tab)); - if (!tab) - return -errno; - - tab->hash = hash_str_new(NULL, NULL); - if (!tab->hash) { - free(tab); - return -ENOMEM; - } - - tab->next = parser->symtab; - parser->symtab = tab; - - for (; descriptor->name; descriptor++) - hash_add(parser->symtab->hash, descriptor->name, descriptor); - - return 0; -} - -static void -symtab_pop(struct parser *parser) -{ - struct symtab *tab = parser->symtab; - - assert(tab); - - hash_free(tab->hash); - parser->symtab = tab->next; - free(tab); -} - -static void emit_lexeme(struct lexer *lexer, struct lexeme *lexeme) -{ - lexer->ring_buffer.lexemes[lexer->ring_buffer.last] = *lexeme; - lexer->ring_buffer.last = (lexer->ring_buffer.last + 1) % N_ELEMENTS(lexer->ring_buffer.lexemes); - lexer->ring_buffer.population++; - - lexer->start = lexer->pos; -} - -static bool consume_lexeme(struct lexer *lexer, struct lexeme **lexeme) -{ - if (!lexer->ring_buffer.population) - return false; - - *lexeme = &lexer->ring_buffer.lexemes[lexer->ring_buffer.first]; - lexer->ring_buffer.first = (lexer->ring_buffer.first + 1) % N_ELEMENTS(lexer->ring_buffer.lexemes); - lexer->ring_buffer.population--; - - return true; -} - -static void emit(struct lexer *lexer, enum lexeme_type lexeme_type) -{ - struct lexeme lexeme = { - .type = lexeme_type, - .value = { - .value = lexer->start, - .len = (size_t)(lexer->pos - lexer->start) - } - }; - emit_lexeme(lexer, &lexeme); -} - -static int next(struct lexer *lexer) -{ - if (lexer->pos >= lexer->end) - return EOF; - int r = *lexer->pos; - lexer->pos++; - return r; -} - -static void ignore(struct lexer *lexer) -{ - lexer->start = lexer->pos; -} - -static void backup(struct lexer *lexer) -{ - lexer->pos--; -} - -static void *error_vlexeme(struct lexeme *lexeme, const char *msg, va_list ap) -{ - int r; - - lexeme->type = LEXEME_ERROR; - - r = vasprintf((char **)&lexeme->value.value, msg, ap); - if (r < 0) { - lexeme->value.value = strdup(strerror(errno)); - if (!lexeme->value.value) - return NULL; - - lexeme->value.len = strlen(lexeme->value.value); - } else { - lexeme->value.len = (size_t)r; - } - - return NULL; -} - -static void *error_lexeme(struct lexeme *lexeme, const char *msg, ...) -{ - void *ret; - va_list ap; - - va_start(ap, msg); - ret = error_vlexeme(lexeme, msg, ap); - va_end(ap); - - return ret; -} - -static void *lex_error(struct lexer *lexer, const char *msg, ...) -{ - struct lexeme lexeme; - va_list ap; - - va_start(ap, msg); - error_vlexeme(&lexeme, msg, ap); - va_end(ap); - - emit_lexeme(lexer, &lexeme); - return NULL; -} - -static bool isident(int ch) -{ - return isalnum(ch) || ch == '_' || ch == '.' || ch == '/'; -} - -static void *lex_identifier(struct lexer *lexer) -{ - while (isident(next(lexer))) - ; - backup(lexer); - emit(lexer, LEXEME_IDENTIFIER); - return lex_inside_action; -} - -static void *lex_partial(struct lexer *lexer) -{ - while (true) { - int r = next(lexer); - - if (r == EOF) - return lex_error(lexer, "unexpected EOF while scanning action"); - if (r == '\n') - return lex_error(lexer, "actions cannot span multiple lines"); - if (isspace(r)) { - ignore(lexer); - continue; - } - if (isident(r)) { - backup(lexer); - return lex_identifier; - } - return lex_error(lexer, "unexpected character: %c", r); - } -} - -static void *lex_quoted_identifier(struct lexer *lexer) -{ - int r; - - emit(lexer, LEXEME_OPEN_CURLY_BRACE); - lex_identifier(lexer); - - r = next(lexer); - if (r != '}') - return lex_error(lexer, "expecting `}', found `%c'", r); - - emit(lexer, LEXEME_CLOSE_CURLY_BRACE); - return lex_inside_action; -} - -static void *lex_comment(struct lexer *lexer) -{ - size_t brackets = strlen(left_meta); - - do { - int r = next(lexer); - if (r == '{') - brackets++; - else if (r == '}') - brackets--; - else if (r == EOF) - return lex_error(lexer, "unexpected EOF while scanning comment end"); - } while (brackets); - - ignore(lexer); - return lex_text; -} - -static void *lex_inside_action(struct lexer *lexer) -{ - while (true) { - int r; - - if (!strncmp(lexer->pos, right_meta, strlen(right_meta))) - return lex_right_meta; - - r = next(lexer); - if (r == EOF) - return lex_error(lexer, "unexpected EOF while scanning action"); - if (r == '\n') - return lex_error(lexer, "actions cannot span multiple lines"); - - if (isspace(r)) { - ignore(lexer); - } else if (r == '#') { - emit(lexer, LEXEME_HASH); - } else if (r == '?') { - emit(lexer, LEXEME_QUESTION_MARK); - } else if (r == '^') { - emit(lexer, LEXEME_HAT); - } else if (r == '>') { - emit(lexer, LEXEME_GREATER_THAN); - return lex_partial; - } else if (r == '{') { - return lex_quoted_identifier; - } else if (r == '/') { - emit(lexer, LEXEME_SLASH); - } else if (isident(r)) { - backup(lexer); - return lex_identifier; - } else { - return lex_error(lexer, "unexpected character: %c", r); - } - } -} - -static void *lex_left_meta(struct lexer *lexer) -{ - lexer->pos += strlen(left_meta); - int r = next(lexer); - if (r == '!') - return lex_comment; - backup(lexer); - - emit(lexer, LEXEME_LEFT_META); - return lex_inside_action; -} - -static void *lex_right_meta(struct lexer *lexer) -{ - lexer->pos += strlen(right_meta); - emit(lexer, LEXEME_RIGHT_META); - return lex_text; -} - -static void *lex_text(struct lexer *lexer) -{ - do { - if (!strncmp(lexer->pos, left_meta, strlen(left_meta))) { - if (lexer->pos > lexer->start) - emit(lexer, LEXEME_TEXT); - return lex_left_meta; - } - if (!strncmp(lexer->pos, right_meta, strlen(right_meta))) - return lex_error(lexer, "unexpected action close sequence"); - } while (next(lexer) != EOF); - if (lexer->pos > lexer->start) - emit(lexer, LEXEME_TEXT); - emit(lexer, LEXEME_EOF); - return NULL; -} - -static bool lex_next(struct lexer *lexer, struct lexeme **lexeme) -{ - while (lexer->state) { - if (consume_lexeme(lexer, lexeme)) - return true; - lexer->state = lexer->state(lexer); - } - - return consume_lexeme(lexer, lexeme); -} - -static void lex_init(struct lexer *lexer, const char *input) -{ - lexer->state = lex_text; - lexer->pos = lexer->start = input; - lexer->end = rawmemchr(input, '\0'); -} - -static void *unexpected_lexeme(struct lexeme *lexeme) -{ - return error_lexeme(lexeme, "unexpected lexeme: %s [%.*s]", - lexeme_type_str[lexeme->type], (int)lexeme->value.len, lexeme->value.value); -} - -static void *unexpected_lexeme_or_lex_error(struct lexeme *lexeme, struct lexeme *lex_error) -{ - if (lex_error && (lex_error->type == LEXEME_ERROR || lex_error->type == LEXEME_EOF)) { - *lexeme = *lex_error; - return NULL; - } - - return unexpected_lexeme(lexeme); -} - -static void parser_push_lexeme(struct parser *parser, struct lexeme *lexeme) -{ - struct stacked_lexeme *stacked_lexeme = malloc(sizeof(*stacked_lexeme)); - if (!stacked_lexeme) - lwan_status_critical_perror("Could not push parser lexeme"); - - stacked_lexeme->lexeme = *lexeme; - list_add(&parser->stack, &stacked_lexeme->stack); -} - -static void emit_chunk(struct parser *parser, enum action action, - enum flags flags, void *data) -{ - struct chunk *chunk; - - if (parser->chunks.used >= parser->chunks.reserved) { - parser->chunks.reserved += array_increment_step; - - chunk = reallocarray(parser->chunks.data, - parser->chunks.reserved, sizeof(struct chunk)); - if (!chunk) - lwan_status_critical_perror("Could not emit template chunk"); - - parser->tpl->chunks = parser->chunks.data = chunk; - } - - chunk = &parser->chunks.data[parser->chunks.used++]; - chunk->action = action; - chunk->flags = flags; - chunk->data = data; -} - -static bool parser_stack_top_matches(struct parser *parser, struct lexeme *lexeme, enum lexeme_type type) -{ - if (list_empty(&parser->stack)) { - error_lexeme(lexeme, "unexpected {{/%.*s}}", (int)lexeme->value.len, lexeme->value.value); - return false; - } - - struct stacked_lexeme *stacked_lexeme = (struct stacked_lexeme *)parser->stack.n.next; - bool matches = (stacked_lexeme->lexeme.type == type - && lexeme->value.len == stacked_lexeme->lexeme.value.len - && !memcmp(stacked_lexeme->lexeme.value.value, lexeme->value.value, lexeme->value.len)); - if (matches) { - list_del(&stacked_lexeme->stack); - free(stacked_lexeme); - return true; - } - - error_lexeme(lexeme, "expecting %s `%.*s' but found `%.*s'", - lexeme_type_str[stacked_lexeme->lexeme.type], - (int)stacked_lexeme->lexeme.value.len, stacked_lexeme->lexeme.value.value, - (int)lexeme->value.len, lexeme->value.value); - return false; -} - -static void *parser_right_meta(struct parser *parser __attribute__((unused)), - struct lexeme *lexeme) -{ - if (lexeme->type != LEXEME_RIGHT_META) - return unexpected_lexeme(lexeme); - return parser_text; -} - -static void *parser_end_iter(struct parser *parser, struct lexeme *lexeme) -{ - struct chunk *iter; - lwan_var_descriptor_t *symbol; - ssize_t idx; - - if (!parser_stack_top_matches(parser, lexeme, LEXEME_IDENTIFIER)) - return NULL; - - symbol = symtab_lookup(parser, strndupa(lexeme->value.value, lexeme->value.len)); - if (!symbol) { - return error_lexeme(lexeme, "Unknown variable: %.*s", (int)lexeme->value.len, - lexeme->value.value); - } - - if (!parser->chunks.used) - return error_lexeme(lexeme, "No chunks were emitted but parsing end iter"); - for (idx = (ssize_t)parser->chunks.used - 1; idx >= 0; idx--) { - iter = &parser->chunks.data[idx]; - - if (iter->action != ACTION_START_ITER) - continue; - if (iter->data == symbol) { - emit_chunk(parser, ACTION_END_ITER, 0, iter); - symtab_pop(parser); - return parser_text; - } - } - - return error_lexeme(lexeme, "Could not find {{#%.*s}}", (int)lexeme->value.len, lexeme->value.value); -} - -static void *parser_end_var_not_empty(struct parser *parser, struct lexeme *lexeme) -{ - struct chunk *iter; - lwan_var_descriptor_t *symbol; - ssize_t idx; - - if (!parser_stack_top_matches(parser, lexeme, LEXEME_IDENTIFIER)) - return NULL; - - symbol = symtab_lookup(parser, strndupa(lexeme->value.value, lexeme->value.len)); - if (!symbol) { - return error_lexeme(lexeme, "Unknown variable: %.*s", (int)lexeme->value.len, - lexeme->value.value); - } - - for (idx = (ssize_t)parser->chunks.used - 1; idx >= 0; idx--) { - iter = &parser->chunks.data[idx]; - if (iter->action != ACTION_IF_VARIABLE_NOT_EMPTY) - continue; - if (iter->data == symbol) { - emit_chunk(parser, ACTION_END_IF_VARIABLE_NOT_EMPTY, 0, symbol); - return parser_right_meta; - } - } - - return error_lexeme(lexeme, "Could not find {{%.*s?}}", (int)lexeme->value.len, lexeme->value.value); -} - -static void *parser_slash(struct parser *parser, struct lexeme *lexeme) -{ - if (lexeme->type == LEXEME_IDENTIFIER) { - struct lexeme *next = NULL; - - if (!lex_next(&parser->lexer, &next)) - return unexpected_lexeme_or_lex_error(lexeme, next); - - if (next->type == LEXEME_RIGHT_META) - return parser_end_iter(parser, lexeme); - - if (next->type == LEXEME_QUESTION_MARK) - return parser_end_var_not_empty(parser, lexeme); - - return unexpected_lexeme_or_lex_error(lexeme, next); - } - - return unexpected_lexeme(lexeme); -} - -static void *parser_iter(struct parser *parser, struct lexeme *lexeme) -{ - if (lexeme->type == LEXEME_IDENTIFIER) { - enum flags negate = parser->flags & FLAGS_NEGATE; - const char *symname = strndupa(lexeme->value.value, lexeme->value.len); - lwan_var_descriptor_t *symbol = symtab_lookup(parser, symname); - if (!symbol) { - return error_lexeme(lexeme, "Unknown variable: %.*s", (int)lexeme->value.len, - lexeme->value.value); - } - - int r = symtab_push(parser, symbol->list_desc); - if (r < 0) { - if (r == -ENODEV) - return error_lexeme(lexeme, "Couldn't find descriptor for variable `%s'", symname); - return error_lexeme(lexeme, "Could not push symbol table (out of memory)"); - } - - emit_chunk(parser, ACTION_START_ITER, negate | FLAGS_NO_FREE, symbol); - - parser_push_lexeme(parser, lexeme); - parser->flags &= ~FLAGS_NEGATE; - return parser_right_meta; - } - - return unexpected_lexeme(lexeme); -} - -static void *parser_negate_iter(struct parser *parser, struct lexeme *lexeme) -{ - if (lexeme->type != LEXEME_HASH) - return unexpected_lexeme(lexeme); - - parser->flags ^= FLAGS_NEGATE; - return parser_iter; -} - -static void *parser_identifier(struct parser *parser, struct lexeme *lexeme) -{ - struct lexeme *next = NULL; - - if (!lex_next(&parser->lexer, &next)) { - if (next) - *lexeme = *next; - return NULL; - } - - if (parser->flags & FLAGS_QUOTE) { - if (next->type != LEXEME_CLOSE_CURLY_BRACE) - return error_lexeme(lexeme, "Expecting closing brace"); - if (!lex_next(&parser->lexer, &next)) - return unexpected_lexeme_or_lex_error(lexeme, next); - } - - if (next->type == LEXEME_RIGHT_META) { - lwan_var_descriptor_t *symbol = symtab_lookup(parser, strndupa(lexeme->value.value, lexeme->value.len)); - if (!symbol) { - return error_lexeme(lexeme, "Unknown variable: %.*s", (int)lexeme->value.len, - lexeme->value.value); - } - - emit_chunk(parser, ACTION_VARIABLE, parser->flags, symbol); - - parser->flags &= ~FLAGS_QUOTE; - parser->tpl->minimum_size += lexeme->value.len + 1; - return parser_text; - } - - if (next->type == LEXEME_QUESTION_MARK) { - lwan_var_descriptor_t *symbol = symtab_lookup(parser, strndupa(lexeme->value.value, lexeme->value.len)); - if (!symbol) { - return error_lexeme(lexeme, "Unknown variable: %.*s", (int)lexeme->value.len, - lexeme->value.value); - } - - emit_chunk(parser, ACTION_IF_VARIABLE_NOT_EMPTY, FLAGS_NO_FREE, symbol); - parser_push_lexeme(parser, lexeme); - - return parser_right_meta; - } - - return unexpected_lexeme_or_lex_error(lexeme, next); -} - -static void *parser_partial(struct parser *parser, struct lexeme *lexeme) -{ - lwan_tpl_t *tpl; - char *filename = strndupa(lexeme->value.value, lexeme->value.len); - - if (lexeme->type != LEXEME_IDENTIFIER) - return unexpected_lexeme(lexeme); - - tpl = lwan_tpl_compile_file(filename, parser->descriptor); - if (tpl) { - emit_chunk(parser, ACTION_APPLY_TPL, 0, tpl); - return parser_right_meta; - } - - return error_lexeme(lexeme, "Could not compile template ``%s''", filename); -} - -static void *parser_meta(struct parser *parser, struct lexeme *lexeme) -{ - if (lexeme->type == LEXEME_OPEN_CURLY_BRACE) { - if (parser->flags & FLAGS_QUOTE) - return unexpected_lexeme(lexeme); - - parser->flags |= FLAGS_QUOTE; - return parser_meta; - } - - if (lexeme->type == LEXEME_IDENTIFIER) - return parser_identifier(parser, lexeme); - - if (lexeme->type == LEXEME_GREATER_THAN) - return parser_partial; - - if (lexeme->type == LEXEME_HASH) - return parser_iter; - - if (lexeme->type == LEXEME_HAT) - return parser_negate_iter; - - if (lexeme->type == LEXEME_SLASH) - return parser_slash; - - return unexpected_lexeme(lexeme); -} - -static void *parser_text(struct parser *parser, struct lexeme *lexeme) -{ - if (lexeme->type == LEXEME_LEFT_META) - return parser_meta; - if (lexeme->type == LEXEME_TEXT) { - if (lexeme->value.len == 1) { - emit_chunk(parser, ACTION_APPEND_CHAR, 0, (void *)(uintptr_t)*lexeme->value.value); - } else { - strbuf_t *buf = strbuf_new_with_size(lexeme->value.len); - if (!buf) - return error_lexeme(lexeme, "Out of memory"); - strbuf_set(buf, lexeme->value.value, lexeme->value.len); - emit_chunk(parser, ACTION_APPEND, 0, buf); - } - parser->tpl->minimum_size += lexeme->value.len; - return parser_text; - } - if (lexeme->type == LEXEME_EOF) { - emit_chunk(parser, ACTION_LAST, 0, NULL); - return NULL; - } - - return unexpected_lexeme(lexeme); -} - -void -lwan_append_int_to_strbuf(strbuf_t *buf, void *ptr) -{ - char convertbuf[INT_TO_STR_BUFFER_SIZE]; - size_t len; - char *converted; - - converted = int_to_string(*(int *)ptr, convertbuf, &len); - strbuf_append_str(buf, converted, len); -} - -bool -lwan_tpl_int_is_empty(void *ptr) -{ - return (*(int *)ptr) == 0; -} - -void -lwan_append_double_to_strbuf(strbuf_t *buf, void *ptr) -{ - strbuf_append_printf(buf, "%f", *(double *)ptr); -} - -bool -lwan_tpl_double_is_empty(void *ptr) -{ - return (*(double *)ptr) == 0.0f; -} - -void -lwan_append_str_to_strbuf(strbuf_t *buf, void *ptr) -{ - const char *str = *(char **)ptr; - - if (LIKELY(str)) - strbuf_append_str(buf, str, 0); -} - -void -lwan_append_str_escaped_to_strbuf(strbuf_t *buf, void *ptr) -{ - if (UNLIKELY(!ptr)) - return; - - const char *str = *(char **)ptr; - if (UNLIKELY(!str)) - return; - - for (const char *p = str; *p; p++) { - if (*p == '<') - strbuf_append_str(buf, "<", 4); - else if (*p == '>') - strbuf_append_str(buf, ">", 4); - else if (*p == '&') - strbuf_append_str(buf, "&", 5); - else if (*p == '"') - strbuf_append_str(buf, """, 6); - else if (*p == '\'') - strbuf_append_str(buf, "'", 6); - else if (*p == '/') - strbuf_append_str(buf, "/", 6); - else - strbuf_append_char(buf, *p); - } -} - -bool -lwan_tpl_str_is_empty(void *ptr) -{ - if (UNLIKELY(!ptr)) - return true; - - const char *str = *(const char **)ptr; - return !str || *str == '\0'; -} - -static void -free_chunk(struct chunk *chunk) -{ - if (!chunk) - return; - if (chunk->flags & FLAGS_NO_FREE) - return; - - switch (chunk->action) { - case ACTION_LAST: - case ACTION_APPEND_CHAR: - case ACTION_VARIABLE: - case ACTION_VARIABLE_STR: - case ACTION_VARIABLE_STR_ESCAPE: - case ACTION_END_IF_VARIABLE_NOT_EMPTY: - case ACTION_END_ITER: - /* do nothing */ - break; - case ACTION_IF_VARIABLE_NOT_EMPTY: - case ACTION_START_ITER: - free(chunk->data); - break; - case ACTION_APPEND: - strbuf_free(chunk->data); - break; - case ACTION_APPLY_TPL: - lwan_tpl_free(chunk->data); - break; - } -} - -void -lwan_tpl_free(lwan_tpl_t *tpl) -{ - struct chunk *iter; - - if (!tpl) - return; - - if (tpl->chunks) { - for (iter = tpl->chunks; iter->action != ACTION_LAST; iter++) - free_chunk(iter); - free(tpl->chunks); - } - - free(tpl); -} - -static bool -post_process_template(struct parser *parser) -{ - size_t idx; - struct chunk *prev_chunk; - -#define CHUNK_IDX(c) (size_t)(ptrdiff_t)((c) - parser->chunks.data) - - for (idx = 0; idx < parser->chunks.used; idx++) { - struct chunk *chunk = &parser->chunks.data[idx]; - - if (chunk->action == ACTION_IF_VARIABLE_NOT_EMPTY) { - for (prev_chunk = chunk; ; chunk++) { - if (chunk->action == ACTION_LAST) - break; - if (chunk->action == ACTION_END_IF_VARIABLE_NOT_EMPTY - && chunk->data == prev_chunk->data) - break; - } - - struct chunk_descriptor *cd = malloc(sizeof(*cd)); - if (!cd) - lwan_status_critical_perror("malloc"); - - cd->descriptor = prev_chunk->data; - cd->chunk = chunk; - prev_chunk->data = cd; - prev_chunk->flags &= ~FLAGS_NO_FREE; - - idx = CHUNK_IDX(prev_chunk) + 1; - } else if (chunk->action == ACTION_START_ITER) { - enum flags flags = chunk->flags; - - for (prev_chunk = chunk; ; chunk++) { - if (chunk->action == ACTION_LAST) - break; - if (chunk->action == ACTION_END_ITER && chunk->data == prev_chunk) { - chunk->flags |= flags; - break; - } - } - - struct chunk_descriptor *cd = malloc(sizeof(*cd)); - if (!cd) - lwan_status_critical_perror("malloc"); - - cd->descriptor = prev_chunk->data; - prev_chunk->data = cd; - prev_chunk->flags &= ~FLAGS_NO_FREE; - - if (chunk->action == ACTION_LAST) - cd->chunk = chunk; - else - cd->chunk = chunk + 1; - - idx = CHUNK_IDX(prev_chunk) + 1; - } else if (chunk->action == ACTION_VARIABLE) { - lwan_var_descriptor_t *descriptor = chunk->data; - bool escape = chunk->flags & FLAGS_QUOTE; - - if (descriptor->append_to_strbuf == lwan_append_str_to_strbuf) { - if (escape) - chunk->action = ACTION_VARIABLE_STR_ESCAPE; - else - chunk->action = ACTION_VARIABLE_STR; - chunk->data = (void *)(uintptr_t)descriptor->offset; - } else if (escape) { - lwan_status_error("Variable must be string to be escaped"); - return false; - } else if (!descriptor->append_to_strbuf) { - lwan_status_error("Invalid variable descriptor"); - return false; - } - } else if (chunk->action == ACTION_LAST) { - break; - } - } - - parser->tpl->chunks = parser->chunks.data; - - return true; - -#undef CHUNK_IDX -} - -static bool parser_init(struct parser *parser, const lwan_var_descriptor_t *descriptor, - const char *string) -{ - struct chunk *chunks; - - if (symtab_push(parser, descriptor) < 0) - return false; - - chunks = reallocarray(NULL, parser->chunks.reserved, sizeof(struct chunk)); - parser->tpl->chunks = parser->chunks.data = chunks; - if (!chunks) { - symtab_pop(parser); - return false; - } - - lex_init(&parser->lexer, string); - list_head_init(&parser->stack); - - return true; -} - -static bool parser_shutdown(struct parser *parser, struct lexeme *lexeme) -{ - bool success = true; - - if (lexeme->type == LEXEME_ERROR && lexeme->value.value) { - lwan_status_error("Parser error: %.*s", (int)lexeme->value.len, lexeme->value.value); - free((char *)lexeme->value.value); - - success = false; - } - - if (!list_empty(&parser->stack)) { - struct stacked_lexeme *stacked, *stacked_next; - - list_for_each_safe(&parser->stack, stacked, stacked_next, stack) { - lwan_status_error("Parser error: EOF while looking for matching {{/%.*s}}", - (int)stacked->lexeme.value.len, stacked->lexeme.value.value); - list_del(&stacked->stack); - free(stacked); - } - - success = false; - } - - symtab_pop(parser); - if (parser->symtab) { - lwan_status_error("Parser error: Symbol table not empty when finishing parser"); - - while (parser->symtab) - symtab_pop(parser); - - success = false; - } - - if (parser->flags & FLAGS_NEGATE) { - lwan_status_error("Parser error: unmatched negation"); - success = false; - } - if (parser->flags & FLAGS_QUOTE) { - lwan_status_error("Parser error: unmatched quote"); - success = false; - } - - if (success) - success = post_process_template(parser); - - if (!success) { - /* Emit a ACTION_LAST chunk so that lwan_tpl_free() knows when to stop */ - emit_chunk(parser, ACTION_LAST, 0, NULL); - } - - return success; -} - -static bool parse_string(lwan_tpl_t *tpl, const char *string, const lwan_var_descriptor_t *descriptor) -{ - struct parser parser = { - .tpl = tpl, - .symtab = NULL, - .chunks = { .used = 0, .reserved = array_increment_step }, - .descriptor = descriptor - }; - void *(*state)(struct parser *parser, struct lexeme *lexeme) = parser_text; - struct lexeme *lexeme = NULL; - - if (!parser_init(&parser, descriptor, string)) - return false; - - while (state && lex_next(&parser.lexer, &lexeme) && lexeme->type != LEXEME_ERROR) - state = state(&parser, lexeme); - - return parser_shutdown(&parser, lexeme); -} - -lwan_tpl_t * -lwan_tpl_compile_string(const char *string, const lwan_var_descriptor_t *descriptor) -{ - lwan_tpl_t *tpl; - - tpl = calloc(1, sizeof(*tpl)); - if (tpl) { - if (parse_string(tpl, string, descriptor)) - return tpl; - } - - lwan_tpl_free(tpl); - return NULL; -} - -lwan_tpl_t * -lwan_tpl_compile_file(const char *filename, const lwan_var_descriptor_t *descriptor) -{ - int fd; - struct stat st; - char *mapped; - lwan_tpl_t *tpl = NULL; - - fd = open(filename, O_RDONLY | O_CLOEXEC); - if (fd < 0) - goto end; - - if (fstat(fd, &st) < 0) - goto close_file; - - mapped = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_SHARED, fd, 0); - if (mapped == MAP_FAILED) - goto close_file; - - tpl = lwan_tpl_compile_string(mapped, descriptor); - - if (munmap(mapped, (size_t)st.st_size) < 0) - lwan_status_perror("munmap"); - -close_file: - close(fd); -end: - return tpl; -} - -static struct chunk * -apply_until(lwan_tpl_t *tpl, struct chunk *chunks, strbuf_t *buf, void *variables, - void *until_data) -{ - static const void *const dispatch_table[] = { - [ACTION_APPEND] = &&action_append, - [ACTION_APPEND_CHAR] = &&action_append_char, - [ACTION_VARIABLE] = &&action_variable, - [ACTION_VARIABLE_STR] = &&action_variable_str, - [ACTION_VARIABLE_STR_ESCAPE] = &&action_variable_str_escape, - [ACTION_IF_VARIABLE_NOT_EMPTY] = &&action_if_variable_not_empty, - [ACTION_END_IF_VARIABLE_NOT_EMPTY] = &&action_end_if_variable_not_empty, - [ACTION_APPLY_TPL] = &&action_apply_tpl, - [ACTION_START_ITER] = &&action_start_iter, - [ACTION_END_ITER] = &&action_end_iter, - [ACTION_LAST] = &&finalize - }; - coro_switcher_t switcher; - coro_t *coro = NULL; - struct chunk *chunk = chunks; - - if (UNLIKELY(!chunk)) - return NULL; - -#define DISPATCH() do { goto *dispatch_table[chunk->action]; } while(false) -#define NEXT_ACTION() do { chunk++; DISPATCH(); } while(false) - - DISPATCH(); - -action_append: - strbuf_append_str(buf, strbuf_get_buffer(chunk->data), - strbuf_get_length(chunk->data)); - NEXT_ACTION(); - -action_append_char: - strbuf_append_char(buf, (char)(uintptr_t)chunk->data); - NEXT_ACTION(); - -action_variable: { - lwan_var_descriptor_t *descriptor = chunk->data; - descriptor->append_to_strbuf(buf, (char *)variables + descriptor->offset); - NEXT_ACTION(); - } - -action_variable_str: - lwan_append_str_to_strbuf(buf, (char *)variables + (uintptr_t)chunk->data); - NEXT_ACTION(); - -action_variable_str_escape: - lwan_append_str_escaped_to_strbuf(buf, (char *)variables + (uintptr_t)chunk->data); - NEXT_ACTION(); - -action_if_variable_not_empty: { - struct chunk_descriptor *cd = chunk->data; - bool empty = cd->descriptor->get_is_empty((char *)variables + cd->descriptor->offset); - if (chunk->flags & FLAGS_NEGATE) - empty = !empty; - if (empty) { - chunk = cd->chunk; - } else { - chunk = apply_until(tpl, chunk + 1, buf, variables, cd->chunk); - } - NEXT_ACTION(); - } - -action_end_if_variable_not_empty: - if (LIKELY(until_data == chunk)) - goto finalize; - NEXT_ACTION(); - -action_apply_tpl: { - strbuf_t *tmp = lwan_tpl_apply(chunk->data, variables); - strbuf_append_str(buf, strbuf_get_buffer(tmp), strbuf_get_length(tmp)); - strbuf_free(tmp); - NEXT_ACTION(); - } - -action_start_iter: - if (UNLIKELY(coro != NULL)) { - lwan_status_warning("Coroutine is not NULL when starting iteration"); - NEXT_ACTION(); - } - - struct chunk_descriptor *cd = chunk->data; - coro = coro_new(&switcher, cd->descriptor->generator, variables); - - bool resumed = coro_resume_value(coro, 0); - enum flags negate = chunk->flags & FLAGS_NEGATE; - if (negate) - resumed = !resumed; - if (!resumed) { - chunk = cd->chunk; - - if (negate) - coro_resume_value(coro, 1); - - coro_free(coro); - coro = NULL; - - if (negate) - DISPATCH(); - NEXT_ACTION(); - } - - chunk = apply_until(tpl, chunk + 1, buf, variables, chunk); - DISPATCH(); - -action_end_iter: - if (until_data == chunk->data) - goto finalize; - - if (UNLIKELY(!coro)) { - if (!chunk->flags) - lwan_status_warning("Coroutine is NULL when finishing iteration"); - NEXT_ACTION(); - } - - if (!coro_resume_value(coro, 0)) { - coro_free(coro); - coro = NULL; - NEXT_ACTION(); - } - - chunk = apply_until(tpl, ((struct chunk *)chunk->data) + 1, buf, variables, chunk->data); - DISPATCH(); - -finalize: - return chunk; -#undef DISPATCH -#undef NEXT_ACTION -} - -strbuf_t * -lwan_tpl_apply_with_buffer(lwan_tpl_t *tpl, strbuf_t *buf, void *variables) -{ - if (UNLIKELY(!strbuf_reset_length(buf))) - return NULL; - - if (UNLIKELY(!strbuf_grow_to(buf, tpl->minimum_size))) - return NULL; - - apply_until(tpl, tpl->chunks, buf, variables, NULL); - - return buf; -} - -strbuf_t * -lwan_tpl_apply(lwan_tpl_t *tpl, void *variables) -{ - strbuf_t *buf = strbuf_new_with_size(tpl->minimum_size); - return lwan_tpl_apply_with_buffer(tpl, buf, variables); -} - -#ifdef TEMPLATE_TEST - -struct test_struct { - int some_int; - char *a_string; -}; - -int main(int argc, char *argv[]) -{ - if (argc < 2) { - printf("Usage: %s file.tpl\n", argv[0]); - return 1; - } - - printf("*** Compiling template...\n"); - lwan_var_descriptor_t desc[] = { - TPL_VAR_INT(struct test_struct, some_int), - TPL_VAR_STR(struct test_struct, a_string), - TPL_VAR_SENTINEL - }; - lwan_tpl_t *tpl = lwan_tpl_compile_file(argv[1], desc); - if (!tpl) - return 1; - - printf("*** Applying template 100000 times...\n"); - for (size_t i = 0; i < 100000; i++) { - strbuf_t *applied = lwan_tpl_apply(tpl, &(struct test_struct) { - .some_int = 42, - .a_string = "some string" - }); - strbuf_free(applied); - } - - lwan_tpl_free(tpl); - return 0; -} - -#endif /* TEMPLATE_TEST */ diff --git a/common/lwan-template.h b/common/lwan-template.h deleted file mode 100644 index e73ff9c09..000000000 --- a/common/lwan-template.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -#pragma once - -#include -#include "strbuf.h" -#include "lwan-coro.h" - -typedef struct lwan_tpl_t_ lwan_tpl_t; -typedef struct lwan_var_descriptor_t_ lwan_var_descriptor_t; - -typedef int (*lwan_tpl_list_generator_t)(coro_t *coro); - -struct lwan_var_descriptor_t_ { - const char *name; - const off_t offset; - - void (*append_to_strbuf)(strbuf_t *buf, void *ptr); - bool (*get_is_empty)(void *ptr); - - lwan_tpl_list_generator_t generator; - const lwan_var_descriptor_t *list_desc; -}; - -#define TPL_VAR_SIMPLE(struct_, var_, append_to_strbuf_, get_is_empty_) \ - { \ - .name = #var_, \ - .offset = offsetof(struct_, var_), \ - .append_to_strbuf = append_to_strbuf_, \ - .get_is_empty = get_is_empty_ \ - } - -#define TPL_VAR_SEQUENCE(struct_, var_, generator_, seqitem_desc_) \ - { \ - .name = #var_, \ - .offset = offsetof(struct_, var_.generator), \ - .generator = generator_, \ - .list_desc = seqitem_desc_ \ - } - -#define TPL_VAR_INT(struct_, var_) \ - TPL_VAR_SIMPLE(struct_, var_, lwan_append_int_to_strbuf, lwan_tpl_int_is_empty) - -#define TPL_VAR_DOUBLE(struct_, var_) \ - TPL_VAR_SIMPLE(struct_, var_, lwan_append_double_to_strbuf, lwan_tpl_double_is_empty) - -#define TPL_VAR_STR(struct_, var_) \ - TPL_VAR_SIMPLE(struct_, var_, lwan_append_str_to_strbuf, lwan_tpl_str_is_empty) - -#define TPL_VAR_STR_ESCAPE(struct_, var_) \ - TPL_VAR_SIMPLE(struct_, var_, lwan_append_str_escaped_to_strbuf, lwan_tpl_str_is_empty) - -#define TPL_VAR_SENTINEL \ - { NULL, 0, NULL, NULL, NULL, NULL } - -/* - * These functions are not meant to be used directly. We do need a pointer to - * them, though, that's why they're exported. Eventually this will move to - * something more opaque. - */ -void lwan_append_int_to_strbuf(strbuf_t *buf, void *ptr); -bool lwan_tpl_int_is_empty(void *ptr); -void lwan_append_str_to_strbuf(strbuf_t *buf, void *ptr); -void lwan_append_str_escaped_to_strbuf(strbuf_t *buf, void *ptr); -bool lwan_tpl_str_is_empty(void *ptr); -void lwan_append_double_to_strbuf(strbuf_t *buf, void *ptr); -bool lwan_tpl_double_is_empty(void *ptr); - -lwan_tpl_t *lwan_tpl_compile_string(const char *string, const lwan_var_descriptor_t *descriptor); -lwan_tpl_t *lwan_tpl_compile_file(const char *filename, const lwan_var_descriptor_t *descriptor); -strbuf_t *lwan_tpl_apply(lwan_tpl_t *tpl, void *variables); -strbuf_t *lwan_tpl_apply_with_buffer(lwan_tpl_t *tpl, strbuf_t *buf, void *variables); -void lwan_tpl_free(lwan_tpl_t *tpl); - diff --git a/common/lwan-thread.c b/common/lwan-thread.c deleted file mode 100644 index 3a88d82b6..000000000 --- a/common/lwan-thread.c +++ /dev/null @@ -1,469 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012, 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include - -#include "lwan-private.h" - -struct death_queue_t { - const lwan_t *lwan; - lwan_connection_t *conns; - lwan_connection_t head; - unsigned time; - unsigned short keep_alive_timeout; -}; - -static const uint32_t events_by_write_flag[] = { - EPOLLOUT | EPOLLRDHUP | EPOLLERR, - EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLET -}; - -static inline int death_queue_node_to_idx(struct death_queue_t *dq, - lwan_connection_t *conn) -{ - return (conn == &dq->head) ? -1 : (int)(ptrdiff_t)(conn - dq->conns); -} - -static inline lwan_connection_t *death_queue_idx_to_node(struct death_queue_t *dq, - int idx) -{ - return (idx < 0) ? &dq->head : &dq->conns[idx]; -} - -static void death_queue_insert(struct death_queue_t *dq, - lwan_connection_t *new_node) -{ - new_node->next = -1; - new_node->prev = dq->head.prev; - lwan_connection_t *prev = death_queue_idx_to_node(dq, dq->head.prev); - dq->head.prev = prev->next = death_queue_node_to_idx(dq, new_node); -} - -static void death_queue_remove(struct death_queue_t *dq, - lwan_connection_t *node) -{ - lwan_connection_t *prev = death_queue_idx_to_node(dq, node->prev); - lwan_connection_t *next = death_queue_idx_to_node(dq, node->next); - next->prev = node->prev; - prev->next = node->next; - - /* FIXME: This shouldn't be required; there may be a bug somewhere - * that manifests if lots of chunked encoding requests are performed. */ - node->next = node->prev = -1; -} - -static bool death_queue_empty(struct death_queue_t *dq) -{ - return dq->head.next < 0; -} - -static void death_queue_move_to_last(struct death_queue_t *dq, - lwan_connection_t *conn) -{ - /* - * If the connection isn't keep alive, it might have a coroutine that - * should be resumed. If that's the case, schedule for this request to - * die according to the keep alive timeout. - * - * If it's not a keep alive connection, or the coroutine shouldn't be - * resumed -- then just mark it to be reaped right away. - */ - conn->time_to_die = dq->time + dq->keep_alive_timeout * - (unsigned)!!(conn->flags & (CONN_KEEP_ALIVE | CONN_SHOULD_RESUME_CORO)); - - death_queue_remove(dq, conn); - death_queue_insert(dq, conn); -} - -static void -death_queue_init(struct death_queue_t *dq, const lwan_t *lwan) -{ - dq->lwan = lwan; - dq->conns = lwan->conns; - dq->time = 0; - dq->keep_alive_timeout = lwan->config.keep_alive_timeout; - dq->head.next = dq->head.prev = -1; -} - -static ALWAYS_INLINE int -death_queue_epoll_timeout(struct death_queue_t *dq) -{ - return death_queue_empty(dq) ? -1 : 1000; -} - -static ALWAYS_INLINE void -destroy_coro(struct death_queue_t *dq, lwan_connection_t *conn) -{ - death_queue_remove(dq, conn); - if (LIKELY(conn->coro)) { - coro_free(conn->coro); - conn->coro = NULL; - } - if (conn->flags & CONN_IS_ALIVE) { - conn->flags &= ~CONN_IS_ALIVE; - close(lwan_connection_get_fd(dq->lwan, conn)); - } -} - -static ALWAYS_INLINE int -min(const int a, const int b) -{ - return a < b ? a : b; -} - -static int -process_request_coro(coro_t *coro) -{ - strbuf_t *strbuf = coro_malloc_full(coro, sizeof(*strbuf), strbuf_free); - lwan_connection_t *conn = coro_get_data(coro); - lwan_t *lwan = conn->thread->lwan; - int fd = lwan_connection_get_fd(lwan, conn); - char request_buffer[DEFAULT_BUFFER_SIZE]; - lwan_value_t buffer = { - .value = request_buffer, - .len = 0 - }; - char *next_request = NULL; - - strbuf_init(strbuf); - - while (true) { - lwan_request_t request = { - .conn = conn, - .fd = fd, - .response = { - .buffer = strbuf - }, - }; - - assert(conn->flags & CONN_IS_ALIVE); - - next_request = lwan_process_request(lwan, &request, &buffer, next_request); - if (!next_request) - break; - - coro_yield(coro, CONN_CORO_MAY_RESUME); - - if (UNLIKELY(!strbuf_reset_length(strbuf))) - return CONN_CORO_ABORT; - } - - return CONN_CORO_FINISHED; -} - -static ALWAYS_INLINE void -resume_coro_if_needed(struct death_queue_t *dq, lwan_connection_t *conn, - int epoll_fd) -{ - assert(conn->coro); - - if (!(conn->flags & CONN_SHOULD_RESUME_CORO)) - return; - - lwan_connection_coro_yield_t yield_result = coro_resume(conn->coro); - /* CONN_CORO_ABORT is -1, but comparing with 0 is cheaper */ - if (yield_result < CONN_CORO_MAY_RESUME) { - destroy_coro(dq, conn); - return; - } - - bool write_events; - if (conn->flags & CONN_MUST_READ) { - write_events = true; - } else { - bool should_resume_coro = (yield_result == CONN_CORO_MAY_RESUME); - - if (should_resume_coro) - conn->flags |= CONN_SHOULD_RESUME_CORO; - else - conn->flags &= ~CONN_SHOULD_RESUME_CORO; - - write_events = (conn->flags & CONN_WRITE_EVENTS); - if (should_resume_coro == write_events) - return; - } - - struct epoll_event event = { - .events = events_by_write_flag[write_events], - .data.ptr = conn - }; - - int fd = lwan_connection_get_fd(dq->lwan, conn); - if (UNLIKELY(epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event) < 0)) - lwan_status_perror("epoll_ctl"); - - conn->flags ^= CONN_WRITE_EVENTS; -} - -static void -death_queue_kill_waiting(struct death_queue_t *dq) -{ - dq->time++; - - while (!death_queue_empty(dq)) { - lwan_connection_t *conn = death_queue_idx_to_node(dq, dq->head.next); - - if (conn->time_to_die > dq->time) - return; - - destroy_coro(dq, conn); - } - - /* Death queue exhausted: reset epoch */ - dq->time = 0; -} - -static void -death_queue_kill_all(struct death_queue_t *dq) -{ - while (!death_queue_empty(dq)) { - lwan_connection_t *conn = death_queue_idx_to_node(dq, dq->head.next); - destroy_coro(dq, conn); - } -} - -void -lwan_format_rfc_time(time_t t, char buffer[30]) -{ - struct tm tm; - - if (UNLIKELY(!gmtime_r(&t, &tm))) { - lwan_status_perror("gmtime_r"); - return; - } - - if (UNLIKELY(!strftime(buffer, 30, "%a, %d %b %Y %H:%M:%S GMT", &tm))) - lwan_status_perror("strftime"); -} - -static void -update_date_cache(lwan_thread_t *thread) -{ - time_t now = time(NULL); - if (now != thread->date.last) { - thread->date.last = now; - lwan_format_rfc_time(now, thread->date.date); - lwan_format_rfc_time(now + (time_t)thread->lwan->config.expires, - thread->date.expires); - } -} - -static ALWAYS_INLINE void -spawn_or_reset_coro_if_needed(lwan_connection_t *conn, - coro_switcher_t *switcher, struct death_queue_t *dq) -{ - if (conn->coro) { - if (conn->flags & CONN_SHOULD_RESUME_CORO) - return; - - coro_reset(conn->coro, process_request_coro, conn); - } else { - conn->coro = coro_new(switcher, process_request_coro, conn); - - death_queue_insert(dq, conn); - conn->flags |= CONN_IS_ALIVE; - } - conn->flags |= CONN_SHOULD_RESUME_CORO; - conn->flags &= ~CONN_WRITE_EVENTS; -} - -static lwan_connection_t * -grab_and_watch_client(int epoll_fd, int pipe_fd, lwan_connection_t *conns) -{ - int fd; - if (UNLIKELY(read(pipe_fd, &fd, sizeof(int)) != sizeof(int))) - return NULL; - - struct epoll_event event = { - .events = events_by_write_flag[1], - .data.ptr = &conns[fd] - }; - if (UNLIKELY(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) < 0)) - return NULL; - - return &conns[fd]; -} - -static void * -thread_io_loop(void *data) -{ - lwan_thread_t *t = data; - const int epoll_fd = t->epoll_fd; - const int read_pipe_fd = t->pipe_fd[0]; - const int max_events = min((int)t->lwan->thread.max_fd, 1024); - const lwan_t *lwan = t->lwan; - lwan_connection_t *conns = lwan->conns; - struct epoll_event *events; - coro_switcher_t switcher; - struct death_queue_t dq; - int n_fds; - - lwan_status_debug("Starting IO loop on thread #%d", - (unsigned short)(ptrdiff_t)(t - t->lwan->thread.threads) + 1); - - events = calloc((size_t)max_events, sizeof(*events)); - if (UNLIKELY(!events)) - lwan_status_critical("Could not allocate memory for events"); - - death_queue_init(&dq, lwan); - - for (;;) { - switch (n_fds = epoll_wait(epoll_fd, events, max_events, - death_queue_epoll_timeout(&dq))) { - case -1: - switch (errno) { - case EBADF: - case EINVAL: - goto epoll_fd_closed; - } - continue; - case 0: /* timeout: shutdown waiting sockets */ - death_queue_kill_waiting(&dq); - break; - default: /* activity in some of this poller's file descriptor */ - update_date_cache(t); - - for (struct epoll_event *ep_event = events; n_fds--; ep_event++) { - lwan_connection_t *conn; - - if (!ep_event->data.ptr) { - conn = grab_and_watch_client(epoll_fd, read_pipe_fd, conns); - if (UNLIKELY(!conn)) - continue; - spawn_or_reset_coro_if_needed(conn, &switcher, &dq); - } else { - conn = ep_event->data.ptr; - if (UNLIKELY(ep_event->events & (EPOLLRDHUP | EPOLLHUP))) { - destroy_coro(&dq, conn); - continue; - } - - spawn_or_reset_coro_if_needed(conn, &switcher, &dq); - resume_coro_if_needed(&dq, conn, epoll_fd); - } - - death_queue_move_to_last(&dq, conn); - } - } - } - -epoll_fd_closed: - death_queue_kill_all(&dq); - free(events); - - return NULL; -} - -static void -create_thread(lwan_t *l, lwan_thread_t *thread) -{ - pthread_attr_t attr; - - memset(thread, 0, sizeof(*thread)); - thread->lwan = l; - - if ((thread->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) - lwan_status_critical_perror("epoll_create"); - - if (pthread_attr_init(&attr)) - lwan_status_critical_perror("pthread_attr_init"); - - if (pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM)) - lwan_status_critical_perror("pthread_attr_setscope"); - - if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE)) - lwan_status_critical_perror("pthread_attr_setdetachstate"); - - if (pipe2(thread->pipe_fd, O_NONBLOCK | O_CLOEXEC) < 0) - lwan_status_critical_perror("pipe"); - - struct epoll_event event = { .events = EPOLLIN, .data.ptr = NULL }; - if (epoll_ctl(thread->epoll_fd, EPOLL_CTL_ADD, thread->pipe_fd[0], &event) < 0) - lwan_status_critical_perror("epoll_ctl"); - - if (pthread_create(&thread->self, &attr, thread_io_loop, thread)) - lwan_status_critical_perror("pthread_create"); - - if (pthread_attr_destroy(&attr)) - lwan_status_critical_perror("pthread_attr_destroy"); -} - -void -lwan_thread_add_client(lwan_thread_t *t, int fd) -{ - t->lwan->conns[fd].flags = 0; - t->lwan->conns[fd].thread = t; - - if (UNLIKELY(write(t->pipe_fd[1], &fd, sizeof(int)) < 0)) - lwan_status_perror("write"); -} - -void -lwan_thread_init(lwan_t *l) -{ - lwan_status_debug("Initializing threads"); - - l->thread.threads = calloc((size_t)l->thread.count, sizeof(lwan_thread_t)); - if (!l->thread.threads) - lwan_status_critical("Could not allocate memory for threads"); - - for (short i = 0; i < l->thread.count; i++) - create_thread(l, &l->thread.threads[i]); -} - -void -lwan_thread_shutdown(lwan_t *l) -{ - lwan_status_debug("Shutting down threads"); - - for (int i = l->thread.count - 1; i >= 0; i--) { - lwan_thread_t *t = &l->thread.threads[i]; - char less_than_int = 0; - - lwan_status_debug("Closing epoll for thread %d (fd=%d)", i, - t->epoll_fd); - - /* Close the epoll_fd and write less than an int to signal the - * thread to gracefully finish. */ - close(t->epoll_fd); - write(t->pipe_fd[1], &less_than_int, sizeof(less_than_int)); - } - - for (int i = l->thread.count - 1; i >= 0; i--) { - lwan_thread_t *t = &l->thread.threads[i]; - - lwan_status_debug("Waiting for thread %d to finish", i); - pthread_join(l->thread.threads[i].self, NULL); - - lwan_status_debug("Closing pipe (%d, %d)", t->pipe_fd[0], - t->pipe_fd[1]); - close(t->pipe_fd[0]); - close(t->pipe_fd[1]); - } - - free(l->thread.threads); -} diff --git a/common/lwan-trie.h b/common/lwan-trie.h deleted file mode 100644 index 64d5a8dc0..000000000 --- a/common/lwan-trie.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#pragma once - -#include -#include - -typedef struct lwan_trie_t_ lwan_trie_t; -typedef struct lwan_trie_node_t_ lwan_trie_node_t; -typedef struct lwan_trie_leaf_t_ lwan_trie_leaf_t; - -struct lwan_trie_node_t_ { - lwan_trie_node_t *next[8]; - lwan_trie_leaf_t *leaf; - int ref_count; -}; - -struct lwan_trie_leaf_t_ { - char *key; - void *data; - lwan_trie_leaf_t *next; -}; - -struct lwan_trie_t_ { - lwan_trie_node_t *root; - void (*free_node)(void *data); -}; - -bool lwan_trie_init(lwan_trie_t *trie, void (*free_node)(void *data)); -void lwan_trie_destroy(lwan_trie_t *trie); -void lwan_trie_add(lwan_trie_t *trie, const char *key, void *data); -void *lwan_trie_lookup_full(lwan_trie_t *trie, const char *key, bool prefix); -void *lwan_trie_lookup_prefix(lwan_trie_t *trie, const char *key); -void *lwan_trie_lookup_exact(lwan_trie_t *trie, const char *key); -int32_t lwan_trie_entry_count(lwan_trie_t *trie); - diff --git a/common/lwan.c b/common/lwan.c deleted file mode 100644 index 37f001d2c..000000000 --- a/common/lwan.c +++ /dev/null @@ -1,629 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012, 2013 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-private.h" - -#include "lwan-config.h" -#include "lwan-http-authorize.h" -#include "lwan-redirect.h" -#include "lwan-rewrite.h" -#include "lwan-serve-files.h" - -#if defined(HAVE_LUA) -#include "lwan-lua.h" -#endif - -static const lwan_config_t default_config = { - .listener = "localhost:8080", - .keep_alive_timeout = 15, - .quiet = false, - .reuse_port = false, - .expires = 1 * ONE_WEEK, - .n_threads = 0 -}; - -static void lwan_module_init(lwan_t *l) -{ - if (!l->module_registry) { - lwan_status_debug("Initializing module registry"); - l->module_registry = hash_str_new(NULL, NULL); - } -} - -static void lwan_module_shutdown(lwan_t *l) -{ - hash_free(l->module_registry); -} - -static void *find_handler_symbol(const char *name) -{ - void *symbol = dlsym(RTLD_NEXT, name); - if (!symbol) - symbol = dlsym(RTLD_DEFAULT, name); - return symbol; -} - -static const lwan_module_t *lwan_module_find(lwan_t *l, const char *name) -{ - lwan_module_t *module = hash_find(l->module_registry, name); - if (!module) { - lwan_module_t *(*module_fn)(void); - char module_symbol[128]; - int r; - - for (const char *p = name; *p; p++) { - if (isalnum(*p) || *p == '_') - continue; - - lwan_status_error("Module name (%s) contains invalid character: %c", - name, *p); - return NULL; - } - - r = snprintf(module_symbol, sizeof(module_symbol), - "lwan_module_%s", name); - if (r < 0 || r >= (int)sizeof(module_symbol)) { - lwan_status_error("Module name too long: %s", name); - return NULL; - } - - module_fn = find_handler_symbol(module_symbol); - if (!module_fn) { - lwan_status_error("Module \"%s\" does not exist", name); - return NULL; - } - - module = module_fn(); - if (!module) { - lwan_status_error("Function \"%s()\" didn't return a module", - module_symbol); - return NULL; - } - - lwan_status_debug("Module \"%s\" registered", name); - hash_add(l->module_registry, module->name, module); - } - - return module; -} - -static void destroy_urlmap(void *data) -{ - lwan_url_map_t *url_map = data; - - if (url_map->module) { - const lwan_module_t *module = url_map->module; - if (module->shutdown) - module->shutdown(url_map->data); - } else if (url_map->data) { - hash_free(url_map->data); - } - - free(url_map->authorization.realm); - free(url_map->authorization.password_file); - free((char *)url_map->prefix); - free(url_map); -} - -static lwan_url_map_t *add_url_map(lwan_trie_t *t, const char *prefix, const lwan_url_map_t *map) -{ - lwan_url_map_t *copy = malloc(sizeof(*copy)); - - if (!copy) - lwan_status_critical_perror("Could not copy URL map"); - - memcpy(copy, map, sizeof(*copy)); - - copy->prefix = strdup(prefix ? prefix : copy->prefix); - copy->prefix_len = strlen(copy->prefix); - lwan_trie_add(t, copy->prefix, copy); - - return copy; -} - -static void parse_listener_prefix_authorization(config_t *c, - config_line_t *l, lwan_url_map_t *url_map) -{ - if (strcmp(l->section.param, "basic")) { - config_error(c, "Only basic authorization supported"); - return; - } - - memset(&url_map->authorization, 0, sizeof(url_map->authorization)); - - while (config_read_line(c, l)) { - switch (l->type) { - case CONFIG_LINE_TYPE_LINE: - if (!strcmp(l->line.key, "realm")) { - free(url_map->authorization.realm); - url_map->authorization.realm = strdup(l->line.value); - } else if (!strcmp(l->line.key, "password_file")) { - free(url_map->authorization.password_file); - url_map->authorization.password_file = strdup(l->line.value); - } - break; - - case CONFIG_LINE_TYPE_SECTION: - config_error(c, "Unexpected section: %s", l->section.name); - goto error; - - case CONFIG_LINE_TYPE_SECTION_END: - if (!url_map->authorization.realm) - url_map->authorization.realm = strdup("Lwan"); - if (!url_map->authorization.password_file) - url_map->authorization.password_file = strdup("htpasswd"); - - url_map->flags |= HANDLER_MUST_AUTHORIZE; - goto out; - } - } - -out: - return; - -error: - free(url_map->authorization.realm); - free(url_map->authorization.password_file); -} - -static void parse_listener_prefix(config_t *c, config_line_t *l, lwan_t *lwan, - const lwan_module_t *module) -{ - lwan_url_map_t url_map = {0}; - struct hash *hash = hash_str_new(free, free); - void *handler = NULL; - char *prefix = strdupa(l->line.value); - config_t isolated = {0}; - - if (!config_isolate_section(c, l, &isolated)) { - config_error(c, "Could not isolate configuration file"); - goto out; - } - - while (config_read_line(c, l)) { - switch (l->type) { - case CONFIG_LINE_TYPE_LINE: - if (!strcmp(l->line.key, "module")) { - if (module) { - config_error(c, "Module already specified"); - goto out; - } - module = lwan_module_find(lwan, l->line.value); - if (!module) { - config_error(c, "Could not find module \"%s\"", l->line.value); - goto out; - } - } else if (!strcmp(l->line.key, "handler")) { - handler = find_handler_symbol(l->line.value); - if (!handler) { - config_error(c, "Could not find handler \"%s\"", l->line.value); - goto out; - } - } else { - hash_add(hash, strdup(l->line.key), strdup(l->line.value)); - } - - break; - case CONFIG_LINE_TYPE_SECTION: - if (!strcmp(l->section.name, "authorization")) { - parse_listener_prefix_authorization(c, l, &url_map); - } else { - if (!config_skip_section(c, l)) { - config_error(c, "Could not skip section"); - goto out; - } - } - - break; - case CONFIG_LINE_TYPE_SECTION_END: - goto add_map; - } - } - - config_error(c, "Expecting section end while parsing prefix"); - goto out; - -add_map: - if (module == handler && !handler) { - config_error(c, "Missing module or handler"); - goto out; - } - if (module && handler) { - config_error(c, "Handler and module are mutually exclusive"); - goto out; - } - - if (handler) { - url_map.handler = handler; - url_map.flags |= HANDLER_PARSE_MASK; - url_map.data = hash; - url_map.module = NULL; - - hash = NULL; - } else if (module && module->init_from_hash && module->handle) { - url_map.data = module->init_from_hash(hash); - if (isolated.file && module->parse_conf) { - if (!module->parse_conf(url_map.data, &isolated)) { - config_error(c, "Error from module: %s", - isolated.error_message ? isolated.error_message : "Unknown"); - goto out; - } - } - url_map.handler = module->handle; - url_map.flags |= module->flags; - url_map.module = module; - } else { - config_error(c, "Invalid handler"); - goto out; - } - - add_url_map(&lwan->url_map_trie, prefix, &url_map); - -out: - hash_free(hash); - config_close(&isolated); -} - -void lwan_set_url_map(lwan_t *l, const lwan_url_map_t *map) -{ - lwan_trie_destroy(&l->url_map_trie); - if (UNLIKELY(!lwan_trie_init(&l->url_map_trie, destroy_urlmap))) - lwan_status_critical_perror("Could not initialize trie"); - - for (; map->prefix; map++) { - lwan_url_map_t *copy = add_url_map(&l->url_map_trie, NULL, map); - - if (UNLIKELY(!copy)) - continue; - - if (copy->module && copy->module->init) { - copy->data = copy->module->init(copy->args); - copy->flags = copy->module->flags; - copy->handler = copy->module->handle; - } else { - copy->flags = HANDLER_PARSE_MASK; - } - } -} - -static void parse_listener(config_t *c, config_line_t *l, lwan_t *lwan) -{ - lwan->config.listener = strdup(l->section.param); - - while (config_read_line(c, l)) { - switch (l->type) { - case CONFIG_LINE_TYPE_LINE: - config_error(c, "Expecting prefix section"); - return; - case CONFIG_LINE_TYPE_SECTION: - if (!strcmp(l->section.name, "prefix")) { - parse_listener_prefix(c, l, lwan, NULL); - } else { - const lwan_module_t *module = lwan_module_find(lwan, l->section.name); - if (!module) { - config_error(c, "Invalid section name or module not found: %s", - l->section.name); - } else { - parse_listener_prefix(c, l, lwan, module); - } - } - break; - case CONFIG_LINE_TYPE_SECTION_END: - return; - } - } - - config_error(c, "Expecting section end while parsing listener"); -} - -const char *get_config_path(char *path_buf) -{ - char *path = NULL; - ssize_t path_len; - - /* FIXME: This should ideally (and portably) done by using argv[0] */ - - path_len = readlink("/proc/self/exe", path_buf, PATH_MAX); - if (path_len < 0) - goto out; - path_buf[path_len] = '\0'; - path = strrchr(path_buf, '/'); - if (!path) - goto out; - int ret = snprintf(path_buf, PATH_MAX, "%s.conf", path + 1); - if (ret < 0 || ret >= PATH_MAX) - goto out; - - return path_buf; - -out: - return "lwan.conf"; -} - -static bool setup_from_config(lwan_t *lwan) -{ - config_t conf; - config_line_t line; - bool has_listener = false; - char path_buf[PATH_MAX]; - const char *path; - - path = get_config_path(path_buf); - lwan_status_info("Loading configuration file: %s", path); - - if (!lwan_trie_init(&lwan->url_map_trie, destroy_urlmap)) - return false; - - if (!config_open(&conf, path)) - return false; - - while (config_read_line(&conf, &line)) { - switch (line.type) { - case CONFIG_LINE_TYPE_LINE: - if (!strcmp(line.line.key, "keep_alive_timeout")) - lwan->config.keep_alive_timeout = (unsigned short)parse_long(line.line.value, - default_config.keep_alive_timeout); - else if (!strcmp(line.line.key, "quiet")) - lwan->config.quiet = parse_bool(line.line.value, - default_config.quiet); - else if (!strcmp(line.line.key, "reuse_port")) - lwan->config.reuse_port = parse_bool(line.line.value, - default_config.reuse_port); - else if (!strcmp(line.line.key, "expires")) - lwan->config.expires = parse_time_period(line.line.value, - default_config.expires); - else if (!strcmp(line.line.key, "threads")) { - long n_threads = parse_long(line.line.value, default_config.n_threads); - if (n_threads < 0) - config_error(&conf, "Invalid number of threads: %d", n_threads); - lwan->config.n_threads = (unsigned short int)n_threads; - } - else - config_error(&conf, "Unknown config key: %s", line.line.key); - break; - case CONFIG_LINE_TYPE_SECTION: - if (!has_listener) { - has_listener = true; - if (!strcmp(line.section.name, "listener")) - parse_listener(&conf, &line, lwan); - else - config_error(&conf, "Unknown section type: %s", line.section.name); - } else { - config_error(&conf, "Only one listener supported"); - } - break; - case CONFIG_LINE_TYPE_SECTION_END: - config_error(&conf, "Unexpected section end"); - } - } - - if (conf.error_message) { - lwan_status_critical("Error on config file \"%s\", line %d: %s", - path, conf.line, conf.error_message); - } - - config_close(&conf); - - return true; -} - -static rlim_t -setup_open_file_count_limits(void) -{ - struct rlimit r; - - if (getrlimit(RLIMIT_NOFILE, &r) < 0) - lwan_status_critical_perror("getrlimit"); - - if (r.rlim_max == r.rlim_cur) - return r.rlim_cur; - - if (r.rlim_max == RLIM_INFINITY) - r.rlim_cur *= 8; - else if (r.rlim_cur < r.rlim_max) - r.rlim_cur = r.rlim_max; - - if (setrlimit(RLIMIT_NOFILE, &r) < 0) - lwan_status_critical_perror("setrlimit"); - - return r.rlim_cur; -} - -static void -allocate_connections(lwan_t *l, size_t max_open_files) -{ - l->conns = calloc(max_open_files, sizeof(lwan_connection_t)); - if (!l->conns) - lwan_status_critical_perror("calloc"); -} - -static unsigned short int -get_number_of_cpus(void) -{ - long n_online_cpus = sysconf(_SC_NPROCESSORS_ONLN); - if (UNLIKELY(n_online_cpus < 0)) { - lwan_status_warning("Could not get number of online CPUs, assuming 1 CPU"); - return 1; - } - return (unsigned short int)n_online_cpus; -} - -void -lwan_init(lwan_t *l) -{ - lwan_init_with_config(l, &default_config); -} - -const lwan_config_t * -lwan_get_default_config(void) -{ - return &default_config; -} - -void -lwan_init_with_config(lwan_t *l, const lwan_config_t *config) -{ - /* Load defaults */ - memset(l, 0, sizeof(*l)); - memcpy(&l->config, config, sizeof(*config)); - - /* Initialize status first, as it is used by other things during - * their initialization. */ - lwan_status_init(l); - - /* These will only print debugging messages. Debug messages are always - * printed if we're on a debug build, so the quiet setting will be - * respected. */ - lwan_job_thread_init(); - lwan_response_init(); - lwan_tables_init(); - - lwan_module_init(l); - - /* Load the configuration file. */ - if (config == &default_config) { - if (!setup_from_config(l)) - lwan_status_warning("Could not read config file, using defaults"); - } - - /* Continue initialization as normal. */ - lwan_status_debug("Initializing lwan web server"); - - if (!l->config.n_threads) { - l->thread.count = get_number_of_cpus(); - if (l->thread.count == 1) - l->thread.count = 2; - } else { - l->thread.count = l->config.n_threads; - } - - rlim_t max_open_files = setup_open_file_count_limits(); - allocate_connections(l, (size_t)max_open_files); - - l->thread.max_fd = (unsigned)max_open_files / (unsigned)l->thread.count; - lwan_status_info("Using %d threads, maximum %d sockets per thread", - l->thread.count, l->thread.max_fd); - - signal(SIGPIPE, SIG_IGN); - - lwan_thread_init(l); - lwan_socket_init(l); - lwan_http_authorize_init(); -} - -void -lwan_shutdown(lwan_t *l) -{ - lwan_status_info("Shutting down"); - - if (l->config.listener != default_config.listener) - free(l->config.listener); - - lwan_job_thread_shutdown(); - lwan_thread_shutdown(l); - - lwan_status_debug("Shutting down URL handlers"); - lwan_trie_destroy(&l->url_map_trie); - - free(l->conns); - - lwan_response_shutdown(); - lwan_tables_shutdown(); - lwan_status_shutdown(l); - lwan_http_authorize_shutdown(); - lwan_module_shutdown(l); -} - -static ALWAYS_INLINE void -schedule_client(lwan_t *l, int fd) -{ - int thread; -#ifdef __x86_64__ - static_assert(sizeof(lwan_connection_t) == 32, - "Two connections per cache line"); - /* Since lwan_connection_t is guaranteed to be 32-byte long, two of them - * can fill up a cache line. This formula will group two connections - * per thread in a way that false-sharing is avoided. This gives wrong - * results when fd=0, but this shouldn't happen (as 0 is either the - * standard input or the main socket, but even if that changes, - * scheduling will still work). */ - thread = ((fd - 1) / 2) % l->thread.count; -#else - static int counter = 0; - thread = counter++ % l->thread.count; -#endif - lwan_thread_t *t = &l->thread.threads[thread]; - lwan_thread_add_client(t, fd); -} - -static volatile sig_atomic_t main_socket = -1; - -static void -sigint_handler(int signal_number __attribute__((unused))) -{ - if (main_socket < 0) - return; - close(main_socket); - main_socket = -1; -} - -void -lwan_main_loop(lwan_t *l) -{ - assert(main_socket == -1); - main_socket = l->main_socket; - if (signal(SIGINT, sigint_handler) == SIG_ERR) - lwan_status_critical("Could not set signal handler"); - - lwan_status_info("Ready to serve"); - - for (;;) { - int client_fd = accept4(main_socket, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); - if (UNLIKELY(client_fd < 0)) { - if (errno != EBADF) { - lwan_status_perror("accept"); - continue; - } - - if (main_socket < 0) { - lwan_status_info("Signal 2 (Interrupt) received"); - } else { - lwan_status_info("Main socket closed for unknown reasons"); - } - - break; - } - - schedule_client(l, client_fd); - } -} diff --git a/common/lwan.h b/common/lwan.h deleted file mode 100644 index 19236b494..000000000 --- a/common/lwan.h +++ /dev/null @@ -1,343 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#pragma once - -#if defined (__cplusplus) -extern "C" { -#endif - -#include -#include -#include -#include -#include - -#include "hash.h" -#include "lwan-config.h" -#include "lwan-coro.h" -#include "lwan-status.h" -#include "lwan-trie.h" -#include "strbuf.h" - -#define DEFAULT_BUFFER_SIZE 4096 -#define DEFAULT_HEADERS_SIZE 512 - -#define N_ELEMENTS(array) (sizeof(array) / sizeof(array[0])) - -#ifdef DISABLE_INLINE_FUNCTIONS -# define ALWAYS_INLINE -#else -# define ALWAYS_INLINE inline __attribute__((always_inline)) -#endif - -#ifdef DISABLE_BRANCH_PREDICTION -# define LIKELY_IS(x,y) (x) -#else -# define LIKELY_IS(x,y) __builtin_expect((x), (y)) -#endif - -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ -# define MULTICHAR_CONSTANT(a,b,c,d) ((int32_t)((a) | (b) << 8 | (c) << 16 | (d) << 24)) -# define MULTICHAR_CONSTANT_SMALL(a,b) ((int16_t)((a) | (b) << 8)) -#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -# define MULTICHAR_CONSTANT(d,c,b,a) ((int32_t)((a) | (b) << 8 | (c) << 16 | (d) << 24)) -# define MULTICHAR_CONSTANT_SMALL(b,a) ((int16_t)((a) | (b) << 8)) -#elif __BYTE_ORDER__ == __ORDER_PDP_ENDIAN__ -# error A PDP? Seriously? -#endif - -#define MULTICHAR_CONSTANT_L(a,b,c,d) (MULTICHAR_CONSTANT(a,b,c,d) | 0x20202020) -#define MULTICHAR_CONSTANT_SMALL_L(a,b) (MULTICHAR_CONSTANT_SMALL(a,b) | 0x2020) - -static ALWAYS_INLINE int32_t string_as_int32(const char *s) -{ - int32_t i; - memcpy(&i, s, sizeof(int32_t)); - return i; -} - -static ALWAYS_INLINE int16_t string_as_int16(const char *s) -{ - int16_t i; - memcpy(&i, s, sizeof(int16_t)); - return i; -} - -#define STRING_SWITCH(s) switch (string_as_int32(s)) -#define STRING_SWITCH_L(s) switch (string_as_int32(s) | 0x20202020) - -#define STRING_SWITCH_SMALL(s) switch (string_as_int16(s)) -#define STRING_SWITCH_SMALL_L(s) switch (string_as_int16(s) | 0x2020) - -#define LIKELY(x) LIKELY_IS(!!(x), 1) -#define UNLIKELY(x) LIKELY_IS((x), 0) - -#define ATOMIC_READ(V) (*(volatile typeof(V) *)&(V)) -#define ATOMIC_AAF(P, V) (__sync_add_and_fetch((P), (V))) -#define ATOMIC_INC(V) ATOMIC_AAF(&(V), 1) -#define ATOMIC_DEC(V) ATOMIC_AAF(&(V), -1) -#define ATOMIC_BITWISE(P, O, V) (__sync_##O##_and_fetch((P), (V))) - -#if defined (__cplusplus) -#define ENFORCE_STATIC_BUFFER_LENGTH -#else -#define ENFORCE_STATIC_BUFFER_LENGTH static -#endif - -typedef struct lwan_t_ lwan_t; -typedef struct lwan_module_t_ lwan_module_t; -typedef struct lwan_key_value_t_ lwan_key_value_t; -typedef struct lwan_request_t_ lwan_request_t; -typedef struct lwan_response_t_ lwan_response_t; -typedef struct lwan_thread_t_ lwan_thread_t; -typedef struct lwan_url_map_t_ lwan_url_map_t; -typedef struct lwan_value_t_ lwan_value_t; -typedef struct lwan_config_t_ lwan_config_t; -typedef struct lwan_connection_t_ lwan_connection_t; - -typedef enum { - HTTP_OK = 200, - HTTP_PARTIAL_CONTENT = 206, - HTTP_MOVED_PERMANENTLY = 301, - HTTP_NOT_MODIFIED = 304, - HTTP_BAD_REQUEST = 400, - HTTP_NOT_AUTHORIZED = 401, - HTTP_FORBIDDEN = 403, - HTTP_NOT_FOUND = 404, - HTTP_NOT_ALLOWED = 405, - HTTP_TIMEOUT = 408, - HTTP_TOO_LARGE = 413, - HTTP_RANGE_UNSATISFIABLE = 416, - HTTP_I_AM_A_TEAPOT = 418, - HTTP_INTERNAL_ERROR = 500, - HTTP_NOT_IMPLEMENTED = 501, - HTTP_UNAVAILABLE = 503, -} lwan_http_status_t; - -typedef enum { - HANDLER_PARSE_QUERY_STRING = 1<<0, - HANDLER_PARSE_IF_MODIFIED_SINCE = 1<<1, - HANDLER_PARSE_RANGE = 1<<2, - HANDLER_PARSE_ACCEPT_ENCODING = 1<<3, - HANDLER_PARSE_POST_DATA = 1<<4, - HANDLER_MUST_AUTHORIZE = 1<<5, - HANDLER_REMOVE_LEADING_SLASH = 1<<6, - HANDLER_CAN_REWRITE_URL = 1<<7, - HANDLER_PARSE_COOKIES = 1<<8, - - HANDLER_PARSE_MASK = 1<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<8 -} lwan_handler_flags_t; - -typedef enum { - REQUEST_ALL_FLAGS = -1, - REQUEST_METHOD_GET = 1<<0, - REQUEST_METHOD_HEAD = 1<<1, - REQUEST_METHOD_POST = 1<<2, - REQUEST_ACCEPT_DEFLATE = 1<<3, - REQUEST_ACCEPT_GZIP = 1<<4, - REQUEST_IS_HTTP_1_0 = 1<<5, - RESPONSE_SENT_HEADERS = 1<<6, - RESPONSE_CHUNKED_ENCODING = 1<<7, - RESPONSE_NO_CONTENT_LENGTH = 1<<8, - RESPONSE_URL_REWRITTEN = 1<<9 -} lwan_request_flags_t; - -typedef enum { - CONN_MASK = -1, - CONN_KEEP_ALIVE = 1<<0, - CONN_IS_ALIVE = 1<<1, - CONN_SHOULD_RESUME_CORO = 1<<2, - CONN_WRITE_EVENTS = 1<<3, - CONN_MUST_READ = 1<<4 -} lwan_connection_flags_t; - -typedef enum { - CONN_CORO_ABORT = -1, - CONN_CORO_MAY_RESUME = 0, - CONN_CORO_FINISHED = 1 -} lwan_connection_coro_yield_t; - -struct lwan_key_value_t_ { - char *key; - char *value; -}; - -struct lwan_response_t_ { - strbuf_t *buffer; - const char *mime_type; - size_t content_length; - lwan_key_value_t *headers; - - struct { - lwan_http_status_t (*callback)(lwan_request_t *request, void *data); - void *data; - void *priv; - } stream; -}; - -struct lwan_value_t_ { - char *value; - size_t len; -}; - -struct lwan_connection_t_ { - /* This structure is exactly 32-bytes on x86-64. If it is changed, - * make sure the scheduler (lwan.c) is updated as well. */ - lwan_connection_flags_t flags; - unsigned int time_to_die; - coro_t *coro; - lwan_thread_t *thread; - int prev, next; /* for death queue */ -}; - -struct lwan_request_t_ { - lwan_request_flags_t flags; - int fd; - lwan_value_t url; - lwan_value_t original_url; - lwan_connection_t *conn; - - struct { - lwan_key_value_t *base; - size_t len; - } query_params, post_data, cookies; - struct { - time_t if_modified_since; - struct { - off_t from; - off_t to; - } range; - } header; - lwan_response_t response; -}; - -struct lwan_module_t_ { - const char *name; - void *(*init)(void *args); - void *(*init_from_hash)(const struct hash *hash); - void (*shutdown)(void *data); - bool (*parse_conf)(void *data, config_t *config); - lwan_http_status_t (*handle)(lwan_request_t *request, lwan_response_t *response, void *data); - lwan_handler_flags_t flags; -}; - -struct lwan_url_map_t_ { - lwan_http_status_t (*handler)(lwan_request_t *request, lwan_response_t *response, void *data); - void *data; - - const char *prefix; - size_t prefix_len; - lwan_handler_flags_t flags; - - const lwan_module_t *module; - void *args; - - struct { - char *realm; - char *password_file; - } authorization; -}; - -struct lwan_thread_t_ { - lwan_t *lwan; - struct { - char date[30]; - char expires[30]; - time_t last; - } date; - - int epoll_fd; - int pipe_fd[2]; - pthread_t self; -}; - -struct lwan_config_t_ { - char *listener; - unsigned short keep_alive_timeout; - bool quiet; - bool reuse_port; - unsigned int expires; - short unsigned int n_threads; -}; - -struct lwan_t_ { - lwan_trie_t url_map_trie; - lwan_connection_t *conns; - - struct { - lwan_thread_t *threads; - unsigned int max_fd; - unsigned short count; - } thread; - - struct hash *module_registry; - lwan_config_t config; - int main_socket; -}; - -void lwan_set_url_map(lwan_t *l, const lwan_url_map_t *map); -void lwan_main_loop(lwan_t *l); - -void lwan_response(lwan_request_t *request, lwan_http_status_t status); -void lwan_default_response(lwan_request_t *request, lwan_http_status_t status); -size_t lwan_prepare_response_header(lwan_request_t *request, lwan_http_status_t status, char header_buffer[], size_t header_buffer_size) - __attribute__((warn_unused_result)); - -const char *lwan_request_get_post_param(lwan_request_t *request, const char *key) - __attribute__((warn_unused_result)); -const char *lwan_request_get_query_param(lwan_request_t *request, const char *key) - __attribute__((warn_unused_result)); -const char * lwan_request_get_cookie(lwan_request_t *request, const char *key) - __attribute__((warn_unused_result)); - -bool lwan_response_set_chunked(lwan_request_t *request, lwan_http_status_t status); -void lwan_response_send_chunk(lwan_request_t *request); - -bool lwan_response_set_event_stream(lwan_request_t *request, lwan_http_status_t status); -void lwan_response_send_event(lwan_request_t *request, const char *event); - -const char *lwan_http_status_as_string(lwan_http_status_t status) - __attribute__((pure)) __attribute__((warn_unused_result)); -const char *lwan_http_status_as_string_with_code(lwan_http_status_t status) - __attribute__((pure)) __attribute__((warn_unused_result)); -const char *lwan_http_status_as_descriptive_string(lwan_http_status_t status) - __attribute__((pure)) __attribute__((warn_unused_result)); -const char *lwan_determine_mime_type_for_file_name(const char *file_name) - __attribute__((pure)) __attribute__((warn_unused_result)); - -void lwan_init(lwan_t *l); -void lwan_init_with_config(lwan_t *l, const lwan_config_t *config); -void lwan_shutdown(lwan_t *l); - -const lwan_config_t *lwan_get_default_config(void); - -int lwan_connection_get_fd(const lwan_t *lwan, const lwan_connection_t *conn) - __attribute__((pure)) __attribute__((warn_unused_result)); - - -const char *lwan_request_get_remote_address(lwan_request_t *request, - char buffer[ENFORCE_STATIC_BUFFER_LENGTH INET6_ADDRSTRLEN]) - __attribute__((warn_unused_result)); - -void lwan_format_rfc_time(time_t t, char buffer[ENFORCE_STATIC_BUFFER_LENGTH 30]); - -#if defined (__cplusplus) -} -#endif diff --git a/common/murmur3.c b/common/murmur3.c deleted file mode 100644 index ac45322b6..000000000 --- a/common/murmur3.c +++ /dev/null @@ -1,346 +0,0 @@ -//----------------------------------------------------------------------------- -// MurmurHash3 was written by Austin Appleby, and is placed in the public -// domain. The author hereby disclaims copyright to this source code. - -// Note - The x86 and x64 versions do _not_ produce the same results, as the -// algorithms are optimized for their respective platforms. You can still -// compile and run any of them on any platform, but your performance with the -// non-native version will be less than optimal. - -#include "murmur3.h" -#include -#include - -//----------------------------------------------------------------------------- -// Platform-specific functions and macros - -#ifdef __GNUC__ -#define FORCE_INLINE __attribute__((always_inline)) inline -#else /* */ -#define FORCE_INLINE -#endif /* */ - -#ifndef __x86_64__ -static FORCE_INLINE uint32_t rotl32(uint32_t x, int8_t r) -{ - return (x << r) | (x >> (32 - r)); -} -#endif - -static FORCE_INLINE uint64_t rotl64(uint64_t x, int8_t r) -{ - return (x << r) | (x >> (64 - r)); -} - -#define ROTL32(x,y) rotl32(x,y) -#define ROTL64(x,y) rotl64(x,y) - -#define BIG_CONSTANT(x) (x##LLU) - -static uint32_t seed_value = 0xdeadbeef; - -//----------------------------------------------------------------------------- -// Finalization mix - force all bits of a hash block to avalanche -#ifndef __x86_64__ -static FORCE_INLINE uint32_t fmix32(uint32_t h) -{ - h ^= h >> 16; - h *= 0x85ebca6b; - h ^= h >> 13; - h *= 0xc2b2ae35; - h ^= h >> 16; - return h; -} -#endif - -//---------- -static FORCE_INLINE uint64_t fmix64(uint64_t k) -{ - k ^= k >> 33; - k *= BIG_CONSTANT(0xff51afd7ed558ccd); - k ^= k >> 33; - k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); - k ^= k >> 33; - return k; -} - - -//----------------------------------------------------------------------------- -#ifndef __x86_64__ -FORCE_INLINE static void -MurmurHash3_x86_32(const void *key, size_t len, uint32_t seed, void *out) -{ - const uint8_t *data = (const uint8_t *)key; - const size_t nblocks = len / 4; - size_t i; - uint32_t h1 = seed; - uint32_t c1 = 0xcc9e2d51; - uint32_t c2 = 0x1b873593; - - //---------- - // body - const uint32_t *blocks = (const uint32_t *)(data + nblocks * 4); - for (i = -nblocks; i; i++) { - uint32_t k1; - - memcpy(&k1, blocks + i, sizeof(k1)); - - k1 *= c1; - k1 = ROTL32(k1, 15); - k1 *= c2; - h1 ^= k1; - h1 = ROTL32(h1, 13); - h1 = h1 * 5 + 0xe6546b64; - } - - //---------- - // tail - const uint8_t *tail = (const uint8_t *)(data + nblocks * 4); - uint32_t k1 = 0; - switch (len & 3) { - case 3: - k1 ^= (uint32_t)tail[2] << 16; - case 2: - k1 ^= (uint32_t)tail[1] << 8; - case 1: - k1 ^= (uint32_t)tail[0]; - k1 *= c1; - k1 = ROTL32(k1, 15); - k1 *= c2; - h1 ^= k1; - }; - - //---------- - // finalization - h1 ^= (uint32_t)len; - h1 = fmix32(h1); - *(uint32_t *) out = h1; -} - - -//----------------------------------------------------------------------------- -FORCE_INLINE static void MurmurHash3_x86_128 (const void *key, const size_t len, - uint32_t seed, void *out) -{ - size_t i; - const uint8_t * data = (const uint8_t*)key; - const size_t nblocks = len / 16; - - uint32_t h1 = seed; - uint32_t h2 = seed; - uint32_t h3 = seed; - uint32_t h4 = seed; - - const uint32_t c1 = 0x239b961b; - const uint32_t c2 = 0xab0e9789; - const uint32_t c3 = 0x38b34ae5; - const uint32_t c4 = 0xa1e38b93; - - //---------- - // body - - const uint32_t * blocks = (const uint32_t *)(data + nblocks*16); - - for(i = -nblocks; i; i++) { - uint32_t k1, k2, k3, k4; - - memcpy(&k1, blocks + i * 4 + 0, sizeof(k1)); - memcpy(&k2, blocks + i * 4 + 1, sizeof(k2)); - memcpy(&k3, blocks + i * 4 + 2, sizeof(k3)); - memcpy(&k4, blocks + i * 4 + 3, sizeof(k4)); - - k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; - h1 = ROTL32(h1,19); h1 += h2; h1 = h1*5+0x561ccd1b; - k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; - h2 = ROTL32(h2,17); h2 += h3; h2 = h2*5+0x0bcaa747; - k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; - h3 = ROTL32(h3,15); h3 += h4; h3 = h3*5+0x96cd1c35; - k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; - h4 = ROTL32(h4,13); h4 += h1; h4 = h4*5+0x32ac3b17; - } - - //---------- - // tail - - const uint8_t * tail = (const uint8_t*)(data + nblocks*16); - - uint32_t k1 = 0; - uint32_t k2 = 0; - uint32_t k3 = 0; - uint32_t k4 = 0; - - switch(len & 15) { - case 15: k4 ^= (uint32_t)tail[14] << 16; - case 14: k4 ^= (uint32_t)tail[13] << 8; - case 13: k4 ^= (uint32_t)tail[12] << 0; - k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; - - case 12: k3 ^= (uint32_t)tail[11] << 24; - case 11: k3 ^= (uint32_t)tail[10] << 16; - case 10: k3 ^= (uint32_t)tail[ 9] << 8; - case 9: k3 ^= (uint32_t)tail[ 8] << 0; - k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; - - case 8: k2 ^= (uint32_t)tail[ 7] << 24; - case 7: k2 ^= (uint32_t)tail[ 6] << 16; - case 6: k2 ^= (uint32_t)tail[ 5] << 8; - case 5: k2 ^= (uint32_t)tail[ 4] << 0; - k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; - - case 4: k1 ^= (uint32_t)tail[ 3] << 24; - case 3: k1 ^= (uint32_t)tail[ 2] << 16; - case 2: k1 ^= (uint32_t)tail[ 1] << 8; - case 1: k1 ^= (uint32_t)tail[ 0] << 0; - k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; - } - - //---------- - // finalization - - h1 ^= (uint32_t)len; h2 ^= (uint32_t)len; - h3 ^= (uint32_t)len; h4 ^= (uint32_t)len; - - h1 += h2; h1 += h3; h1 += h4; - h2 += h1; h3 += h1; h4 += h1; - - h1 = fmix32(h1); - h2 = fmix32(h2); - h3 = fmix32(h3); - h4 = fmix32(h4); - - h1 += h2; h1 += h3; h1 += h4; - h2 += h1; h3 += h1; h4 += h1; - - ((uint32_t*)out)[0] = h1; - ((uint32_t*)out)[1] = h2; - ((uint32_t*)out)[2] = h3; - ((uint32_t*)out)[3] = h4; -} -#endif - -//----------------------------------------------------------------------------- -FORCE_INLINE static void -MurmurHash3_x64_128(const void *key, const size_t len, const uint32_t seed, - void *out) -{ - const uint8_t *data = (const uint8_t *)key; - const size_t nblocks = len / 16; - size_t i; - uint64_t h1 = seed; - uint64_t h2 = seed; - uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); - uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f); - - //---------- - // body - const uint64_t *blocks = (const uint64_t *)(data); - for (i = 0; i < nblocks; i++) { - uint64_t k1, k2; - - memcpy(&k1, blocks + i * 2 + 0, sizeof(k1)); - memcpy(&k2, blocks + i * 2 + 1, sizeof(k2)); - - k1 *= c1; - k1 = ROTL64(k1, 31); - k1 *= c2; - h1 ^= k1; - h1 = ROTL64(h1, 27); - h1 += h2; - h1 = h1 * 5 + 0x52dce729; - k2 *= c2; - k2 = ROTL64(k2, 33); - k2 *= c1; - h2 ^= k2; - h2 = ROTL64(h2, 31); - h2 += h1; - h2 = h2 * 5 + 0x38495ab5; - } - - //---------- - // tail - const uint8_t *tail = (const uint8_t *)(data + nblocks * 16); - uint64_t k1 = 0; - uint64_t k2 = 0; - switch (len & 15) { - case 15: - k2 ^= (uint64_t) (tail[14]) << 48; - case 14: - k2 ^= (uint64_t) (tail[13]) << 40; - case 13: - k2 ^= (uint64_t) (tail[12]) << 32; - case 12: - k2 ^= (uint64_t) (tail[11]) << 24; - case 11: - k2 ^= (uint64_t) (tail[10]) << 16; - case 10: - k2 ^= (uint64_t) (tail[9]) << 8; - case 9: - k2 ^= (uint64_t) (tail[8]) << 0; - k2 *= c2; - k2 = ROTL64(k2, 33); - k2 *= c1; - h2 ^= k2; - case 8: - k1 ^= (uint64_t) (tail[7]) << 56; - case 7: - k1 ^= (uint64_t) (tail[6]) << 48; - case 6: - k1 ^= (uint64_t) (tail[5]) << 40; - case 5: - k1 ^= (uint64_t) (tail[4]) << 32; - case 4: - k1 ^= (uint64_t) (tail[3]) << 24; - case 3: - k1 ^= (uint64_t) (tail[2]) << 16; - case 2: - k1 ^= (uint64_t) (tail[1]) << 8; - case 1: - k1 ^= (uint64_t) (tail[0]) << 0; - k1 *= c1; - k1 = ROTL64(k1, 31); - k1 *= c2; - h1 ^= k1; - }; - - //---------- - // finalization - h1 ^= (uint64_t)len; - h2 ^= (uint64_t)len; - h1 += h2; - h2 += h1; - h1 = fmix64(h1); - h2 = fmix64(h2); - h1 += h2; - h2 += h1; - ((uint64_t *) out)[0] = h1; - ((uint64_t *) out)[1] = h2; -} - - -//----------------------------------------------------------------------------- -unsigned int -murmur3_simple(const void *keyptr) -{ - size_t len = strlen((char *)keyptr); -#ifdef __x86_64__ - uint64_t hash[2]; - MurmurHash3_x64_128(keyptr, len, seed_value, hash); - return (unsigned int)hash[1]; -#else - if (len <= 16) { - unsigned int hash; - MurmurHash3_x86_32(keyptr, len, seed_value, &hash); - return hash; - } - - unsigned int hash[4]; - MurmurHash3_x86_128(keyptr, len, seed_value, hash); - return hash[3]; -#endif -} - -void -murmur3_set_seed(const uint32_t seed) -{ - seed_value = seed; -} diff --git a/common/murmur3.h b/common/murmur3.h deleted file mode 100644 index dd1d3dca6..000000000 --- a/common/murmur3.h +++ /dev/null @@ -1,15 +0,0 @@ -//----------------------------------------------------------------------------- -// MurmurHash3 was written by Austin Appleby, and is placed in the -// public domain. The author hereby disclaims copyright to this source -// code. - -#pragma once - -#include - -//----------------------------------------------------------------------------- - -void murmur3_set_seed(const uint32_t seed); -unsigned int murmur3_simple(const void *key); - -//----------------------------------------------------------------------------- diff --git a/common/reallocarray.c b/common/reallocarray.c deleted file mode 100644 index 5ff26694b..000000000 --- a/common/reallocarray.c +++ /dev/null @@ -1,53 +0,0 @@ -/* $OpenBSD: reallocarray.c,v 1.2 2014/12/08 03:45:00 bcook Exp $ */ -/* - * Copyright (c) 2008 Otto Moerbeek - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include -#include -#include -#include -#include - -#include "lwan.h" - -#if !defined(HAVE_BUILTIN_MUL_OVERFLOW) -/* - * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX - * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW - */ -#define MUL_NO_OVERFLOW ((size_t)1 << (sizeof(size_t) * 4)) - -static inline bool umull_overflow(size_t a, size_t b, size_t *out) -{ - if ((a >= MUL_NO_OVERFLOW || b >= MUL_NO_OVERFLOW) && a > 0 && SIZE_MAX / a < b) - return true; - *out = a * b; - return false; -} -#else -#define umull_overflow __builtin_mul_overflow -#endif - -void * -reallocarray(void *optr, size_t nmemb, size_t size) -{ - size_t total_size; - if (UNLIKELY(umull_overflow(nmemb, size, &total_size))) { - errno = ENOMEM; - return NULL; - } - return realloc(optr, total_size); -} diff --git a/common/reallocarray.h b/common/reallocarray.h deleted file mode 100644 index 5c54dea6d..000000000 --- a/common/reallocarray.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -void *reallocarray(void *optr, size_t nmemb, size_t size); diff --git a/common/strbuf.c b/common/strbuf.c deleted file mode 100644 index 4b90a51cc..000000000 --- a/common/strbuf.c +++ /dev/null @@ -1,319 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include -#include - -#include "lwan.h" -#include "strbuf.h" - -static const unsigned int STATIC = 1<<0; -static const unsigned int DYNAMICALLY_ALLOCATED = 1<<1; -static const size_t DEFAULT_BUF_SIZE = 64; - -static size_t -find_next_power_of_two(size_t number) -{ -#if HAVE_BUILTIN_CLZLL - static const int size_bits = (int)sizeof(number) * CHAR_BIT; - - if (sizeof(size_t) == sizeof(unsigned int)) { - return 1U << (size_bits - __builtin_clz((unsigned int)number)); - } else if (sizeof(size_t) == sizeof(unsigned long)) { - return 1UL << (size_bits - __builtin_clzl((unsigned long)number)); - } else if (sizeof(size_t) == sizeof(unsigned long long)) { - return 1ULL << (size_bits - __builtin_clzll((unsigned long long)number)); - } else { - __builtin_unreachable(); - } -#else - number--; - number |= number >> 1; - number |= number >> 2; - number |= number >> 4; - number |= number >> 8; - number |= number >> 16; - return number + 1; -#endif -} - -static ALWAYS_INLINE size_t -max(size_t one, size_t another) -{ - return (one > another) ? one : another; -} - -static bool -grow_buffer_if_needed(strbuf_t *s, size_t size) -{ - if (s->flags & STATIC) { - const size_t next_power = find_next_power_of_two(max(size + 1, - s->len.buffer)); - char *buffer = malloc(next_power); - if (!buffer) - return false; - - memcpy(buffer, s->value.static_buffer, s->len.buffer); - buffer[s->len.buffer + 1] = '\0'; - - s->flags &= ~STATIC; - s->len.allocated = next_power; - s->value.buffer = buffer; - - return true; - } - - if (UNLIKELY(s->len.allocated < size)) { - const size_t next_power = find_next_power_of_two(size); - char *buffer = realloc(s->value.buffer, next_power + 1); - if (UNLIKELY(!buffer)) - return false; - s->len.allocated = next_power; - s->value.buffer = buffer; - } - - return true; -} - -bool -strbuf_init_with_size(strbuf_t *s, size_t size) -{ - if (UNLIKELY(!s)) - return false; - - memset(s, 0, sizeof(*s)); - - if (UNLIKELY(!grow_buffer_if_needed(s, size))) - return false; - - s->len.buffer = 0; - s->value.buffer[0] = '\0'; - - return true; -} - -ALWAYS_INLINE bool -strbuf_init(strbuf_t *s) -{ - return strbuf_init_with_size(s, DEFAULT_BUF_SIZE); -} - -strbuf_t * -strbuf_new_with_size(size_t size) -{ - strbuf_t *s = malloc(sizeof(*s)); - if (UNLIKELY(!strbuf_init_with_size(s, size))) { - free(s); - s = NULL; - } else { - s->flags |= DYNAMICALLY_ALLOCATED; - } - return s; -} - -ALWAYS_INLINE strbuf_t * -strbuf_new(void) -{ - return strbuf_new_with_size(DEFAULT_BUF_SIZE); -} - -void -strbuf_free(strbuf_t *s) -{ - if (UNLIKELY(!s)) - return; - if (!(s->flags & STATIC)) - free(s->value.buffer); - if (s->flags & DYNAMICALLY_ALLOCATED) - free(s); -} - -bool -strbuf_append_char(strbuf_t *s, const char c) -{ - if (UNLIKELY(!grow_buffer_if_needed(s, s->len.buffer + 2))) - return false; - - *(s->value.buffer + s->len.buffer++) = c; - *(s->value.buffer + s->len.buffer) = '\0'; - - return true; -} - -bool -strbuf_append_str(strbuf_t *s1, const char *s2, size_t sz) -{ - if (!sz) - sz = strlen(s2); - - if (UNLIKELY(!grow_buffer_if_needed(s1, s1->len.buffer + sz + 2))) - return false; - - memcpy(s1->value.buffer + s1->len.buffer, s2, sz); - s1->len.buffer += sz; - s1->value.buffer[s1->len.buffer] = '\0'; - - return true; -} - -bool -strbuf_set_static(strbuf_t *s1, const char *s2, size_t sz) -{ - if (!sz) - sz = strlen(s2); - - if (!(s1->flags & STATIC)) - free(s1->value.buffer); - s1->value.static_buffer = s2; - s1->len.allocated = s1->len.buffer = sz; - s1->flags |= STATIC; - - return true; -} - -bool -strbuf_set(strbuf_t *s1, const char *s2, size_t sz) -{ - if (!sz) - sz = strlen(s2); - - if (UNLIKELY(!grow_buffer_if_needed(s1, sz + 1))) - return false; - - memcpy(s1->value.buffer, s2, sz); - s1->len.buffer = sz; - s1->value.buffer[sz] = '\0'; - - return true; -} - -ALWAYS_INLINE int -strbuf_cmp(strbuf_t *s1, strbuf_t *s2) -{ - if (s1 == s2) - return 0; - int result = memcmp(s1->value.buffer, s2->value.buffer, s1->len.buffer < s2->len.buffer ? s1->len.buffer : s2->len.buffer); - if (!result) - return (int)(s1->len.buffer - s2->len.buffer); - return result; -} - -static ALWAYS_INLINE bool -internal_printf(strbuf_t *s1, bool (*save_str)(strbuf_t *, const char *, size_t), const char *fmt, va_list values) -{ - char *s2; - int len; - - if (UNLIKELY((len = vasprintf(&s2, fmt, values)) < 0)) - return false; - - bool success = save_str(s1, s2, (size_t)len); - free(s2); - - return success; -} - -bool -strbuf_printf(strbuf_t *s, const char *fmt, ...) -{ - bool could_printf; - va_list values; - - va_start(values, fmt); - could_printf = internal_printf(s, strbuf_set, fmt, values); - va_end(values); - - return could_printf; -} - -bool -strbuf_append_printf(strbuf_t *s, const char *fmt, ...) -{ - bool could_printf; - va_list values; - - va_start(values, fmt); - could_printf = internal_printf(s, strbuf_append_str, fmt, values); - va_end(values); - - return could_printf; -} - -bool -strbuf_shrink_to(strbuf_t *s, size_t new_size) -{ - if (s->len.allocated <= new_size) - return true; - - if (s->flags & STATIC) - return true; - - size_t next_power_of_two = find_next_power_of_two(new_size); - char *buffer = realloc(s->value.buffer, next_power_of_two + 1); - if (UNLIKELY(!buffer)) - return false; - - s->value.buffer = buffer; - s->len.allocated = next_power_of_two; - if (s->len.buffer > next_power_of_two) { - s->len.buffer = next_power_of_two - 1; - s->value.buffer[s->len.buffer + 1] = '\0'; - } - - return true; -} - -ALWAYS_INLINE bool -strbuf_shrink_to_default(strbuf_t *s) -{ - return strbuf_shrink_to(s, DEFAULT_BUF_SIZE); -} - -ALWAYS_INLINE bool -strbuf_reset(strbuf_t *s) -{ - strbuf_shrink_to_default(s); - s->len.buffer = 0; - return false; -} - -bool -strbuf_grow_to(strbuf_t *s, size_t new_size) -{ - return grow_buffer_if_needed(s, new_size + 1); -} - -bool -strbuf_reset_length(strbuf_t *s) -{ - if (s->flags & STATIC) { - s->flags &= ~STATIC; - s->value.buffer = malloc(s->len.allocated); - if (UNLIKELY(!s->value.buffer)) - return false; - } - - s->len.buffer = 0; - s->value.buffer[0] = '\0'; - - return true; -} diff --git a/common/strbuf.h b/common/strbuf.h deleted file mode 100644 index 99a47a0a6..000000000 --- a/common/strbuf.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#pragma once - -#include -#include - -typedef struct strbuf_t_ strbuf_t; - -struct strbuf_t_ { - union { - char *buffer; - const char *static_buffer; - } value; - struct { - size_t allocated, buffer; - } len; - unsigned int flags; -}; - -bool strbuf_init_with_size(strbuf_t *buf, size_t size); -bool strbuf_init(strbuf_t *buf); -strbuf_t *strbuf_new_with_size(size_t size); -strbuf_t *strbuf_new(void); -void strbuf_free(strbuf_t *s); -bool strbuf_append_char(strbuf_t *s, const char c); -bool strbuf_append_str(strbuf_t *s1, const char *s2, size_t sz); -bool strbuf_set_static(strbuf_t *s1, const char *s2, size_t sz); -bool strbuf_set(strbuf_t *s1, const char *s2, size_t sz); -int strbuf_cmp(strbuf_t *s1, strbuf_t *s2); -bool strbuf_append_printf(strbuf_t *s, const char *fmt, ...); -bool strbuf_printf(strbuf_t *s1, const char *fmt, ...); -bool strbuf_shrink_to(strbuf_t *s, size_t new_size); -bool strbuf_shrink_to_default(strbuf_t *s); -bool strbuf_grow_to(strbuf_t *s, size_t new_size); -bool strbuf_reset(strbuf_t *s); -bool strbuf_reset_length(strbuf_t *s); - -#define strbuf_get_length(s) (((strbuf_t *)(s))->len.buffer) -#define strbuf_get_buffer(s) (((strbuf_t *)(s))->value.buffer) - diff --git a/freegeoip/freegeoip.c b/freegeoip/freegeoip.c deleted file mode 100644 index 9c86ec9f7..000000000 --- a/freegeoip/freegeoip.c +++ /dev/null @@ -1,449 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * SQL query is copied from freegeoip.go - * Copyright (c) 2013 Alexandre Fiori - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _DEFAULT_SOURCE -#include -#include -#include -#include - -#include "lwan.h" -#include "lwan-cache.h" -#include "lwan-serve-files.h" -#include "lwan-template.h" - -/* Set to 0 to disable */ -#define QUERIES_PER_HOUR 10000 - -struct ip_info_t { - struct cache_entry_t base; - struct { - char *code; - char *name; - } country, region; - struct { - char *name; - char *zip_code; - } city; - double latitude, longitude; - struct { - char *code, *area; - } metro; - char *ip; - const char *callback; -}; - -static const lwan_var_descriptor_t template_descriptor[] = { - TPL_VAR_STR(struct ip_info_t, country.code), - TPL_VAR_STR(struct ip_info_t, country.name), - TPL_VAR_STR(struct ip_info_t, region.code), - TPL_VAR_STR(struct ip_info_t, region.name), - TPL_VAR_STR(struct ip_info_t, city.name), - TPL_VAR_STR(struct ip_info_t, city.zip_code), - TPL_VAR_DOUBLE(struct ip_info_t, latitude), - TPL_VAR_DOUBLE(struct ip_info_t, longitude), - TPL_VAR_STR(struct ip_info_t, metro.code), - TPL_VAR_STR(struct ip_info_t, metro.area), - TPL_VAR_STR(struct ip_info_t, ip), - TPL_VAR_STR(struct ip_info_t, callback), - TPL_VAR_SENTINEL -}; - -static const char const json_template_str[] = \ - "{{callback?}}{{callback}}({{/callback?}}" \ - "{" \ - "\"country_code\":\"{{country.code}}\"," \ - "\"country_name\":\"{{country.name}}\"," \ - "\"region_code\":\"{{region.code}}\"," \ - "\"region_name\":\"{{region.name}}\"," \ - "\"city\":\"{{city.name}}\"," \ - "\"zipcode\":\"{{city.zip_code}}\"," \ - "\"latitude\":{{latitude}}," \ - "\"longitude\":{{longitude}}," \ - "\"metro_code\":\"{{metro.code}}\"," \ - "\"areacode\":\"{{metro.area}}\"," \ - "\"ip\":\"{{ip}}\"" \ - "}" - "{{callback?}});{{/callback?}}"; - -static const char const xml_template_str[] = \ - "" \ - "" \ - "{{ip}}" \ - "{{country.code}}" \ - "{{country.name}}" \ - "{{region.code}}" \ - "{{region.name}}" \ - "{{city.name}}" \ - "{{city.zip_code}}" \ - "{{latitude}}" \ - "{{longitude}}" \ - "{{metro.code}}" \ - "{{metro.area}}" \ - ""; - -static const char const csv_template_str[] = \ - "\"{{ip}}\"," \ - "\"{{country.code}}\"," \ - "\"{{country.name}}\"," \ - "\"{{region.code}}\"," \ - "\"{{region.name}}\"," \ - "\"{{city.name}}\"," - "\"{{city.zip_code}}\"," \ - "\"{{latitude}}\"," \ - "\"{{longitude}}\"," \ - "\"{{metro.code}}\"," \ - "\"{{metro.area}}\""; - - -static const char const ip_to_city_query[] = \ - "SELECT " \ - " city_location.country_code, country_blocks.country_name," \ - " city_location.region_code, region_names.region_name," \ - " city_location.city_name, city_location.postal_code," \ - " city_location.latitude, city_location.longitude," \ - " city_location.metro_code, city_location.area_code " \ - "FROM city_blocks " \ - " NATURAL JOIN city_location " \ - " INNER JOIN country_blocks ON " \ - " city_location.country_code = country_blocks.country_code " \ - " INNER JOIN region_names ON " \ - " city_location.country_code = region_names.country_code " \ - " AND " \ - " city_location.region_code = region_names.region_code " \ - "WHERE city_blocks.ip_start <= ? " \ - "ORDER BY city_blocks.ip_start DESC LIMIT 1"; - - -union ip_to_octet { - unsigned char octet[sizeof(in_addr_t)]; - in_addr_t ip; -}; - -struct ip_net_t { - union ip_to_octet ip; - union ip_to_octet mask; -}; - -/* http://en.wikipedia.org/wiki/Reserved_IP_addresses */ -#define ADDRESS(o1, o2, o3, o4) \ - { .octet[0] = o1, .octet[1] = o2, .octet[2] = o3, .octet[3] = o4 } - -static const struct ip_net_t reserved_ips[] = { - {ADDRESS(0, 0, 0, 0), ADDRESS(255, 0, 0, 0)}, - {ADDRESS(10, 0, 0, 0), ADDRESS(255, 0, 0, 0)}, - {ADDRESS(100, 64, 0, 0), ADDRESS(255, 192, 0, 0)}, - {ADDRESS(127, 0, 0, 0), ADDRESS(255, 0, 0, 0)}, - {ADDRESS(169, 254, 0, 0), ADDRESS(255, 255, 0, 0)}, - {ADDRESS(172, 16, 0, 0), ADDRESS(255, 240, 0, 0)}, - {ADDRESS(192, 0, 0, 0), ADDRESS(255, 255, 255, 248)}, - {ADDRESS(192, 0, 2, 0), ADDRESS(255, 255, 255, 0)}, - {ADDRESS(192, 88, 99, 0), ADDRESS(255, 255, 255, 0)}, - {ADDRESS(192, 168, 0, 0), ADDRESS(255, 255, 0, 0)}, - {ADDRESS(198, 18, 0, 0), ADDRESS(255, 254, 0, 0)}, - {ADDRESS(198, 51, 100, 0), ADDRESS(255, 255, 255, 0)}, - {ADDRESS(203, 0, 113, 0), ADDRESS(255, 255, 255, 0)}, - {ADDRESS(224, 0, 0, 0), ADDRESS(240, 0, 0, 0)}, - {ADDRESS(240, 0, 0, 0), ADDRESS(240, 0, 0, 0)}, - {ADDRESS(255, 255, 255, 255), ADDRESS(255, 255, 255, 255)}, -}; - -#undef ADDRESS - - -#if QUERIES_PER_HOUR != 0 -struct query_limit_t { - struct cache_entry_t base; - unsigned queries; -}; - -static struct cache_t *query_limit; -#endif - - -static lwan_tpl_t *json_template = NULL; -static lwan_tpl_t *xml_template = NULL; -static lwan_tpl_t *csv_template = NULL; -static struct cache_t *cache = NULL; -static sqlite3 *db = NULL; - -static bool -net_contains_ip(const struct ip_net_t *net, in_addr_t ip) -{ - union ip_to_octet _ip = { .ip = ip }; - return (net->ip.octet[0] & net->mask.octet[0]) == (_ip.octet[0] & net->mask.octet[0]) && \ - (net->ip.octet[1] & net->mask.octet[1]) == (_ip.octet[1] & net->mask.octet[1]) && \ - (net->ip.octet[2] & net->mask.octet[2]) == (_ip.octet[2] & net->mask.octet[2]) && \ - (net->ip.octet[3] & net->mask.octet[3]) == (_ip.octet[3] & net->mask.octet[3]); -} - -static bool -is_reserved_ip(in_addr_t ip) -{ - size_t i; - for (i = 0; i < N_ELEMENTS(reserved_ips); i++) { - if (net_contains_ip(&reserved_ips[i], ip)) - return true; - } - return false; -} - -static void -destroy_ipinfo(struct cache_entry_t *entry, - void *context __attribute__((unused))) -{ - struct ip_info_t *ip_info = (struct ip_info_t *)entry; - - if (!ip_info) - return; - - free(ip_info->country.code); - free(ip_info->country.name); - free(ip_info->region.code); - free(ip_info->region.name); - free(ip_info->city.name); - free(ip_info->city.zip_code); - free(ip_info->metro.code); - free(ip_info->metro.area); - free(ip_info->ip); - free(ip_info); -} - -static ALWAYS_INLINE char * -text_column_helper(sqlite3_stmt *stmt, int ind) -{ - const unsigned char *value; - - value = sqlite3_column_text(stmt, ind); - return value ? strdup((char *)value) : NULL; -} - -static struct cache_entry_t * -create_ipinfo(const char *key, void *context __attribute__((unused))) -{ - sqlite3_stmt *stmt; - struct ip_info_t *ip_info = NULL; - struct in_addr addr; - - if (UNLIKELY(!inet_aton(key, &addr))) - goto end_no_finalize; - - if (is_reserved_ip(addr.s_addr)) { - ip_info = calloc(1, sizeof(*ip_info)); - if (LIKELY(ip_info)) { - ip_info->country.code = strdup("RD"); - ip_info->country.name = strdup("Reserved"); - ip_info->ip = strdup(key); - } - goto end_no_finalize; - } - - if (sqlite3_prepare(db, ip_to_city_query, - sizeof(ip_to_city_query) - 1, - &stmt, NULL) != SQLITE_OK) - goto end_no_finalize; - - if (sqlite3_bind_int64(stmt, 1, ntohl(addr.s_addr)) != SQLITE_OK) - goto end; - - if (sqlite3_step(stmt) != SQLITE_ROW) - goto end; - - ip_info = malloc(sizeof(*ip_info)); - if (!ip_info) - goto end; - -#define TEXT_COLUMN(index) text_column_helper(stmt, index) - - ip_info->country.code = TEXT_COLUMN(0); - ip_info->country.name = TEXT_COLUMN(1); - ip_info->region.code = TEXT_COLUMN(2); - ip_info->region.name = TEXT_COLUMN(3); - ip_info->city.name = TEXT_COLUMN(4); - ip_info->city.zip_code = TEXT_COLUMN(5); - ip_info->latitude = sqlite3_column_double(stmt, 6); - ip_info->longitude = sqlite3_column_double(stmt, 7); - ip_info->metro.code = TEXT_COLUMN(8); - ip_info->metro.area = TEXT_COLUMN(9); - -#undef TEXT_COLUMN - - ip_info->ip = strdup(key); - -end: - sqlite3_finalize(stmt); -end_no_finalize: - return (struct cache_entry_t *)ip_info; -} - -#if QUERIES_PER_HOUR != 0 -static struct cache_entry_t * -create_query_limit(const char *key __attribute__((unused)), - void *context __attribute__((unused))) -{ - struct query_limit_t *entry = malloc(sizeof(*entry)); - if (LIKELY(entry)) - entry->queries = 0; - return (struct cache_entry_t *)entry; -} - -static void -destroy_query_limit(struct cache_entry_t *entry, - void *context __attribute__((unused))) -{ - free(entry); -} -#endif - -static struct ip_info_t * -internal_query(lwan_request_t *request, const char *ip_address) -{ - const char *query; - - if (request->url.len == 0) - query = ip_address; - else if (request->url.len < 7) - query = NULL; - else - query = request->url.value; - if (UNLIKELY(!query)) - return NULL; - - return (struct ip_info_t *)cache_coro_get_and_ref_entry(cache, - request->conn->coro, query); -} - -#if QUERIES_PER_HOUR != 0 -static bool is_rate_limited(const char *ip_address) -{ - bool limited; - int error; - struct query_limit_t *limit; - - limit = (struct query_limit_t *) - cache_get_and_ref_entry(query_limit, ip_address, &error); - if (!limit) - return true; - - limited = ATOMIC_AAF(&limit->queries, 1) > QUERIES_PER_HOUR; - cache_entry_unref(query_limit, &limit->base); - - return limited; -} -#endif - -static lwan_http_status_t -templated_output(lwan_request_t *request, - lwan_response_t *response, - void *data) -{ - const char *ip_address; - lwan_tpl_t *tpl = data; - struct ip_info_t *info; - char ip_address_buf[INET6_ADDRSTRLEN]; - - ip_address = lwan_request_get_remote_address(request, ip_address_buf); - if (UNLIKELY(!ip_address)) - return HTTP_INTERNAL_ERROR; - -#if QUERIES_PER_HOUR != 0 - if (UNLIKELY(is_rate_limited(ip_address))) - return HTTP_FORBIDDEN; -#endif - - info = internal_query(request, ip_address); - if (UNLIKELY(!info)) - return HTTP_NOT_FOUND; - - if (tpl == json_template) - response->mime_type = "application/json; charset=UTF-8"; - else if (tpl == xml_template) - response->mime_type = "application/xml; charset=UTF-8"; - else - response->mime_type = "text/plain; charset=UTF-8"; - - const char *callback = lwan_request_get_query_param(request, "callback"); - struct ip_info_t info_with_callback = *info; - info_with_callback.callback = callback; - - lwan_tpl_apply_with_buffer(tpl, response->buffer, - &info_with_callback); - - return HTTP_OK; -} - -int -main(void) -{ - lwan_t l; - - lwan_init(&l); - - json_template = lwan_tpl_compile_string(json_template_str, template_descriptor); - if (!json_template) - lwan_status_critical("Could not compile JSON template"); - xml_template = lwan_tpl_compile_string(xml_template_str, template_descriptor); - if (!xml_template) - lwan_status_critical("Could not compile XML template"); - csv_template = lwan_tpl_compile_string(csv_template_str, template_descriptor); - if (!csv_template) - lwan_status_critical("Could not compile CSV template"); - - int result = sqlite3_open_v2("./db/ipdb.sqlite", &db, - SQLITE_OPEN_READONLY, NULL); - if (result != SQLITE_OK) - lwan_status_critical("Could not open database: %s", - sqlite3_errmsg(db)); - cache = cache_create(create_ipinfo, destroy_ipinfo, NULL, 10); - -#if QUERIES_PER_HOUR != 0 - lwan_status_info("Limiting to %d queries per hour per client", - QUERIES_PER_HOUR); - query_limit = cache_create(create_query_limit, - destroy_query_limit, NULL, 3600); -#else - lwan_status_info("Rate-limiting disabled"); -#endif - - const lwan_url_map_t default_map[] = { - { .prefix = "/json/", .handler = templated_output, .data = json_template }, - { .prefix = "/xml/", .handler = templated_output, .data = xml_template }, - { .prefix = "/csv/", .handler = templated_output, .data = csv_template }, - { .prefix = "/", SERVE_FILES("./static") }, - { .prefix = NULL } - }; - - lwan_set_url_map(&l, default_map); - lwan_main_loop(&l); - lwan_shutdown(&l); - - lwan_tpl_free(csv_template); - lwan_tpl_free(xml_template); - lwan_tpl_free(json_template); -#if QUERIES_PER_HOUR != 0 - cache_destroy(query_limit); -#endif - cache_destroy(cache); - sqlite3_close(db); - - return 0; -} diff --git a/fuzz/README b/fuzz/README new file mode 100644 index 000000000..162c94cfe --- /dev/null +++ b/fuzz/README @@ -0,0 +1,2 @@ +`http.dict' has been copied from the h2o web server project: +https://github.com/h2o/h2o/blob/c14554e7/fuzz/http.dict diff --git a/fuzz/corpus/corpus-config-234910329 b/fuzz/corpus/corpus-config-234910329 new file mode 100644 index 000000000..5fc1d7064 --- /dev/null +++ b/fuzz/corpus/corpus-config-234910329 @@ -0,0 +1,125 @@ +# Timeout in seconds to keep a connection alive. +keep_alive_timeout = ${KEEP_ALIVE_TIMEOUT:15} + +# Set to true to not print any debugging messages. (Only effective in +# release builds.) +quiet = false + +# Set SO_REUSEPORT=1 in the master socket. +reuse_port = false + +# Value of "Expires" header. Default is 1 month and 1 week. +expires = 1M 1w + +# Number of I/O threads. Default (0) is number of online CPUs. +threads = 0 + +# This flag is enabled here so that the automated tests can be executed +# properly, but should be disabled unless absolutely needed (an example +# would be haproxy). +proxy_protocol = true + +# Maximum post data size of slightly less than 1MiB. The default is too +# small for testing purposes. +max_post_data_size = 1000000 + +# Enable straitjacket by default. The `drop_capabilities` option is `true` +# by default. Other options may require more privileges. +straitjacket + +listener *:8080 { + &custom_header /customhdr + + &sleep /sleep + + &hello_world /hello + + &quit_lwan /quit-lwan + + &test_proxy /proxy + + &test_chunked_encoding /chunked + + &test_server_sent_event /sse + + &gif_beacon /beacon + + &gif_beacon /favicon.ico + + &test_post_will_it_blend /post/blend + + &test_post_big /post/big + + redirect /elsewhere { to = http://lwan.ws } + + redirect /redirect307 { + to = http://lwan.ws + code = 307 + } + + rewrite /read-env { + pattern user { rewrite as = /hello?name=${USER} } + } + + response /brew-coffee { code = 418 } + + &hello_world /admin { + authorization basic { + realm = Administration Page + password file = htpasswd + } + } + lua /inline { + default type = text/html + cache period = 30s + script = '''function handle_get_root(req) + req:say('Hello') + end''' + } + lua /lua { + default type = text/html + script file = test.lua + cache period = 30s + } + lua /luawait { + script='''function handle_get_root(req) + local ms = req:query_param[[ms]] + if not ms then ms = 1234 end + req:say("sleeping "..ms.."ms") + req:sleep(ms) + req:say("slept") + end''' + } + rewrite /pattern { + pattern foo/(%d+)(%a)(%d+) { + redirect to = /hello?name=pre%2middle%3othermiddle%1post + } + pattern bar/(%d+)/test { + rewrite as = /hello?name=rewritten%1 + } + pattern lua/redir/(%d+)x(%d+) { + expand_with_lua = true + redirect to = ''' + function handle_rewrite(req, captures) + local r = captures[1] * captures[2] + return '/hello?name=redirected' .. r + end + ''' + } + pattern lua/rewrite/(%d+)x(%d+) { + expand_with_lua = true + rewrite as = """function handle_rewrite(req, captures) + local r = captures[1] * captures[2] + return '/hello?name=rewritten' .. r + end""" + } + } + serve_files / { + path = ./wwwroot + + # When requesting for file.ext, look for a smaller/newer file.ext.gz, + # and serve that instead if `Accept-Encoding: gzip` is in the + # request headers. + serve precompressed files = true + } +} diff --git a/fuzz/corpus/corpus-h2_huffman-1 b/fuzz/corpus/corpus-h2_huffman-1 new file mode 100644 index 000000000..8edd843c5 --- /dev/null +++ b/fuzz/corpus/corpus-h2_huffman-1 @@ -0,0 +1,11 @@ +եK�2iĦK��.��O��D�d(d(5l} +D�z��9�:����%�J} �J�E,(�҃�H�j��ɉu�9T��&��:��9���9jS"��=ڦ�R1�rȥ%PiS��!��HRSԕ%=�Tj��֚�� + D��VZ��_)�hYkPYI�@�)9T���b�j/��g���Ρm)!Ԡ�B�gU R��¨�R_�T�dR��Zc-�S�J�{���%'L�C�B�j�i RGE��Q��'T���)8�D�U$(�P�H����B��z�)=.���>�I�EjR_�L�N>�RC�U��<�R:�(��T�Q3�*dP��:T��Q'Rq�*���(j�EfIT<�S�J��]K��L�N>�R[h�҄=R��K"��� ����҇�I�b�-�jKRTAԔv(4)Qf��I��Jm$�&q�L�٧J�*$�N>�R��AH�ȡ��Q eE�u2IA��A��~���B���)O��T�VsT��ȢGԪTu ad2TIԧ� D��9T�iIlYc%C!A�1�J�a/�+:T!C%u7�[ +��*��Z��Cʥ��J�z�*��E+��'҇�DP �$�Gґڕ�Ū�TU %�C!A�`�2}.�2�d)QҠԤu)�s��i'*g�*x2gRs!I�T�P�)9��g'R� ���9T!�M���ғ� +Rt%D$v&�U2��v �-�BLE �8Ҥ�P�Ptx�(j�Z�Z=)����dJ�B���������Z�҆��U2���٧�Җ�AP�QT�hR� R}(yT�{:���!�S|尪 +Tu%�� R��L�GL�e��k-���R¡+�? \ No newline at end of file diff --git a/fuzz/corpus/corpus-h2_huffman-2 b/fuzz/corpus/corpus-h2_huffman-2 new file mode 100644 index 000000000..539971170 --- /dev/null +++ b/fuzz/corpus/corpus-h2_huffman-2 @@ -0,0 +1,2 @@ +��c��ɻ��^R�j�f�q7 +�]c�{*��d��~/������6A \ No newline at end of file diff --git a/fuzz/corpus/corpus-h2_huffman-3 b/fuzz/corpus/corpus-h2_huffman-3 new file mode 100644 index 000000000..4eabe97b7 --- /dev/null +++ b/fuzz/corpus/corpus-h2_huffman-3 @@ -0,0 +1 @@ +�d�ue,� \ No newline at end of file diff --git a/fuzz/corpus/corpus-pattern-1 b/fuzz/corpus/corpus-pattern-1 new file mode 100644 index 000000000..7513f19d0 --- /dev/null +++ b/fuzz/corpus/corpus-pattern-1 @@ -0,0 +1 @@ +lua/rewrite/7x6 \ No newline at end of file diff --git a/fuzz/corpus/corpus-pattern-2 b/fuzz/corpus/corpus-pattern-2 new file mode 100644 index 000000000..6a8320f92 --- /dev/null +++ b/fuzz/corpus/corpus-pattern-2 @@ -0,0 +1 @@ +lua/redir/6x7 \ No newline at end of file diff --git a/fuzz/corpus/corpus-pattern-3 b/fuzz/corpus/corpus-pattern-3 new file mode 100644 index 000000000..c841d324d --- /dev/null +++ b/fuzz/corpus/corpus-pattern-3 @@ -0,0 +1 @@ +bar/42/test \ No newline at end of file diff --git a/fuzz/corpus/corpus-pattern-4 b/fuzz/corpus/corpus-pattern-4 new file mode 100644 index 000000000..53e3de601 --- /dev/null +++ b/fuzz/corpus/corpus-pattern-4 @@ -0,0 +1 @@ +foo/1234x5678 \ No newline at end of file diff --git a/fuzz/corpus/corpus-request-1012502954 b/fuzz/corpus/corpus-request-1012502954 new file mode 100644 index 000000000..c97aa5e32 --- /dev/null +++ b/fuzz/corpus/corpus-request-1012502954 @@ -0,0 +1,7 @@ +GET /redirect307 HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1025202362 b/fuzz/corpus/corpus-request-1025202362 new file mode 100644 index 000000000..88e6612d2 --- /dev/null +++ b/fuzz/corpus/corpus-request-1025202362 @@ -0,0 +1,8 @@ +GET /admin HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Authorization: Basic bm9zdWNoOnVzZXI= + diff --git a/fuzz/corpus/corpus-request-1036140795 b/fuzz/corpus/corpus-request-1036140795 new file mode 100644 index 000000000..7059dc05f --- /dev/null +++ b/fuzz/corpus/corpus-request-1036140795 @@ -0,0 +1,8 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Range: bytes=-100 + diff --git a/fuzz/corpus/corpus-request-1067854538 b/fuzz/corpus/corpus-request-1067854538 new file mode 100644 index 000000000..0f13870b2 --- /dev/null +++ b/fuzz/corpus/corpus-request-1067854538 @@ -0,0 +1,9 @@ +POST /post/big HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Content-Type: x-test/trololo +Content-Length: 203 + diff --git a/fuzz/corpus/corpus-request-1102520059 b/fuzz/corpus/corpus-request-1102520059 new file mode 100644 index 000000000..8b30950cb --- /dev/null +++ b/fuzz/corpus/corpus-request-1102520059 @@ -0,0 +1,8 @@ +GET /admin HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Authorization: Basic Zm9vYmFyOnRlc3QxMjM= + diff --git a/fuzz/corpus/corpus-request-1117142618 b/fuzz/corpus/corpus-request-1117142618 new file mode 100644 index 000000000..1800f3461 --- /dev/null +++ b/fuzz/corpus/corpus-request-1117142618 @@ -0,0 +1,7 @@ +GET /lua/hello?name=foo HTTP/1.1 +Host: localhost:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1129566413 b/fuzz/corpus/corpus-request-1129566413 new file mode 100644 index 000000000..e53c2a648 --- /dev/null +++ b/fuzz/corpus/corpus-request-1129566413 @@ -0,0 +1,7 @@ +GET /100.html HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1139901474 b/fuzz/corpus/corpus-request-1139901474 new file mode 100644 index 000000000..53ad6dda4 --- /dev/null +++ b/fuzz/corpus/corpus-request-1139901474 @@ -0,0 +1,5 @@ +GET /hello?name=name0006 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-116087764 b/fuzz/corpus/corpus-request-116087764 new file mode 100644 index 000000000..115184de8 --- /dev/null +++ b/fuzz/corpus/corpus-request-116087764 @@ -0,0 +1,2 @@ +GET / FROG/1.0 + diff --git a/fuzz/corpus/corpus-request-1186452551 b/fuzz/corpus/corpus-request-1186452551 new file mode 100644 index 000000000..54b5f349e --- /dev/null +++ b/fuzz/corpus/corpus-request-1186452551 @@ -0,0 +1,5 @@ +GET /hello?name=name0001 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1231192379 b/fuzz/corpus/corpus-request-1231192379 new file mode 100644 index 000000000..8e43c521f --- /dev/null +++ b/fuzz/corpus/corpus-request-1231192379 @@ -0,0 +1,7 @@ +GET /lua/invalid_code HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1239036029 b/fuzz/corpus/corpus-request-1239036029 new file mode 100644 index 000000000..25cc0b700 --- /dev/null +++ b/fuzz/corpus/corpus-request-1239036029 @@ -0,0 +1,5 @@ +GET /hello?name=name000a HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1244316437 b/fuzz/corpus/corpus-request-1244316437 new file mode 100644 index 000000000..1284eef1b --- /dev/null +++ b/fuzz/corpus/corpus-request-1244316437 @@ -0,0 +1,5 @@ +GET /hello?name=name0002 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1264095060 b/fuzz/corpus/corpus-request-1264095060 new file mode 100644 index 000000000..c99c42826 --- /dev/null +++ b/fuzz/corpus/corpus-request-1264095060 @@ -0,0 +1,8 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Range: bytes=0-50 + diff --git a/fuzz/corpus/corpus-request-1285228804 b/fuzz/corpus/corpus-request-1285228804 new file mode 100644 index 000000000..e1b743c80 --- /dev/null +++ b/fuzz/corpus/corpus-request-1285228804 @@ -0,0 +1,8 @@ +GET /customhdr?hdr=Marco HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Marco: Polo + diff --git a/fuzz/corpus/corpus-request-1335354340 b/fuzz/corpus/corpus-request-1335354340 new file mode 100644 index 000000000..01b7bad08 --- /dev/null +++ b/fuzz/corpus/corpus-request-1335354340 @@ -0,0 +1,7 @@ +GET /pattern/foo/1234x5678 HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1350573793 b/fuzz/corpus/corpus-request-1350573793 new file mode 100644 index 000000000..ed7be3e03 --- /dev/null +++ b/fuzz/corpus/corpus-request-1350573793 @@ -0,0 +1,5 @@ +GET /hello?name=name000d HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1365180540 b/fuzz/corpus/corpus-request-1365180540 new file mode 100644 index 000000000..9bb955c75 --- /dev/null +++ b/fuzz/corpus/corpus-request-1365180540 @@ -0,0 +1,7 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1374344043 b/fuzz/corpus/corpus-request-1374344043 new file mode 100644 index 000000000..8f8507dcf --- /dev/null +++ b/fuzz/corpus/corpus-request-1374344043 @@ -0,0 +1,7 @@ +HEAD /100.html HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foobar +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1376710097 b/fuzz/corpus/corpus-request-1376710097 new file mode 100644 index 000000000..cbbe0f6ba --- /dev/null +++ b/fuzz/corpus/corpus-request-1376710097 @@ -0,0 +1,8 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Range: bytes=0-40000 + diff --git a/fuzz/corpus/corpus-request-137806862 b/fuzz/corpus/corpus-request-137806862 new file mode 100644 index 000000000..942d58446 --- /dev/null +++ b/fuzz/corpus/corpus-request-137806862 @@ -0,0 +1,7 @@ +GET /icons HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foobar +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1398295499 b/fuzz/corpus/corpus-request-1398295499 new file mode 100644 index 000000000..99a809136 --- /dev/null +++ b/fuzz/corpus/corpus-request-1398295499 @@ -0,0 +1,2 @@ + + diff --git a/fuzz/corpus/corpus-request-1409959708 b/fuzz/corpus/corpus-request-1409959708 new file mode 100644 index 000000000..917c13920 --- /dev/null +++ b/fuzz/corpus/corpus-request-1409959708 @@ -0,0 +1,8 @@ +GET /lua/cookie HTTP/1.1 +Host: localhost:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Cookie: FOO=BAR + diff --git a/fuzz/corpus/corpus-request-1424268980 b/fuzz/corpus/corpus-request-1424268980 new file mode 100644 index 000000000..66a5df446 --- /dev/null +++ b/fuzz/corpus/corpus-request-1424268980 @@ -0,0 +1,7 @@ +GET /100.html HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: deflote +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1433925857 b/fuzz/corpus/corpus-request-1433925857 new file mode 100644 index 000000000..fb0923d94 --- /dev/null +++ b/fuzz/corpus/corpus-request-1433925857 @@ -0,0 +1,7 @@ +GET / HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1450573622 b/fuzz/corpus/corpus-request-1450573622 new file mode 100644 index 000000000..eccd0a8ec --- /dev/null +++ b/fuzz/corpus/corpus-request-1450573622 @@ -0,0 +1,7 @@ +GET /pattern/lua/redir/6x7 HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1476153275 b/fuzz/corpus/corpus-request-1476153275 new file mode 100644 index 000000000..4c4c1565c --- /dev/null +++ b/fuzz/corpus/corpus-request-1476153275 @@ -0,0 +1,5 @@ +GET /hello?name=name0004 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1494613810 b/fuzz/corpus/corpus-request-1494613810 new file mode 100644 index 000000000..ec577c4c8 --- /dev/null +++ b/fuzz/corpus/corpus-request-1494613810 @@ -0,0 +1,7 @@ +GET /pattern/bar/42/test HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1504569917 b/fuzz/corpus/corpus-request-1504569917 new file mode 100644 index 000000000..e16018a8d --- /dev/null +++ b/fuzz/corpus/corpus-request-1504569917 @@ -0,0 +1,5 @@ +PROXY TCP4 192.168.242.221 192.168.242.242 56324 31337 +GET /proxy HTTP/1.1 +Connection: keep-alive +Host: 192.168.0.11 + diff --git a/fuzz/corpus/corpus-request-1548233367 b/fuzz/corpus/corpus-request-1548233367 new file mode 100644 index 000000000..3ef72da54 --- /dev/null +++ b/fuzz/corpus/corpus-request-1548233367 @@ -0,0 +1,7 @@ +HEAD /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foobar +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-159259470 b/fuzz/corpus/corpus-request-159259470 new file mode 100644 index 000000000..b88dc132c --- /dev/null +++ b/fuzz/corpus/corpus-request-159259470 @@ -0,0 +1,4 @@ +GET /proxy HTTP/1.1 +Connection: keep-alive +Host: 192.168.0.11 + diff --git a/fuzz/corpus/corpus-request-1600028624 b/fuzz/corpus/corpus-request-1600028624 new file mode 100644 index 000000000..3f8813ee5 --- /dev/null +++ b/fuzz/corpus/corpus-request-1600028624 @@ -0,0 +1,7 @@ +GET /hello?name=testsuite HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1605894428 b/fuzz/corpus/corpus-request-1605894428 new file mode 100644 index 000000000..e1b8a7fa5 --- /dev/null +++ b/fuzz/corpus/corpus-request-1605894428 @@ -0,0 +1,5 @@ +GET /hello?name=name000f HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1605908235 b/fuzz/corpus/corpus-request-1605908235 new file mode 100644 index 000000000..5e889f187 --- /dev/null +++ b/fuzz/corpus/corpus-request-1605908235 @@ -0,0 +1,5 @@ +GET /hello?name=name000c HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1626276121 b/fuzz/corpus/corpus-request-1626276121 new file mode 100644 index 000000000..49315d481 --- /dev/null +++ b/fuzz/corpus/corpus-request-1626276121 @@ -0,0 +1,5 @@ +GET /hello?name=name0007 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1631518149 b/fuzz/corpus/corpus-request-1631518149 new file mode 100644 index 000000000..89f8de102 --- /dev/null +++ b/fuzz/corpus/corpus-request-1631518149 @@ -0,0 +1,7 @@ +GET /lua/hello HTTP/1.1 +Host: localhost:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1649760492 b/fuzz/corpus/corpus-request-1649760492 new file mode 100644 index 000000000..40620dff5 --- /dev/null +++ b/fuzz/corpus/corpus-request-1649760492 @@ -0,0 +1,7 @@ +GET /admin HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1714636915 b/fuzz/corpus/corpus-request-1714636915 new file mode 100644 index 000000000..3f741e52b --- /dev/null +++ b/fuzz/corpus/corpus-request-1714636915 @@ -0,0 +1,7 @@ +GET /hello HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1726956429 b/fuzz/corpus/corpus-request-1726956429 new file mode 100644 index 000000000..473918993 --- /dev/null +++ b/fuzz/corpus/corpus-request-1726956429 @@ -0,0 +1,7 @@ +GET /100.html HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1749698586 b/fuzz/corpus/corpus-request-1749698586 new file mode 100644 index 000000000..41f8aa1b6 --- /dev/null +++ b/fuzz/corpus/corpus-request-1749698586 @@ -0,0 +1,8 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Range: bytes=50-0 + diff --git a/fuzz/corpus/corpus-request-1780695788 b/fuzz/corpus/corpus-request-1780695788 new file mode 100644 index 000000000..5a28ae31b --- /dev/null +++ b/fuzz/corpus/corpus-request-1780695788 @@ -0,0 +1,7 @@ +HEAD /icons/ HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1827336327 b/fuzz/corpus/corpus-request-1827336327 new file mode 100644 index 000000000..b749615ff --- /dev/null +++ b/fuzz/corpus/corpus-request-1827336327 @@ -0,0 +1,7 @@ +GET /../../../../../../../../../etc/passwd HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-184803526 b/fuzz/corpus/corpus-request-184803526 new file mode 100644 index 000000000..f7322189b --- /dev/null +++ b/fuzz/corpus/corpus-request-184803526 @@ -0,0 +1,7 @@ +GET /100.html HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foo,bar,deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-187309218 b/fuzz/corpus/corpus-request-187309218 new file mode 100644 index 000000000..a8e57eed9 --- /dev/null +++ b/fuzz/corpus/corpus-request-187309218 @@ -0,0 +1,16 @@ +GET /ws HTTP/1.1 +Host: localhost:8080 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0 +Accept: */* +Accept-Language: en-US,en;q=0.5 +Accept-Encoding: gzip, deflate +Sec-WebSocket-Version: 13 +Origin: http://localhost:8080 +Sec-WebSocket-Extensions: permessage-deflate +Sec-WebSocket-Key: YNDTJflH7V1X70XY2XHR0A== +DNT: 1 +Connection: keep-alive, Upgrade +Pragma: no-cache +Cache-Control: no-cache +Upgrade: websocket + diff --git a/fuzz/corpus/corpus-request-1884661237 b/fuzz/corpus/corpus-request-1884661237 new file mode 100644 index 000000000..89acdd422 --- /dev/null +++ b/fuzz/corpus/corpus-request-1884661237 @@ -0,0 +1,5 @@ +GET /hello?name=name000b HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-1889947178 b/fuzz/corpus/corpus-request-1889947178 new file mode 100644 index 000000000..ce8b6f907 --- /dev/null +++ b/fuzz/corpus/corpus-request-1889947178 @@ -0,0 +1,7 @@ +HEAD /icons HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1909002904 b/fuzz/corpus/corpus-request-1909002904 new file mode 100644 index 000000000..12ed6b893 --- /dev/null +++ b/fuzz/corpus/corpus-request-1909002904 @@ -0,0 +1,9 @@ +POST /post/big HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Content-Type: x-test/trololo +Content-Length: 23 + diff --git a/fuzz/corpus/corpus-request-1911165193 b/fuzz/corpus/corpus-request-1911165193 new file mode 100644 index 000000000..937d7c042 --- /dev/null +++ b/fuzz/corpus/corpus-request-1911165193 @@ -0,0 +1,2 @@ +GET / HTTP/2.0 + diff --git a/fuzz/corpus/corpus-request-1939964443 b/fuzz/corpus/corpus-request-1939964443 new file mode 100644 index 000000000..b31713a6d --- /dev/null +++ b/fuzz/corpus/corpus-request-1939964443 @@ -0,0 +1,9 @@ +POST /post/big HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Content-Type: x-test/trololo +Content-Length: 2003 + diff --git a/fuzz/corpus/corpus-request-1957747793 b/fuzz/corpus/corpus-request-1957747793 new file mode 100644 index 000000000..ef97e38f9 --- /dev/null +++ b/fuzz/corpus/corpus-request-1957747793 @@ -0,0 +1,8 @@ +GET /admin HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Authorization: Basic Zm9vOnRlc3QxMjM= + diff --git a/fuzz/corpus/corpus-request-1973594324 b/fuzz/corpus/corpus-request-1973594324 new file mode 100644 index 000000000..d23555f60 --- /dev/null +++ b/fuzz/corpus/corpus-request-1973594324 @@ -0,0 +1,7 @@ +GET /chunked HTTP/1.1 +Host: localhost:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-1975960378 b/fuzz/corpus/corpus-request-1975960378 new file mode 100644 index 000000000..a43e840ea --- /dev/null +++ b/fuzz/corpus/corpus-request-1975960378 @@ -0,0 +1,8 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Range: bytes=50- + diff --git a/fuzz/corpus/corpus-request-2114738097 b/fuzz/corpus/corpus-request-2114738097 new file mode 100644 index 000000000..c671b13ea --- /dev/null +++ b/fuzz/corpus/corpus-request-2114738097 @@ -0,0 +1,7 @@ +HEAD /hello HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foobar +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-2130794395 b/fuzz/corpus/corpus-request-2130794395 new file mode 100644 index 000000000..a189e4fc4 --- /dev/null +++ b/fuzz/corpus/corpus-request-2130794395 @@ -0,0 +1,5 @@ +GET /hello?name=name0009 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-213975407 b/fuzz/corpus/corpus-request-213975407 new file mode 100644 index 000000000..1763a1b45 --- /dev/null +++ b/fuzz/corpus/corpus-request-213975407 @@ -0,0 +1,5 @@ +GET /hello?name=name0005 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-269455306 b/fuzz/corpus/corpus-request-269455306 new file mode 100644 index 000000000..15b612a60 --- /dev/null +++ b/fuzz/corpus/corpus-request-269455306 @@ -0,0 +1,7 @@ +GET /hello?key=&otherkey=&name=testsuite&dump_vars=1 HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-321123 b/fuzz/corpus/corpus-request-321123 new file mode 100644 index 000000000..a8505ceea --- /dev/null +++ b/fuzz/corpus/corpus-request-321123 @@ -0,0 +1,8 @@ +PROXY TCP4 192.168.242.221 192.168.242.242 56324 31337 +GET /hello HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-3298283 b/fuzz/corpus/corpus-request-3298283 new file mode 100644 index 000000000..bcbc1085e Binary files /dev/null and b/fuzz/corpus/corpus-request-3298283 differ diff --git a/fuzz/corpus/corpus-request-338888228 b/fuzz/corpus/corpus-request-338888228 new file mode 100644 index 000000000..dc0ed21b2 --- /dev/null +++ b/fuzz/corpus/corpus-request-338888228 @@ -0,0 +1,2 @@ +asldkfjg238045tgqwdcjv1li 2u4ftw dfjkb12345t + diff --git a/fuzz/corpus/corpus-request-356426808 b/fuzz/corpus/corpus-request-356426808 new file mode 100644 index 000000000..ef2d21481 --- /dev/null +++ b/fuzz/corpus/corpus-request-356426808 @@ -0,0 +1,7 @@ +HEAD / HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-364228444 b/fuzz/corpus/corpus-request-364228444 new file mode 100644 index 000000000..476a2861f --- /dev/null +++ b/fuzz/corpus/corpus-request-364228444 @@ -0,0 +1,8 @@ +GET /hello?dump_vars=1 HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Cookie: OTHERCOOKIE=some cookie value; SOMECOOKIE=1c330301-89e4-408a-bf6c-ce107efe8a27; foo=bar + diff --git a/fuzz/corpus/corpus-request-387346491 b/fuzz/corpus/corpus-request-387346491 new file mode 100644 index 000000000..7066c1bf4 --- /dev/null +++ b/fuzz/corpus/corpus-request-387346491 @@ -0,0 +1,2 @@ +GET http://example.com HTTP/1.0 + diff --git a/fuzz/corpus/corpus-request-412776091 b/fuzz/corpus/corpus-request-412776091 new file mode 100644 index 000000000..037677a6e --- /dev/null +++ b/fuzz/corpus/corpus-request-412776091 @@ -0,0 +1,7 @@ +GET /100.html HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foo, bar, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-42999170 b/fuzz/corpus/corpus-request-42999170 new file mode 100644 index 000000000..7b48f3ea6 --- /dev/null +++ b/fuzz/corpus/corpus-request-42999170 @@ -0,0 +1,7 @@ +GET /icons/ HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foobar +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-439493451 b/fuzz/corpus/corpus-request-439493451 new file mode 100644 index 000000000..88f63b04b --- /dev/null +++ b/fuzz/corpus/corpus-request-439493451 @@ -0,0 +1,7 @@ +GET /inline HTTP/1.1 +Host: localhost:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-511702305 b/fuzz/corpus/corpus-request-511702305 new file mode 100644 index 000000000..2234dc344 --- /dev/null +++ b/fuzz/corpus/corpus-request-511702305 @@ -0,0 +1,7 @@ +GET /icons HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-515530019 b/fuzz/corpus/corpus-request-515530019 new file mode 100644 index 000000000..557dc0f4a --- /dev/null +++ b/fuzz/corpus/corpus-request-515530019 @@ -0,0 +1,7 @@ +GET /lua/brew_coffee HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-524872353 b/fuzz/corpus/corpus-request-524872353 new file mode 100644 index 000000000..91dbe2745 --- /dev/null +++ b/fuzz/corpus/corpus-request-524872353 @@ -0,0 +1,7 @@ +GET /read-env/user HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-532670688 b/fuzz/corpus/corpus-request-532670688 new file mode 100644 index 000000000..fa594c1c2 --- /dev/null +++ b/fuzz/corpus/corpus-request-532670688 @@ -0,0 +1,9 @@ +POST /post/blend HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Content-Length: 23 +Content-Type: application/json + diff --git a/fuzz/corpus/corpus-request-593209441 b/fuzz/corpus/corpus-request-593209441 new file mode 100644 index 000000000..840fb19d9 --- /dev/null +++ b/fuzz/corpus/corpus-request-593209441 @@ -0,0 +1,7 @@ +GET /100.html HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foobar +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-603570492 b/fuzz/corpus/corpus-request-603570492 new file mode 100644 index 000000000..4077d2d49 --- /dev/null +++ b/fuzz/corpus/corpus-request-603570492 @@ -0,0 +1,7 @@ +GET //////////100.html HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-631704567 b/fuzz/corpus/corpus-request-631704567 new file mode 100644 index 000000000..4f1a27733 --- /dev/null +++ b/fuzz/corpus/corpus-request-631704567 @@ -0,0 +1,9 @@ +POST /hello?dump_vars=1 HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Content-Length: 25 +Content-Type: application/x-www-form-urlencoded + diff --git a/fuzz/corpus/corpus-request-653468858 b/fuzz/corpus/corpus-request-653468858 new file mode 100644 index 000000000..b9bccd276 --- /dev/null +++ b/fuzz/corpus/corpus-request-653468858 @@ -0,0 +1,5 @@ +GET /hello?name=name0008 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-654887343 b/fuzz/corpus/corpus-request-654887343 new file mode 100644 index 000000000..5040086b0 --- /dev/null +++ b/fuzz/corpus/corpus-request-654887343 @@ -0,0 +1,7 @@ +GET /pattern/lua/rewrite/7x6 HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-709393584 b/fuzz/corpus/corpus-request-709393584 new file mode 100644 index 000000000..6b0a5b90a --- /dev/null +++ b/fuzz/corpus/corpus-request-709393584 @@ -0,0 +1,7 @@ +HEAD /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-745425661 b/fuzz/corpus/corpus-request-745425661 new file mode 100644 index 000000000..8232cd31f --- /dev/null +++ b/fuzz/corpus/corpus-request-745425661 @@ -0,0 +1,7 @@ +GET /sleep?ms=1500 HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-752392754 b/fuzz/corpus/corpus-request-752392754 new file mode 100644 index 000000000..de1cdda06 --- /dev/null +++ b/fuzz/corpus/corpus-request-752392754 @@ -0,0 +1,7 @@ +GET /icons/non-existent-file.png HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-76065818 b/fuzz/corpus/corpus-request-76065818 new file mode 100644 index 000000000..ac2d9f199 --- /dev/null +++ b/fuzz/corpus/corpus-request-76065818 @@ -0,0 +1,5 @@ +GET /hello?name=name000e HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-771151432 b/fuzz/corpus/corpus-request-771151432 new file mode 100644 index 000000000..685f61f0f --- /dev/null +++ b/fuzz/corpus/corpus-request-771151432 @@ -0,0 +1,5 @@ +GET /hello?name=name0000 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-request-805750846 b/fuzz/corpus/corpus-request-805750846 new file mode 100644 index 000000000..60a15c149 --- /dev/null +++ b/fuzz/corpus/corpus-request-805750846 @@ -0,0 +1,7 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: foobar +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-846930886 b/fuzz/corpus/corpus-request-846930886 new file mode 100644 index 000000000..5058aaa19 --- /dev/null +++ b/fuzz/corpus/corpus-request-846930886 @@ -0,0 +1,7 @@ +GET /brew-coffee HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-8936987 b/fuzz/corpus/corpus-request-8936987 new file mode 100644 index 000000000..710bcc35e --- /dev/null +++ b/fuzz/corpus/corpus-request-8936987 @@ -0,0 +1,2 @@ +GET / + diff --git a/fuzz/corpus/corpus-request-933110197 b/fuzz/corpus/corpus-request-933110197 new file mode 100644 index 000000000..f7a1302d7 --- /dev/null +++ b/fuzz/corpus/corpus-request-933110197 @@ -0,0 +1,8 @@ +GET /customhdr?hdr=Polo HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Marco: Polo + diff --git a/fuzz/corpus/corpus-request-943947739 b/fuzz/corpus/corpus-request-943947739 new file mode 100644 index 000000000..0e6f9a24f --- /dev/null +++ b/fuzz/corpus/corpus-request-943947739 @@ -0,0 +1,8 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Range: bytes=50-50 + diff --git a/fuzz/corpus/corpus-request-945117276 b/fuzz/corpus/corpus-request-945117276 new file mode 100644 index 000000000..facd1f2f5 --- /dev/null +++ b/fuzz/corpus/corpus-request-945117276 @@ -0,0 +1,7 @@ +HEAD /icons/back.gif HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-959997301 b/fuzz/corpus/corpus-request-959997301 new file mode 100644 index 000000000..103b9081c --- /dev/null +++ b/fuzz/corpus/corpus-request-959997301 @@ -0,0 +1,7 @@ +GET //////////etc/passwd HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-968338082 b/fuzz/corpus/corpus-request-968338082 new file mode 100644 index 000000000..2124602a3 --- /dev/null +++ b/fuzz/corpus/corpus-request-968338082 @@ -0,0 +1,7 @@ +GET /elsewhere HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive + diff --git a/fuzz/corpus/corpus-request-971899228 b/fuzz/corpus/corpus-request-971899228 new file mode 100644 index 000000000..fc1a7faae --- /dev/null +++ b/fuzz/corpus/corpus-request-971899228 @@ -0,0 +1,5 @@ +GET /hello?name=name0003 HTTP/1.1 +Host: localhost +Connection: keep-alive +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7 + diff --git a/fuzz/corpus/corpus-template-1 b/fuzz/corpus/corpus-template-1 new file mode 100644 index 000000000..916980142 --- /dev/null +++ b/fuzz/corpus/corpus-template-1 @@ -0,0 +1,41 @@ + + +{{rel_path?}} Index of {{rel_path}}{{/rel_path?}} +{{^rel_path?}} Index of /{{/rel_path?}} + + + +{{rel_path?}}

Index of {{rel_path}}

{{/rel_path?}} +{{^rel_path?}}

Index of /

{{/rel_path?}} +{{readme?}}
{{readme}}
{{/readme?}} + + + + + + + + + + + +{{#file_list}} + + + + + + +{{/file_list}} +{{^#file_list}} + + + +{{/file_list}} +
Parent directory
 File nameTypeSize
{{file_list.icon_alt}}{{{file_list.name}}}{{file_list.type}}{{file_list.size}}{{file_list.unit}}
Empty directory.
+ + diff --git a/fuzz/disabled/README b/fuzz/disabled/README new file mode 100644 index 000000000..e0a1ce6b7 --- /dev/null +++ b/fuzz/disabled/README @@ -0,0 +1,5 @@ +This directory contains some inputs that crashed Lwan previously, +but for reasons I can't investigate right now, are hanging the +test harness. + +They're still kept here for future investigations. diff --git a/fuzz/disabled/clusterfuzz-testcase-request_fuzzer-5091179189764096 b/fuzz/disabled/clusterfuzz-testcase-request_fuzzer-5091179189764096 new file mode 100644 index 000000000..05a456661 Binary files /dev/null and b/fuzz/disabled/clusterfuzz-testcase-request_fuzzer-5091179189764096 differ diff --git a/fuzz/disabled/clusterfuzz-testcase-request_fuzzer-5689112759107584 b/fuzz/disabled/clusterfuzz-testcase-request_fuzzer-5689112759107584 new file mode 100644 index 000000000..66b75c48d Binary files /dev/null and b/fuzz/disabled/clusterfuzz-testcase-request_fuzzer-5689112759107584 differ diff --git a/fuzz/disabled/crash-61ff81c30aa3c76e78afea62b2e3bd1dfa49e854 b/fuzz/disabled/crash-61ff81c30aa3c76e78afea62b2e3bd1dfa49e854 new file mode 100644 index 000000000..94eec33bc --- /dev/null +++ b/fuzz/disabled/crash-61ff81c30aa3c76e78afea62b2e3bd1dfa49e854 @@ -0,0 +1 @@ +POST \ No newline at end of file diff --git a/fuzz/disabled/crash-96cb33f2b70df83e63d2c6b2e571bd0c17b4adb3 b/fuzz/disabled/crash-96cb33f2b70df83e63d2c6b2e571bd0c17b4adb3 new file mode 100644 index 000000000..57b91a84e --- /dev/null +++ b/fuzz/disabled/crash-96cb33f2b70df83e63d2c6b2e571bd0c17b4adb3 @@ -0,0 +1,2 @@ +� + diff --git a/fuzz/disabled/crash-bd24b5eed9697284d0d67356e1b489b9e2e17ff6 b/fuzz/disabled/crash-bd24b5eed9697284d0d67356e1b489b9e2e17ff6 new file mode 100644 index 000000000..4d6ffbd5c --- /dev/null +++ b/fuzz/disabled/crash-bd24b5eed9697284d0d67356e1b489b9e2e17ff6 @@ -0,0 +1,2 @@ +GET /GEQ2d3a��ޣ0gf59865f0b8aa1727ac%3b%20pa�h%3d%2f%3b%20HXJLU1A0ZTZQ�̏�K1dKcXlpTEF"djJHLzUwbytwSnVpS0xhdFp6NU9kTDhtCmgxamVXMkI0ehttp://%5Bun2ix%A3%Ft-rep9Nck1rMStLUmV0TEdUe3b%20patxMVUE:PS5tQUxSWU5n\m8aa1a27 3%b7%20path%3d%2f%3b%20HXJLU1A0ZTZQc3pSEF2djJHLzUwbytwSnVpS0xhdFp27ac%3b%20path%3d%2f�̝��Ϸ�tpnl yOHTTP/1.1 +hostBkAAQAAP%2Ftmp%2F5q-url=http://%5Bun2i5B \ No newline at end of file diff --git a/fuzz/mayhem/Dockerfile b/fuzz/mayhem/Dockerfile new file mode 100644 index 000000000..d28dad6a0 --- /dev/null +++ b/fuzz/mayhem/Dockerfile @@ -0,0 +1,28 @@ +# Copyright 2019 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +FROM gcr.io/oss-fuzz-base/base-builder +RUN apt-get update +RUN apt-get install -y build-essential cmake git ninja-build zlib1g-dev + +COPY . lwan +RUN rm -rf lwan/mayhem +WORKDIR lwan + +COPY fuzz/mayhem/build.sh $SRC/ + +ENV FUZZING_LANGUAGE=c++ SANITIZER=address +RUN compile \ No newline at end of file diff --git a/fuzz/mayhem/Dockerfile.dockerignore b/fuzz/mayhem/Dockerfile.dockerignore new file mode 100644 index 000000000..e69de29bb diff --git a/fuzz/mayhem/build.sh b/fuzz/mayhem/build.sh new file mode 100644 index 000000000..bcdd36887 --- /dev/null +++ b/fuzz/mayhem/build.sh @@ -0,0 +1,43 @@ +#!/bin/bash -euv +# Copyright 2019 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + + +mkdir -p $WORK/lwan +cd $WORK/lwan + +cmake -GNinja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER="${CC}" \ + -DCMAKE_C_FLAGS="${CFLAGS}" \ + $SRC/lwan/ + +ninja -v liblwan.a + +cp $SRC/lwan/fuzz/*.dict $OUT/ + +for fuzzer in $SRC/lwan/src/bin/fuzz/*_fuzzer.cc; do + executable=$(basename $fuzzer .cc) + corpus_base=$(basename $fuzzer _fuzzer.cc) + + zip -jr $OUT/${executable}_seed_corpus.zip $SRC/lwan/fuzz/corpus/corpus-${corpus_base}-* + + $CXX $CXXFLAGS -std=c++11 \ + -Wl,-whole-archive $WORK/lwan/src/lib/liblwan.a -Wl,-no-whole-archive \ + -I$SRC/lwan/src/lib $fuzzer \ + $LIB_FUZZING_ENGINE -lpthread -lz \ + -o $OUT/$executable +done diff --git a/fuzz/mayhem/config_fuzzer.mayhemfile b/fuzz/mayhem/config_fuzzer.mayhemfile new file mode 100644 index 000000000..9957e53cf --- /dev/null +++ b/fuzz/mayhem/config_fuzzer.mayhemfile @@ -0,0 +1,4 @@ +project: PROJECT +target: config_fuzzer +cmds: +- cmd: /out/config_fuzzer diff --git a/fuzz/mayhem/h2_huffman_fuzzer.mayhemfile b/fuzz/mayhem/h2_huffman_fuzzer.mayhemfile new file mode 100644 index 000000000..6da25af45 --- /dev/null +++ b/fuzz/mayhem/h2_huffman_fuzzer.mayhemfile @@ -0,0 +1,4 @@ +project: PROJECT +target: h2_huffman_fuzzer +cmds: +- cmd: /out/h2_huffman_fuzzer diff --git a/fuzz/mayhem/pattern_fuzzer.mayhemfile b/fuzz/mayhem/pattern_fuzzer.mayhemfile new file mode 100644 index 000000000..a3f1bc479 --- /dev/null +++ b/fuzz/mayhem/pattern_fuzzer.mayhemfile @@ -0,0 +1,4 @@ +project: PROJECT +target: pattern_fuzzer +cmds: +- cmd: /out/pattern_fuzzer diff --git a/fuzz/mayhem/request_fuzzer.mayhemfile b/fuzz/mayhem/request_fuzzer.mayhemfile new file mode 100644 index 000000000..9735c85e7 --- /dev/null +++ b/fuzz/mayhem/request_fuzzer.mayhemfile @@ -0,0 +1,4 @@ +project: PROJECT +target: request_fuzzer +cmds: +- cmd: /out/request_fuzzer diff --git a/fuzz/mayhem/template_fuzzer.mayhemfile b/fuzz/mayhem/template_fuzzer.mayhemfile new file mode 100644 index 000000000..f96749df8 --- /dev/null +++ b/fuzz/mayhem/template_fuzzer.mayhemfile @@ -0,0 +1,4 @@ +project: PROJECT +target: template_fuzzer +cmds: +- cmd: /out/template_fuzzer diff --git a/fuzz/regression/README b/fuzz/regression/README new file mode 100644 index 000000000..84880c04d --- /dev/null +++ b/fuzz/regression/README @@ -0,0 +1,4 @@ +This directory contains files that managed to crash something in Lwan +after being fuzzed. They have been committed alongside the fix, so +it's easier to find them with git blame. + diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-config_fuzzer-5462763322277888 b/fuzz/regression/clusterfuzz-testcase-minimized-config_fuzzer-5462763322277888 new file mode 100644 index 000000000..304fbd5fc --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-config_fuzzer-5462763322277888 @@ -0,0 +1,233 @@ +$=$�=$=$=$=$=$=$=$=$=$=$=$=$=$��=$=$=$$==$=$=$=$=$=$=$=$=$=$=$=$=$=$=$=$=$=$=$=$=$��=$=$=$=$=$=$=$=$=$=""$=$=$=$=$=$=$=$=$off +!=off +' ] +���" := +"��������������" := +"������=" +constants{M=Z=M""$"������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:}${M:}${M:}${M:M:}Z=M${M:}${I:}${M:}$$=$=$=$=$=$=$=""$"����������������ʘ�����������dd4" ���˜���$=$=$=$=$=$=$=$=$=$=$=$=$=$�������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:{{M:}${M:}${M:M:}Z=M${M:}${I:�dd4" ���˜���$=$=$=$=$T$=$=$=$=$=$=$=$=$��=$=$=" +" +" +" +" +" +" + +"�" +" +" +" +" +" +" +" +" +" +" +" +" +" + +" +" +" +" +" +" +" +" +" +" +" +" +" +" +" +" +" +" + +" +" +" +" +" +" +" +" +" +" +" +" +" +" +" +" +" +" + +"����������" := +"������=" +constants{M=Z=M""$"������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:}${M:}${M:}${M:M:}Z=M${M:}${I:}${M:}$$=$=$=$=$=$=$=""$"��=$=$=$=$=$=$=$=$=$=$=$=$=$�������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:{{M:}${M:}${M:M:}Z=M${M:}${I:�dd4" ���˜���$=$=$=$=$T$=$=$=$=$=$=$=$=$��=$=$=" +" +" +" +" + +" +" +" + +" +" +" +" +" +"$=$=$=$�������=$constan�s$=$=$=$=$=$=$��=$=$=$=$=$=$=$B$=$=""$=$=$=�=$=$" +" +" +" +" +" +" +" +" +" +" +" +" +" + +"����������" := +"������=" +constants{M=Z=M""$"������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:}${M:}${M:}${M:M:}Z=M${M:}${I:}${M:}$$=$=$=$=$=$=$=""$"��=$=$=$=$=$=$=$=$=$=$=$=$=$�������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:{{M:}${M:}${M:M:}Z=M${M:}${I:�dd4" ���˜���$=$=$=$=$T$=$=$=$=$=$=$=$=$��=$=$=" +" +" +" +" + +" +" +" + +" +" +" +" +" +"$=$=$=$�������=$constan:}${M:}${M:}${M:}${M:M:}Z=M${M:}${I:}${M:}$$=$=$=$=$=$=$=""$"����������������ʘ�����������dd4" ���˜���$=$=$=$=$=$=$=$=$=$=$=$=$=$�������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:{{M:}${M:}${M:M:}Z=M${M:}${I:�dd4" ���˜���$=$=$=$=$T$=$=$=$=$=$=$=$=$��=$=$=" +" +" +" +" + +" +" +" + +" +" +" +" +" +"$=$=$=$�������=$constan:}${M:}${M:}${M:}${M:M:}Z=M${M:}${I:}${M:}$$=$=$=$=$=$=$=""$"����������������ʘ�����������dd4" ���˜���$=$=$=$=$=$=$=$=$=$=$=$=$=$�������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:{{M:}${M:}${M:M:}Z=M${M:}${I:�dd4" ���˜���$=$=$=$=$T$=$=$=$=$=$=$=$=$��=$=$=" +" +" +" +" + +" +" +" + +" +" +" +" +" +"$=$=$=$�������=$constan:}${M:}${M:}${M:}${M:M:}Z=M${M:}${I:}${M:}$$=$=$=$=$=$=$=""$"����������������ʘ�����������dd4" ���˜���$=$=$=$=$=$=$=$=$=$=$=$=$=$�������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:{{M:}${M:}${M:M:}Z=M${M:}${I:�dd4" ���˜���$=$=$=$=$T$=$=$=$=$=$=$=$=$��=$=$=" +" +" +" +" + +" +" +" + +" +" +" +" +" +"$=$=$=$�������=$constan:}${M:}${M:}${M:}${M:M:}Z=M${M:}${I:}${M:}$$=$=$=$=$=$=$=""$"����������������ʘ�����������dd4" ���˜���$=$=$=$=$=$=$=$=$=$=$=$=$=$�������������� := +"������=" +constants{M=]=${M:}${M:.}${M:}${I:}${M:}${N:}${M:}${M:}${M:{{M:}${M:}${M:M:}Z=M${M:}${I:�dd4" ���˜���$=$=$=$=$T$=$=$=$=$=$=$=$=$��=$=$=" +" +" +" +" +" +" + +"�" +" +" +" +" +" +" +" +" +" +" +" +" +" + +" +" +" +" +" +" +" +" +" +" +" +�s$=$=$=$=$=$=$��=$=$=$=$=$=$=$B$=$=""$=$=$=�=$=$=$=$=$=$=$B$=$=""$=$=$=$=$=$�$=$=$=off +(=off +(=of{M:}$����+Z=M""$"�������������" : +" +" +" +" +" +" +" +" +" +" +" +"= +"������*=" + +co \ No newline at end of file diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-h2_huffman_fuzzer-4703583657918464 b/fuzz/regression/clusterfuzz-testcase-minimized-h2_huffman_fuzzer-4703583657918464 new file mode 100644 index 000000000..790cc272b --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-h2_huffman_fuzzer-4703583657918464 @@ -0,0 +1 @@ + � \ No newline at end of file diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-4811721351954432 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-4811721351954432 new file mode 100644 index 000000000..061489462 --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-4811721351954432 @@ -0,0 +1,2 @@ +GET / HTTP/1.0 Cookie: N + diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5191405204406272 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5191405204406272 new file mode 100644 index 000000000..e90dd6a1e --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5191405204406272 @@ -0,0 +1,2 @@ +GET /%64T /?%%��HTTP/1.1 + diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5636210766118912 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5636210766118912 new file mode 100644 index 000000000..db04e06c3 --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5636210766118912 @@ -0,0 +1,3 @@ +GET / HTTP/1.1 + + \ No newline at end of file diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5649134389821440 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5649134389821440 new file mode 100644 index 000000000..ffa011b34 --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5649134389821440 @@ -0,0 +1,3 @@ +GET / � � ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������&/T&R&/1.&T?&�?&�&T /&�&T /?& GE??&�?&�4T.& GE/?&�?&�&T & GE/?&�?&�&T /& /?&�&T&�?&y&T /&H?& /?&?& GE&�&RH?&/?&/TDR&HTTP/1.0 l"l" + + i \ No newline at end of file diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5652010562486272 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5652010562486272 new file mode 100644 index 000000000..03e0bd4d3 --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5652010562486272 @@ -0,0 +1,3 @@ + + +GET / HTTP/1.0 Rang : \ No newline at end of file diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5658117347475456 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5658117347475456 new file mode 100644 index 000000000..15e03c769 --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5658117347475456 @@ -0,0 +1,41 @@ +PROX + �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� + +�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + +�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������װ�� + diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5717480481226752 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5717480481226752 new file mode 100644 index 000000000..a8fdd1492 Binary files /dev/null and b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5717480481226752 differ diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5723694463844352 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5723694463844352 new file mode 100644 index 000000000..816cc8220 --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5723694463844352 @@ -0,0 +1,2 @@ +GET /ws-write HTTP/1.1 Conn : Upg Upgrade: websocket Sec-WebSocket-Key: + diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5729298679332864 b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5729298679332864 new file mode 100644 index 000000000..e234374b7 Binary files /dev/null and b/fuzz/regression/clusterfuzz-testcase-minimized-request_fuzzer-5729298679332864 differ diff --git a/fuzz/regression/clusterfuzz-testcase-minimized-template_fuzzer-6208074244227072 b/fuzz/regression/clusterfuzz-testcase-minimized-template_fuzzer-6208074244227072 new file mode 100644 index 000000000..1a8fa700a --- /dev/null +++ b/fuzz/regression/clusterfuzz-testcase-minimized-template_fuzzer-6208074244227072 @@ -0,0 +1 @@ +{{#readme \ No newline at end of file diff --git a/fuzz/regression/clusterfuzz-testcase-request_fuzzer-5675545829834752 b/fuzz/regression/clusterfuzz-testcase-request_fuzzer-5675545829834752 new file mode 100644 index 000000000..9baabfa25 Binary files /dev/null and b/fuzz/regression/clusterfuzz-testcase-request_fuzzer-5675545829834752 differ diff --git a/fuzz/regression/request_fuzzer-crash-9df31650ba7cf7fa3aae24d59b3e0a335ab6c23a b/fuzz/regression/request_fuzzer-crash-9df31650ba7cf7fa3aae24d59b3e0a335ab6c23a new file mode 100644 index 000000000..a43e840ea --- /dev/null +++ b/fuzz/regression/request_fuzzer-crash-9df31650ba7cf7fa3aae24d59b3e0a335ab6c23a @@ -0,0 +1,8 @@ +GET /zero HTTP/1.1 +Host: 127.0.0.1:8080 +User-Agent: python-requests/2.21.0 +Accept-Encoding: gzip, deflate +Accept: */* +Connection: keep-alive +Range: bytes=50- + diff --git a/fuzz/request_fuzzer.dict b/fuzz/request_fuzzer.dict new file mode 100644 index 000000000..284a82e5f --- /dev/null +++ b/fuzz/request_fuzzer.dict @@ -0,0 +1,128 @@ +POST="POST" +GET="GET" +FOO="FOO" +post="post" +get="post" +foo="foo" +slash="/" +url="http://foo/" +version="HTTP/1.1" +content_length="Content-Length" +transfer_encoding="Transfer-Encoding" +text="text" +semicolon=";" +comma="," +hdr1="Header" +hdr2="Hea-Der" +colon=":" +minus="-" +zero="0" +one="1" +minus_one="-1" +small_size="123" +small_positive_size="+123" +small_negative_size="-123" +medium_size="12345" +medium_positive_size="+12345" +medium_negative_size="-12345" +large_size="999999999" +large_positive_size="+999999999" +large_negative_size="-999999999" +float_size="123.456" +chunked="chunked" +gzip="gzip" +nul="\x00" +bs="\x08" +ht="\x09" +nl="\x0A" +vt="\x0B" +np="\x0C" +cr="\x0D" +crlf="\x0D\x0A" +space="\x20" +del="\x7F" +hi="\x80" +ff="\xFF" +curl="HTTP/1.0" +curl="100" +curl="200" +curl="301" +curl="400" +curl="Server:" +curl="Last-Modified:" +curl="Content-Type:" +curl="text/html" +curl="charset=UTF-8" +curl="Accept-Ranges:" +curl="bytes" +curl="Content-Length:" +curl="Transfer-Encoding:" +curl="compress" +curl="exi" +curl="gzip" +curl="identity" +curl="pack200-gzip" +curl="br" +curl="deflate" +curl="bzip2" +curl="lzma" +curl="xz" +curl="Content-Encoding:" +curl="chunked" +curl="Connection:" +curl="close" +curl="Date:" +curl="Expires:" +curl="Fri, 31 Dec 1999 23:59:59 GMT" +curl="Cache-Control:" +curl="no-cache" +curl="no-store" +curl="must-revalidate" +curl="Pragma:" +curl="no-cache" +curl="Host:" +vdf="Accept" +vdf="Accept-Charset" +vdf="Accept-Encoding" +vdf="Accept-Language" +vdf="Accept-Datetime" +vdf="Authorization" +vdf="Cache-Control" +vdf="Connection" +vdf="Cookie" +vdf="Content-Length" +vdf="Content-MD5" +vdf="Content-Type" +vdf="Date" +vdf="Expect" +vdf="Forwarded" +vdf="From" +vdf="Host" +vdf="If-Match" +vdf="If-Modified-Since" +vdf="If-None-Match" +vdf="If-Range" +vdf="If-Unmodified-Since" +vdf="Max-Forwards" +vdf="Origin" +vdf="Proxy-Authorization" +vdf="Range" +vdf="TE" +vdf="User-Agent" +vdf="Upgrade" +vdf="Via" +vdf="Warning" +vdf="X-Requested-With" +vdf="X-Forwarded-Host" +vdf="X-Forwarded-Host" +vdf="X-Forwarded-Proto" +vdf="Front-End-Https" +vdf="X-HTTP-Method-Override" +vdf="X-Att-Deviceid" +vdf="x-wap-profile" +vdf="Proxy-Connection" +vdf="X-UIDH" +vdf="X-XSRF-TOKEN" +vdf="X-Csrf-Token" +vdf="Sec-WebSocket-Key" +vdf="Upgrade" diff --git a/lwan.conf b/lwan.conf index 2637af8f7..85a4e3236 100644 --- a/lwan.conf +++ b/lwan.conf @@ -1,50 +1,37 @@ +# Timeout in seconds to keep a connection alive. keep_alive_timeout = 15 + +# Set to true to not print any debugging messages. (Only effective in +# release builds.) quiet = false -reuse_port = false + +# Value of "Expires" header. Default is 1 month and 1 week. expires = 1M 1w + +# Number of I/O threads. Default (0) is number of online CPUs. threads = 0 -listener *:8080 { - prefix /hello { - handler = hello_world - } - prefix /chunked { - handler = test_chunked_encoding - } - prefix /sse { - handler = test_server_sent_event - } - prefix /beacon { - handler = gif_beacon - } - prefix /favicon.ico { - handler = gif_beacon - } - redirect /elsewhere { - to = http://lwan.ws - } - prefix /admin { - handler = hello_world - authorization basic { - realm = Administration Page - password file = htpasswd - } - } - lua /lua { - default type = text/html - script file = test.lua - cache period = 30s - } - rewrite /pattern { - pattern foo/(%d+)(%a)(%d+) { - redirect to = /hello?name=pre%2middle%3othermiddle%1post - } - pattern bar/(%d+)/test { - rewrite as = /hello?name=rewritten%1 - } - } +# Disable HAProxy's PROXY protocol by default. Only enable if needed. +proxy_protocol = false + +# Enable straitjacket by default. The `drop_capabilities` option is `true` +# by default. Other options may require more privileges. +straitjacket + +listener *:8080 + +#tls_listener *:8081 { +# cert = /path/to/cert.pem +# key = /path/to/key.pem +#} + +site { serve_files / { path = ./wwwroot + + # When requesting for file.ext, look for a smaller/newer file.ext.gz, + # and serve that instead if `Accept-Encoding: gzip` is in the + # request headers. serve precompressed files = true } } diff --git a/lwan/CMakeLists.txt b/lwan/CMakeLists.txt deleted file mode 100644 index 9dbbaef06..000000000 --- a/lwan/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -add_executable(lwan main.c) - -target_link_libraries(lwan - -Wl,-whole-archive lwan-common -Wl,-no-whole-archive - dl - ${ADDITIONAL_LIBRARIES} -) - diff --git a/lwan/main.c b/lwan/main.c deleted file mode 100644 index 4286ca546..000000000 --- a/lwan/main.c +++ /dev/null @@ -1,239 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#define _GNU_SOURCE -#include -#include -#include - -#include "lwan.h" -#include "lwan-serve-files.h" - -enum args { - ARGS_FAILED, - ARGS_USE_CONFIG, - ARGS_SERVE_FILES -}; - -lwan_http_status_t -gif_beacon(lwan_request_t *request __attribute__((unused)), - lwan_response_t *response, - void *data __attribute__((unused))) -{ - /* - * 1x1 transparent GIF image generated with tinygif - * http://www.perlmonks.org/?node_id=7974 - */ - static const unsigned char gif_beacon_data[] = { - 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x90, - 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, - 0x05, 0x10, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x04, 0x01 - }; - - response->mime_type = "image/gif"; - strbuf_set_static(response->buffer, (char*)gif_beacon_data, sizeof(gif_beacon_data)); - - return HTTP_OK; -} - -lwan_http_status_t -test_chunked_encoding(lwan_request_t *request, - lwan_response_t *response, - void *data __attribute__((unused))) -{ - int i; - - response->mime_type = "text/plain"; - - strbuf_printf(response->buffer, "Testing chunked encoding! First chunk\n"); - lwan_response_send_chunk(request); - - for (i = 0; i <= 10; i++) { - strbuf_printf(response->buffer, "*This is chunk %d*\n", i); - lwan_response_send_chunk(request); - } - - strbuf_printf(response->buffer, "Last chunk\n"); - lwan_response_send_chunk(request); - - return HTTP_OK; -} - -lwan_http_status_t -test_server_sent_event(lwan_request_t *request, - lwan_response_t *response, - void *data __attribute__((unused))) -{ - int i; - - for (i = 0; i <= 10; i++) { - strbuf_printf(response->buffer, "Current value is %d", i); - lwan_response_send_event(request, "currval"); - } - - return HTTP_OK; -} - -lwan_http_status_t -hello_world(lwan_request_t *request, - lwan_response_t *response, - void *data __attribute__((unused))) -{ - static lwan_key_value_t headers[] = { - { .key = "X-The-Answer-To-The-Universal-Question", .value = "42" }, - { NULL, NULL } - }; - response->headers = headers; - response->mime_type = "text/plain"; - - const char *name = lwan_request_get_query_param(request, "name"); - if (name) - strbuf_printf(response->buffer, "Hello, %s!", name); - else - strbuf_set_static(response->buffer, "Hello, world!", sizeof("Hello, world!") -1); - - const char *dump_vars = lwan_request_get_query_param(request, "dump_vars"); - if (!dump_vars) - goto end; - - if (request->cookies.base) { - strbuf_append_str(response->buffer, "\n\nCookies\n", 0); - strbuf_append_str(response->buffer, "-------\n\n", 0); - - lwan_key_value_t *qs = request->cookies.base; - for (; qs->key; qs++) - strbuf_append_printf(response->buffer, - "Key = \"%s\"; Value = \"%s\"\n", qs->key, qs->value); - } - - strbuf_append_str(response->buffer, "\n\nQuery String Variables\n", 0); - strbuf_append_str(response->buffer, "----------------------\n\n", 0); - - lwan_key_value_t *qs = request->query_params.base; - for (; qs->key; qs++) - strbuf_append_printf(response->buffer, - "Key = \"%s\"; Value = \"%s\"\n", qs->key, qs->value); - - if (!(request->flags & REQUEST_METHOD_POST)) - goto end; - - strbuf_append_str(response->buffer, "\n\nPOST data\n", 0); - strbuf_append_str(response->buffer, "---------\n\n", 0); - - for (qs = request->post_data.base; qs->key; qs++) - strbuf_append_printf(response->buffer, - "Key = \"%s\"; Value = \"%s\"\n", qs->key, qs->value); - -end: - return HTTP_OK; -} - -static enum args -parse_args(int argc, char *argv[], lwan_config_t *config, char **root) -{ - static const struct option opts[] = { - { .name = "root", .has_arg = 1, .val = 'r' }, - { .name = "listen", .has_arg = 1, .val = 'l' }, - { .name = "help", .val = 'h' }, - { } - }; - int c, optidx = 0; - enum args result = ARGS_USE_CONFIG; - - while ((c = getopt_long(argc, argv, "hr:l:", opts, &optidx)) != -1) { - switch (c) { - case 'l': - free(config->listener); - config->listener = strdup(optarg); - result = ARGS_SERVE_FILES; - break; - - case 'r': - free(*root); - *root = strdup(optarg); - result = ARGS_SERVE_FILES; - break; - - default: - printf("Run %s --help for usage information.\n", argv[0]); - return ARGS_FAILED; - - case 'h': - printf("Usage: %s [--root /path/to/root/dir] [--listener addr:port]\n", argv[0]); - printf("\t[--config]\n"); - printf("Serve files through HTTP.\n\n"); - printf("Defaults to listening on all interfaces, port 8080, serving current directory.\n\n"); - printf("Options:\n"); - printf("\t-r, --root Path to serve files from (default: current dir).\n"); - printf("\t-l, --listener Listener (default: %s).\n", config->listener); - printf("\t-h, --help This.\n"); - printf("\n"); - printf("Examples:\n"); - printf(" Serve system-wide documentation: %s -r /usr/share/doc\n", argv[0]); - printf(" Serve on a different port: %s -l '*:1337'\n", argv[0]); - printf("\n"); - printf("Report bugs at .\n"); - return ARGS_FAILED; - } - } - - return result; -} - -int -main(int argc, char *argv[]) -{ - lwan_t l; - lwan_config_t c; - char *root = get_current_dir_name(); - int exit_status = EXIT_SUCCESS; - - c = *lwan_get_default_config(); - c.listener = strdup("*:8080"); - - switch (parse_args(argc, argv, &c, &root)) { - case ARGS_SERVE_FILES: - lwan_status_info("Serving files from %s", root); - lwan_init_with_config(&l, &c); - - const lwan_url_map_t map[] = { - { .prefix = "/", SERVE_FILES(root) }, - { } - }; - lwan_set_url_map(&l, map); - break; - case ARGS_USE_CONFIG: - lwan_init(&l); - break; - case ARGS_FAILED: - exit_status = EXIT_FAILURE; - goto out; - } - - lwan_main_loop(&l); - lwan_shutdown(&l); - -out: - - free(root); - free(c.listener); - - return exit_status; -} diff --git a/src/3rdparty/libucontext/LICENSE b/src/3rdparty/libucontext/LICENSE new file mode 100644 index 000000000..9fbc5d306 --- /dev/null +++ b/src/3rdparty/libucontext/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2018-2022 Ariadne Conill + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +This software is provided 'as is' and without any warranty, express or +implied. In no event shall the authors be liable for any damages arising +from the use of this software. diff --git a/src/3rdparty/libucontext/Makefile b/src/3rdparty/libucontext/Makefile new file mode 100644 index 000000000..587729543 --- /dev/null +++ b/src/3rdparty/libucontext/Makefile @@ -0,0 +1,292 @@ +SCDOC := scdoc + +ARCH := $(shell uname -m) +ifeq ($(ARCH),$(filter $(ARCH),i386 i686)) + override ARCH = x86 +endif +ifeq ($(ARCH),$(filter $(ARCH),sh2 sh4)) + override ARCH = sh +endif +ifeq ($(ARCH),$(filter $(ARCH),ppc64le)) + override ARCH = ppc64 +endif +ifeq ($(ARCH),$(filter $(ARCH),armv7l)) + override ARCH = arm +endif +ifeq ($(ARCH),$(filter $(ARCH),arm64)) + override ARCH = aarch64 +endif + +prefix = /usr +libdir = ${prefix}/lib +shared_libdir = ${libdir} +static_libdir = ${libdir} +includedir = ${prefix}/include +pkgconfigdir = ${prefix}/lib/pkgconfig + +CFLAGS ?= -ggdb3 -O2 -Wall +CPPFLAGS := -Iinclude -Iarch/${ARCH} -Iarch/common +ifneq ($(shell uname),Darwin) + EXPORT_UNPREFIXED := yes +else + # Darwin does not support aliases + EXPORT_UNPREFIXED := no +endif +FREESTANDING := no + +ifeq ($(FREESTANDING),yes) + CPPFLAGS += -DFREESTANDING + EXPORT_UNPREFIXED = no +endif + +FORCE_SOFT_FLOAT := no + +ifeq ($(FORCE_SOFT_FLOAT),yes) + CPPFLAGS += -DFORCE_SOFT_FLOAT +endif + +FORCE_HARD_FLOAT := no + +ifeq ($(FORCE_HARD_FLOAT),yes) + CPPFLAGS += -DFORCE_HARD_FLOAT +endif + +ifeq ($(EXPORT_UNPREFIXED),yes) + CPPFLAGS += -DEXPORT_UNPREFIXED +endif + +LIBUCONTEXT_C_SRC = $(wildcard arch/${ARCH}/*.c) +LIBUCONTEXT_S_SRC = $(wildcard arch/${ARCH}/*.S) + +ifeq ($(shell test -d arch/${ARCH}/include; echo $?),0) + CPPFLAGS += -Iarch/${ARCH}/include +endif + +LIBUCONTEXT_VERSION := $(shell head -n 1 VERSION) +LIBUCONTEXT_OBJ = ${LIBUCONTEXT_C_SRC:.c=.o} ${LIBUCONTEXT_S_SRC:.S=.o} +LIBUCONTEXT_SOVERSION = 1 +ifeq ($(shell uname),Darwin) + LIBUCONTEXT_NAME = libucontext.dylib + LIBUCONTEXT_SONAME = libucontext.${LIBUCONTEXT_SOVERSION}.dylib + LIBUCONTEXT_POSIX_NAME = libucontext_posix.dylib + LIBUCONTEXT_POSIX_SONAME = libucontext_posix.${LIBUCONTEXT_SOVERSION}.dylib + LIBUCONTEXT_LINKER_FLAGS = -Wl,-dynamiclib,-install_name,${LIBUCONTEXT_SONAME},-current_version,${LIBUCONTEXT_SOVERSION},-compatibility_version,${LIBUCONTEXT_SOVERSION} + LIBUCONTEXT_POSIX_LINKER_FLAGS = -Wl,-dynamiclib,-install_name,${LIBUCONTEXT_POSIX_SONAME},-current_version,${LIBUCONTEXT_SOVERSION},-compatibility_version,${LIBUCONTEXT_SOVERSION} +else + LIBUCONTEXT_NAME = libucontext.so + LIBUCONTEXT_SONAME = libucontext.so.${LIBUCONTEXT_SOVERSION} + LIBUCONTEXT_POSIX_NAME = libucontext_posix.so + LIBUCONTEXT_POSIX_SONAME = libucontext_posix.so.${LIBUCONTEXT_SOVERSION} + LIBUCONTEXT_LINKER_FLAGS = -shared -Wl,-soname,${LIBUCONTEXT_SONAME} -Wl,-z,noexecstack + LIBUCONTEXT_POSIX_LINKER_FLAGS = -shared -Wl,-soname,${LIBUCONTEXT_POSIX_SONAME} -Wl,-z,noexecstack + ASFLAGS = -Wa,--noexecstack +endif +LIBUCONTEXT_STATIC_NAME = libucontext.a +LIBUCONTEXT_PC = libucontext.pc +LIBUCONTEXT_PATH = ${shared_libdir}/${LIBUCONTEXT_SONAME} +LIBUCONTEXT_STATIC_PATH = ${static_libdir}/${LIBUCONTEXT_STATIC_NAME} +LIBUCONTEXT_HEADERS = \ + include/libucontext/libucontext.h \ + include/libucontext/bits.h +LIBUCONTEXT_EXAMPLES = \ + examples/cooperative_threading +LIBUCONTEXT_POSIX_STATIC_NAME = libucontext_posix.a +LIBUCONTEXT_POSIX_C_SRC = libucontext_posix.c +LIBUCONTEXT_POSIX_OBJ = ${LIBUCONTEXT_POSIX_C_SRC:.c=.o} +LIBUCONTEXT_POSIX_PATH = ${shared_libdir}/${LIBUCONTEXT_POSIX_SONAME} +LIBUCONTEXT_POSIX_STATIC_PATH = ${static_libdir}/${LIBUCONTEXT_POSIX_STATIC_NAME} + +ifeq ($(FREESTANDING),yes) + LIBUCONTEXT_POSIX_NAME = + LIBUCONTEXT_POSIX_STATIC_NAME = +endif + +all: ${LIBUCONTEXT_SONAME} ${LIBUCONTEXT_STATIC_NAME} ${LIBUCONTEXT_POSIX_NAME} ${LIBUCONTEXT_POSIX_STATIC_NAME} ${LIBUCONTEXT_PC} + +${LIBUCONTEXT_POSIX_NAME}: ${LIBUCONTEXT_NAME} ${LIBUCONTEXT_POSIX_OBJ} + $(CC) -fPIC -o ${LIBUCONTEXT_POSIX_NAME} ${LIBUCONTEXT_POSIX_LINKER_FLAGS} ${LIBUCONTEXT_POSIX_OBJ} -L. -lucontext ${LDFLAGS} + +${LIBUCONTEXT_POSIX_STATIC_NAME}: ${LIBUCONTEXT_STATIC_NAME} ${LIBUCONTEXT_POSIX_OBJ} + $(AR) rcs ${LIBUCONTEXT_POSIX_STATIC_NAME} ${LIBUCONTEXT_POSIX_OBJ} + +${LIBUCONTEXT_POSIX_SONAME}: ${LIBUCONTEXT_POSIX_NAME} + ln -sf ${LIBUCONTEXT_POSIX_NAME} ${LIBUCONTEXT_POSIX_SONAME} + +${LIBUCONTEXT_STATIC_NAME}: ${LIBUCONTEXT_HEADERS} ${LIBUCONTEXT_OBJ} + $(AR) rcs ${LIBUCONTEXT_STATIC_NAME} ${LIBUCONTEXT_OBJ} + +${LIBUCONTEXT_NAME}: ${LIBUCONTEXT_HEADERS} ${LIBUCONTEXT_OBJ} + $(CC) -fPIC -o ${LIBUCONTEXT_NAME} ${LIBUCONTEXT_LINKER_FLAGS} ${LIBUCONTEXT_OBJ} ${LDFLAGS} + +${LIBUCONTEXT_SONAME}: ${LIBUCONTEXT_NAME} + ln -sf ${LIBUCONTEXT_NAME} ${LIBUCONTEXT_SONAME} + +${LIBUCONTEXT_PC}: libucontext.pc.in + sed -e s:@LIBUCONTEXT_VERSION@:${LIBUCONTEXT_VERSION}:g \ + -e s:@LIBUCONTEXT_SHARED_LIBDIR@:${shared_libdir}:g \ + -e s:@LIBUCONTEXT_STATIC_LIBDIR@:${static_libdir}:g \ + -e s:@LIBUCONTEXT_INCLUDEDIR@:${includedir}:g $< > $@ + +MANPAGES_SYMLINKS_3 = \ + libucontext_getcontext.3 \ + libucontext_makecontext.3 \ + libucontext_setcontext.3 \ + libucontext_swapcontext.3 +MANPAGES_3 = doc/libucontext.3 + +MANPAGES = ${MANPAGES_3} + +.scd.3: + ${SCDOC} < $< > $@ + +.SUFFIXES: .scd .3 + +docs: ${MANPAGES} + +.c.o: + $(CC) -std=gnu99 -D_DEFAULT_SOURCE -fPIC -DPIC ${CFLAGS} ${CPPFLAGS} -c -o $@ $< + +.S.o: + $(CC) -fPIC -DPIC ${CFLAGS} ${CPPFLAGS} ${ASFLAGS} -c -o $@ $< + +${LIBUCONTEXT_NAME}_clean: + rm -f ${LIBUCONTEXT_NAME} + +${LIBUCONTEXT_SONAME}_clean: + rm -f ${LIBUCONTEXT_SONAME} + +${LIBUCONTEXT_STATIC_NAME}_clean: + rm -f ${LIBUCONTEXT_STATIC_NAME} + +libucontext_obj_clean: + rm -f ${LIBUCONTEXT_OBJ} + +${LIBUCONTEXT_PC}_clean: + rm -f ${LIBUCONTEXT_PC} + +bits_clean: + rm -f include/libucontext/bits.h + +${LIBUCONTEXT_POSIX_NAME}_clean: + rm -f ${LIBUCONTEXT_POSIX_NAME} + +${LIBUCONTEXT_POSIX_SONAME}_clean: + rm -f ${LIBUCONTEXT_POSIX_SONAME} + +${LIBUCONTEXT_POSIX_STATIC_NAME}_clean: + rm -f ${LIBUCONTEXT_POSIX_STATIC_NAME} + +libucontext_posix_obj_clean: + rm -f ${LIBUCONTEXT_POSIX_OBJ} + +check_clean: check_bare_clean check_posix_clean check_bare_posixabi_clean + +check_bare_clean: + rm -f test_libucontext + +check_posix_clean: + rm -f test_libucontext_posix + +check_bare_posixabi_clean: + rm -f test_libucontext_bare_posixabi + +docs_clean: + rm -f ${MANPAGES} + +clean: ${LIBUCONTEXT_NAME}_clean +clean: ${LIBUCONTEXT_SONAME}_clean +clean: ${LIBUCONTEXT_STATIC_NAME}_clean +clean: ${LIBUCONTEXT_PC}_clean +clean: bits_clean +clean: ${LIBUCONTEXT_POSIX_NAME}_clean +clean: ${LIBUCONTEXT_POSIX_SONAME}_clean +clean: ${LIBUCONTEXT_POSIX_STATIC_NAME}_clean +clean: libucontext_posix_obj_clean +clean: libucontext_obj_clean +clean: check_clean +clean: docs_clean + +install-shared: ${LIBUCONTEXT_SONAME} + install -D -m755 ${LIBUCONTEXT_NAME} ${DESTDIR}${LIBUCONTEXT_PATH} + ln -sf ${LIBUCONTEXT_SONAME} ${DESTDIR}${shared_libdir}/${LIBUCONTEXT_NAME} + +install-static: ${LIBUCONTEXT_STATIC_NAME} + install -D -m664 ${LIBUCONTEXT_STATIC_NAME} ${DESTDIR}${LIBUCONTEXT_STATIC_PATH} + +install-pkgconf: + install -D -m644 ${LIBUCONTEXT_PC} ${DESTDIR}${pkgconfigdir}/${LIBUCONTEXT_PC} + if [ -n "${LIBUCONTEXT_POSIX_NAME}" ]; then \ + install -D -m755 ${LIBUCONTEXT_POSIX_NAME} ${DESTDIR}${LIBUCONTEXT_POSIX_PATH}; \ + install -D -m644 ${LIBUCONTEXT_POSIX_STATIC_NAME} ${DESTDIR}${LIBUCONTEXT_POSIX_STATIC_PATH}; \ + fi + +install-headers: + for i in ${LIBUCONTEXT_HEADERS}; do \ + destfn=$$(echo $$i | sed s:include/::g); \ + install -D -m644 $$i ${DESTDIR}${includedir}/$$destfn; \ + done + +install: all install-shared install-static install-pkgconf install-headers + +install_docs: docs + install -D -m644 doc/libucontext.3 ${DESTDIR}/usr/share/man/man3/libucontext.3 + for i in ${MANPAGES_SYMLINKS_3}; do \ + ln -s libucontext.3 ${DESTDIR}/usr/share/man/man3/$$i; \ + done + +ifneq (${FREESTANDING},yes) +check: check_libucontext_posix + +check_libucontext_posix: test_libucontext_posix ${LIBUCONTEXT_POSIX_SONAME} ${LIBUCONTEXT_SONAME} + env LD_LIBRARY_PATH=$(shell pwd) ./test_libucontext_posix + +test_libucontext_posix: test_libucontext_posix.c ${LIBUCONTEXT_POSIX_NAME} + $(CC) -std=gnu99 -D_DEFAULT_SOURCE ${CFLAGS} ${CPPFLAGS} $@.c -o $@ -L. -lucontext -lucontext_posix +endif + +ifeq ($(EXPORT_UNPREFIXED),yes) +check: check_libucontext_bare_posixabi + +check_libucontext_bare_posixabi: test_libucontext_bare_posixabi ${LIBUCONTEXT_SONAME} + env LD_LIBRARY_PATH=$(shell pwd) ./test_libucontext_bare_posixabi + +test_libucontext_bare_posixabi: test_libucontext_posix.c ${LIBUCONTEXT_NAME} + $(CC) -std=gnu99 -D_DEFAULT_SOURCE ${CFLAGS} ${CPPFLAGS} test_libucontext_posix.c -o $@ -L. -lucontext +endif + +check_libucontext: test_libucontext ${LIBUCONTEXT_SONAME} + env LD_LIBRARY_PATH=$(shell pwd) ./test_libucontext + +check: check_libucontext + +test_libucontext: test_libucontext.c ${LIBUCONTEXT_NAME} + $(CC) -std=gnu99 -D_DEFAULT_SOURCE ${CFLAGS} ${CPPFLAGS} $@.c -o $@ -L. -lucontext + +examples: ${LIBUCONTEXT_EXAMPLES} +examples/cooperative_threading: examples/cooperative_threading.c ${LIBUCONTEXT_NAME} + $(CC) -std=gnu99 -D_DEFAULT_SOURCE ${CFLAGS} ${CPPFLAGS} $@.c -o $@ -L. -lucontext + +ifeq ($(FREESTANDING),no) + +include/libucontext/bits.h: arch/common/include/libucontext/bits.h + cp $< $@ + +else + +include/libucontext/bits.h: arch/${ARCH}/include/libucontext/bits.h + cp $< $@ + +endif + +PACKAGE_NAME = libucontext +PACKAGE_VERSION = ${LIBUCONTEXT_VERSION} +DIST_NAME = ${PACKAGE_NAME}-${PACKAGE_VERSION} +DIST_TARBALL = ${DIST_NAME}.tar.xz + +distcheck: check dist +dist: ${DIST_TARBALL} +${DIST_TARBALL}: + git archive --format=tar --prefix=${DIST_NAME}/ -o ${DIST_NAME}.tar ${DIST_NAME} + xz ${DIST_NAME}.tar + +.PHONY: check dist diff --git a/src/3rdparty/libucontext/NEWS b/src/3rdparty/libucontext/NEWS new file mode 100644 index 000000000..af46a1021 --- /dev/null +++ b/src/3rdparty/libucontext/NEWS @@ -0,0 +1,133 @@ +Changes from 1.1 to 1.2 +----------------------- + +* Added Loongarch64 port. + +* Added OpenRISC (or1k) port. + +* Fixed various build system issues: + - libucontext_posix.so is no longer underlinked + - Executable stacks are now disabled when using GNU-like toolchains + - CPPFLAGS is used consistently + - Users may now build on Darwin without using Meson + +* aarch64 now implements the necessary SIMD register save/restore as + mandatory in AAPCS64. + Patches contributed by Richard Campbell. + +Changes from 1.0 to 1.1 +----------------------- + +* Added RISC-V RV32 port. + +* Cleaned up use of _GNU_SOURCE in C code. + Partially from patches contributed by osy. + +* Added automatic detection of armv7l architecture. + Patch contributed by Leandro Pereira. + +* Fixed installation path of libucontext_posix.a. + +* Work around deficiency in clang built-in assembler on AArch64 targets. + Patch contributed by osy. + +Changes from 0.13.1 to 1.0 +-------------------------- + +* Implement common libucontext_trampoline, written in C with inline + assembly. + +* Added Renesas / Hitachi SH-2/SH-4 port (sh). + +* Added Meson build system, primarily for the convenience of using + libucontext with qemu as a subproject. + +* Added support for Mach-O ABI. + +* Fixed deficiencies in libucontext ABI, SONAME has been bumped due + to the ABI regressions in 0.13. + +Changes from 0.13 to 0.13.1 +--------------------------- + +* Fix installation of libucontext.pc. + Patch contributed by Ömer Faruk IRMAK. + +Changes from 0.12 to 0.13 +------------------------- + +* Aligned RISC-V RV64 port's header usage with musl 1.2 to remove + warnings and ensure consistent register name usage. + +* Added Motorola 680X0 / NXP ColdFire port (m68k). + +* Added support for building for bare-metal targets with newlib via + make FREESTANDING=yes. Other OS are also supported (for example, + the m68k freestanding port was tested on AmigaOS), PowerPC requires + kernel assistance and cannot be built with FREESTANDING=yes. Not + all ports have support for FREESTANDING yet, patches welcome. + +Changes from 0.11 to 0.12 +------------------------- + +* Fixed compilation of RISC-V RV64 port with modern musl releases. + +Changes from 0.10 to 0.11 +------------------------- + +* Added RISC-V RV64 port. + +* Fixed compilation with clang. + Patch contributed by Khem Raj. + +* Add ${LIBDIR} variable to build system. + Patch contributed by Khem Raj. + +Changes from 0.9.0 to 0.10 +-------------------------- + +* Added MIPS O32 and MIPS N64 ports. + MIPS N32 ABI is theoretically supported by ARCH=mips64 with + a MIPS N32 toolchain, but this has not been tested. + +* Improved test program (test_libucontext) verbosity. + +* Modernized all architectures to use common assembly + macros, such as REG_OFFSET(reg), FUNC() and ALIAS(). + +* Added debugging hints to assembly functions for GDB. + +* Automatically alias i386/i686 to x86 in makefile. + Patch contributed by Gabriel Ivascu. + +Changes from 0.1.3 to 0.9.0 +--------------------------- + +* Pass ${LDFLAGS} when linking the libucontext library. + Patch contributed by Khem Raj. + +* Fix clobbering of the first stack argument on x86. + Patch contributed by A. Wilcox. + +* Add support for building a static libucontext. + Patches contributed by Gabriel Ivascu. + +* Rewrite ppc/ppc64 implementation to fully use the + swapcontext(3) syscall. + Patches contributed by Bobby Bingham. + +Changes from 0.1.1 to 0.1.3 +--------------------------- + +* Fix register clobbering on x86_64. + Patches contributed by A. Wilcox and Timo Teräs. + +Changes from 0.1.0 to 0.1.1 +--------------------------- + +* Added S390X port. + +* Cleaned up the ppc/ppc64 trampoline. + +* Fixed up GOT clobbering and removed a textrel from the x86 + trampoline. diff --git a/src/3rdparty/libucontext/README.md b/src/3rdparty/libucontext/README.md new file mode 100644 index 000000000..d9d5dbe89 --- /dev/null +++ b/src/3rdparty/libucontext/README.md @@ -0,0 +1,105 @@ +# `libucontext` + +`libucontext` is a library which provides the `ucontext.h` C API. Unlike other implementations, +it faithfully follows the kernel process ABI when doing context swaps. + +Notably, when combined with `gcompat`, it provides a fully compatible implementation of the ucontext +functions that are ABI compatible with glibc. + +Since version 0.13, for some architectures, you can deploy to bare metal using newlib via the +`FREESTANDING=yes` make option. Systems which use a syscall cannot work this way. The table +below shows which architecture ports have been adapted to build with `FREESTANDING=yes`. + +Adding support for new architectures is easy, but you need to know assembly language for the +target to do it. + + +## supported features + +| Architecture | Works on musl | Syscall | Supports FREESTANDING | Common trampoline | +|--------------|---------------|---------|-----------------------|-------------------| +| aarch64 | ✓ | | ✓ | ✓ | +| arm | ✓ | | ✓ | ✓ | +| loongarch64 | ✓ | | ✓ | | +| m68k | ✓ | | ✓ | ✓ | +| mips | ✓ | | ✓ | | +| mips64 | ✓ | | ✓ | | +| or1k | ✓ | | ✓ | ✓ | +| ppc | ✓ | ✓ | | | +| ppc64 | ✓ | ✓ | | | +| riscv32 | ✓ | | ✓ | ✓ | +| riscv64 | ✓ | | ✓ | ✓ | +| s390x | ✓ | | ✓ | | +| sh | ✓ | | ✓ | ✓ | +| x86 | ✓ | | ✓ | ✓ | +| x86_64 | ✓ | | ✓ | ✓ | + + +## building + +`libucontext` uses a simple makefile build system. You should define `ARCH=` at build time, otherwise +the build system will attempt to guess using `uname -m`. + +``` +$ make ARCH=x86_64 +$ make ARCH=x86_64 check +$ make ARCH=x86_64 DESTDIR=out install +``` + +There are a few options: + +* `ARCH`: The architecture libucontext is being built for. Must be set to one of the architectures + listed in the feature support table. If unset, the build system will attempt to guess based on what + architecture the host is running. Setting this option explicitly is highly recommended. + +* `FREESTANDING`: If this is set to `yes`, the system ucontext.h headers will not be used. Instead, + the headers in `arch/${ARCH}/freestanding` will be used for definitions where appropriate. + Default is `no`. + +* `EXPORT_UNPREFIXED`: If this is set to `yes`, the POSIX 2004 names `getcontext`, `setcontext`, + `swapcontext` and `makecontext` will be provided as weak symbols aliased against their `libucontext_` + namespaced equivalents. This is necessary for libucontext to provide these functions on musl + systems, but you may wish to disable this when using `FREESTANDING` mode to avoid conflicts with + the target's libc. Default is `yes`. + +* `DESTDIR`: If this variable is set, the installed files will be installed to the specified path instead + of the system root. + +If you have `scdoc` installed, you can build manpages and install them: + +``` +$ make docs +$ make DESTDIR=out install_docs +``` + + +## real-world use cases + +`libucontext` is used on almost all musl distributions to provide the legacy `ucontext.h` API. +Additionally, it is used by: + +* [UTM](https://getutm.app) -- friendly qemu distribution for macOS and iOS devices. UTM uses libucontext + as qemu's coroutine backend. + +* [Lwan](https://lwan.ws) -- a high-performance embeddable asynchronous web server. Lwan uses libucontext + to provide green threads when building on non-x86 architectures. + + +## caveats + +`libucontext`, while largely functionally equivalent does have some differences over traditional POSIX +ucontext functions: + +* Saving and restoring the signal mask is not implemented by default in order to avoid kernel syscall + overhead. Use `-lucontext_posix` if you actually need this functionality, which provides a POSIX + compliant implementation at the cost of performance. + +* Only basic GPR registers are saved and restored when context swapping. The glibc implementation uses + hardware capability detection to save/restore other register groups, such as the FPU registers or + vector processing (AltiVec/AVX/NEON) registers. Adding this capability detection would significantly + increase the complexity of the project and thus is not implemented. Support for compiling in code to + save/restore FPU registers or vector registers may be added in a later release as a build-time + setting -- for now, we assume a soft-float ABI with no optional processor features. In practice, this + does not really matter, code using these functions are unlikely to be impacted by this design + assumption. This is a work in progress, as newer compilers will spill even non-floating-point state + through floating point registers when allowed to do so. diff --git a/src/3rdparty/libucontext/VERSION b/src/3rdparty/libucontext/VERSION new file mode 100644 index 000000000..ea710abb9 --- /dev/null +++ b/src/3rdparty/libucontext/VERSION @@ -0,0 +1 @@ +1.2 \ No newline at end of file diff --git a/src/3rdparty/libucontext/arch/aarch64/defs.h b/src/3rdparty/libucontext/arch/aarch64/defs.h new file mode 100644 index 000000000..5398adcde --- /dev/null +++ b/src/3rdparty/libucontext/arch/aarch64/defs.h @@ -0,0 +1,27 @@ +#ifndef __ARCH_AARCH64_DEFS_H +#define __ARCH_AARCH64_DEFS_H + +#define REG_SZ (8) +#define MCONTEXT_GREGS (184) + +#define R0_OFFSET REG_OFFSET(0) + +#define SP_OFFSET 432 +#define PC_OFFSET 440 +#define PSTATE_OFFSET 448 +#define FPSIMD_CONTEXT_OFFSET 464 + +#ifndef FPSIMD_MAGIC +# define FPSIMD_MAGIC 0x46508001 +#endif + +#ifndef ESR_MAGIC +# define ESR_MAGIC 0x45535201 +#endif + +#define FETCH_LINKPTR(dest) \ + asm("mov %0, x19" : "=r" ((dest))) + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/aarch64/getcontext.S b/src/3rdparty/libucontext/arch/aarch64/getcontext.S new file mode 100644 index 000000000..37390a6bd --- /dev/null +++ b/src/3rdparty/libucontext/arch/aarch64/getcontext.S @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + + .global PROC_NAME(libucontext_getcontext); + .align 2; + TYPE(libucontext_getcontext) + ENT(libucontext_getcontext) +PROC_NAME(libucontext_getcontext): + str xzr, [x0, #REG_OFFSET(0)] + + /* save GPRs */ + stp x0, x1, [x0, #REG_OFFSET(0)] + stp x2, x3, [x0, #REG_OFFSET(2)] + stp x4, x5, [x0, #REG_OFFSET(4)] + stp x6, x7, [x0, #REG_OFFSET(6)] + stp x8, x9, [x0, #REG_OFFSET(8)] + stp x10, x11, [x0, #REG_OFFSET(10)] + stp x12, x13, [x0, #REG_OFFSET(12)] + stp x14, x15, [x0, #REG_OFFSET(14)] + stp x16, x17, [x0, #REG_OFFSET(16)] + stp x18, x19, [x0, #REG_OFFSET(18)] + stp x20, x21, [x0, #REG_OFFSET(20)] + stp x22, x23, [x0, #REG_OFFSET(22)] + stp x24, x25, [x0, #REG_OFFSET(24)] + stp x26, x27, [x0, #REG_OFFSET(26)] + stp x28, x29, [x0, #REG_OFFSET(28)] + str x30, [x0, #REG_OFFSET(30)] + + /* save current program counter in link register */ + str x30, [x0, #PC_OFFSET] + + /* save current stack pointer */ + mov x2, sp + str x2, [x0, #SP_OFFSET] + + /* save pstate */ + str xzr, [x0, #PSTATE_OFFSET] + + add x2, x0, #FPSIMD_CONTEXT_OFFSET + stp q8, q9, [x2, #144] + stp q10, q11, [x2, #176] + stp q12, q13, [x2, #208] + stp q14, q15, [x2, #240] + + mov x0, #0 + ret +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/aarch64/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/aarch64/include/libucontext/bits.h new file mode 100644 index 000000000..e249448f9 --- /dev/null +++ b/src/3rdparty/libucontext/arch/aarch64/include/libucontext/bits.h @@ -0,0 +1,34 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef unsigned long libucontext_greg_t; +typedef unsigned long libucontext_gregset_t[34]; + +typedef struct { + __uint128_t vregs[32]; + unsigned int fpsr; + unsigned int fpcr; +} libucontext_fpregset_t; + +typedef struct sigcontext { + unsigned long fault_address; + unsigned long regs[31]; + unsigned long sp, pc, pstate; + long double __reserved[256]; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + unsigned char __pad[136]; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/aarch64/makecontext.c b/src/3rdparty/libucontext/arch/aarch64/makecontext.c new file mode 100644 index 000000000..f6823cf6c --- /dev/null +++ b/src/3rdparty/libucontext/arch/aarch64/makecontext.c @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include +#include +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + +_Static_assert(offsetof(libucontext_ucontext_t, uc_mcontext.regs[0]) == R0_OFFSET, "R0_OFFSET is invalid"); +_Static_assert(offsetof(libucontext_ucontext_t, uc_mcontext.sp) == SP_OFFSET, "SP_OFFSET is invalid"); +_Static_assert(offsetof(libucontext_ucontext_t, uc_mcontext.pc) == PC_OFFSET, "PC_OFFSET is invalid"); +_Static_assert(offsetof(libucontext_ucontext_t, uc_mcontext.pstate) == PSTATE_OFFSET, "PSTATE_OFFSET is invalid"); + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + unsigned long *sp; + unsigned long *regp; + va_list va; + int i; + + sp = (unsigned long *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= argc < 8 ? 0 : argc - 8; + sp = (unsigned long *) (((uintptr_t) sp & -16L)); + + ucp->uc_mcontext.sp = (uintptr_t) sp; + ucp->uc_mcontext.pc = (uintptr_t) func; + ucp->uc_mcontext.regs[19] = (uintptr_t) ucp->uc_link; + ucp->uc_mcontext.regs[30] = (uintptr_t) &libucontext_trampoline; + + va_start(va, argc); + + regp = &(ucp->uc_mcontext.regs[0]); + + for (i = 0; (i < argc && i < 8); i++) + *regp++ = va_arg (va, unsigned long); + + for (; i < argc; i++) + *sp++ = va_arg (va, unsigned long); + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/aarch64/setcontext.S b/src/3rdparty/libucontext/arch/aarch64/setcontext.S new file mode 100644 index 000000000..f34836026 --- /dev/null +++ b/src/3rdparty/libucontext/arch/aarch64/setcontext.S @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + + .global PROC_NAME(libucontext_setcontext); + .align 2; + TYPE(libucontext_setcontext) + ENT(libucontext_setcontext) +PROC_NAME(libucontext_setcontext): + /* restore GPRs */ + ldp x18, x19, [x0, #REG_OFFSET(18)] + ldp x20, x21, [x0, #REG_OFFSET(20)] + ldp x22, x23, [x0, #REG_OFFSET(22)] + ldp x24, x25, [x0, #REG_OFFSET(24)] + ldp x26, x27, [x0, #REG_OFFSET(26)] + ldp x28, x29, [x0, #REG_OFFSET(28)] + ldr x30, [x0, #REG_OFFSET(30)] + + /* save current stack pointer */ + ldr x2, [x0, #SP_OFFSET] + mov sp, x2 + + add x2, x0, #FPSIMD_CONTEXT_OFFSET + ldp q8, q9, [x2, #144] + ldp q10, q11, [x2, #176] + ldp q12, q13, [x2, #208] + ldp q14, q15, [x2, #240] + + /* save current program counter in link register */ + ldr x16, [x0, #PC_OFFSET] + + /* restore args */ + ldp x2, x3, [x0, #REG_OFFSET(2)] + ldp x4, x5, [x0, #REG_OFFSET(4)] + ldp x6, x7, [x0, #REG_OFFSET(6)] + ldp x0, x1, [x0, #REG_OFFSET(0)] + + /* jump to new PC */ + br x16 +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/aarch64/swapcontext.S b/src/3rdparty/libucontext/arch/aarch64/swapcontext.S new file mode 100644 index 000000000..fc587e389 --- /dev/null +++ b/src/3rdparty/libucontext/arch/aarch64/swapcontext.S @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + + .global PROC_NAME(libucontext_swapcontext); + .align 2; + TYPE(libucontext_swapcontext) + ENT(libucontext_swapcontext) +PROC_NAME(libucontext_swapcontext): + str xzr, [x0, #REG_OFFSET(0)] + + /* save GPRs */ + stp x2, x3, [x0, #REG_OFFSET(2)] + stp x4, x5, [x0, #REG_OFFSET(4)] + stp x6, x7, [x0, #REG_OFFSET(6)] + stp x8, x9, [x0, #REG_OFFSET(8)] + stp x10, x11, [x0, #REG_OFFSET(10)] + stp x12, x13, [x0, #REG_OFFSET(12)] + stp x14, x15, [x0, #REG_OFFSET(14)] + stp x16, x17, [x0, #REG_OFFSET(16)] + stp x18, x19, [x0, #REG_OFFSET(18)] + stp x20, x21, [x0, #REG_OFFSET(20)] + stp x22, x23, [x0, #REG_OFFSET(22)] + stp x24, x25, [x0, #REG_OFFSET(24)] + stp x26, x27, [x0, #REG_OFFSET(26)] + stp x28, x29, [x0, #REG_OFFSET(28)] + str x30, [x0, #REG_OFFSET(30)] + + /* save current program counter in link register */ + str x30, [x0, #PC_OFFSET] + + /* save current stack pointer */ + mov x2, sp + str x2, [x0, #SP_OFFSET] + + /* save pstate */ + str xzr, [x0, #PSTATE_OFFSET] + + add x2, x0, #FPSIMD_CONTEXT_OFFSET + stp q8, q9, [x2, #144] + stp q10, q11, [x2, #176] + stp q12, q13, [x2, #208] + stp q14, q15, [x2, #240] + + /* context to swap to is in x1 so... we move to x0 and call setcontext */ + /* store our link register in x28 */ + mov x28, x30 + + /* move x1 to x0 and call setcontext */ + mov x0, x1 + bl PROC_NAME(libucontext_setcontext) + + /* hmm, we came back here try to return */ + mov x30, x28 + ret +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/aarch64/trampoline.c b/src/3rdparty/libucontext/arch/aarch64/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/aarch64/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/arch/arm/defs.h b/src/3rdparty/libucontext/arch/arm/defs.h new file mode 100644 index 000000000..1c3b7ddea --- /dev/null +++ b/src/3rdparty/libucontext/arch/arm/defs.h @@ -0,0 +1,15 @@ +#ifndef __ARCH_ARM_DEFS_H + +#define REG_SZ (4) +#define MCONTEXT_GREGS (32) +#define VFP_MAGIC_OFFSET (232) +#define VFP_D8_OFFSET (304) + +#define TYPE(__proc) .type __proc, %function; + +#define FETCH_LINKPTR(dest) \ + asm("movs %0, r4" : "=r" ((dest))) + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/arm/getcontext.S b/src/3rdparty/libucontext/arch/arm/getcontext.S new file mode 100644 index 000000000..0afa1506b --- /dev/null +++ b/src/3rdparty/libucontext/arch/arm/getcontext.S @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + /* copy all of the current registers into the ucontext structure */ + add r1, r0, #REG_OFFSET(4) + stmia r1, {r4-r12} + str r13, [r0, #REG_OFFSET(13)] + str r14, [r0, #REG_OFFSET(15)] + +#ifndef FORCE_SOFT_FLOAT +#ifndef FORCE_HARD_FLOAT + /* test for vfp, set kernel-defined magic number in uc_regspace */ + push {r0-r1,fp,lr} + mov r0, #16 + bl getauxval + tst r0, #64 + pop {r0-r1,fp,lr} + moveq r2, #0 + ldrne r2, =#0x56465001 + str r2, [r0, #VFP_MAGIC_OFFSET] + beq 1f +#endif + /* if vfp detected, save d8-d15 */ + .fpu vfp + add r1, r0, #VFP_D8_OFFSET + vstmia r1, {d8-d15} + .fpu softvfp +1: +#endif + + /* return 0 */ + mov r0, #0 + mov pc, lr +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/arm/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/arm/include/libucontext/bits.h new file mode 100644 index 000000000..caad8d4bd --- /dev/null +++ b/src/3rdparty/libucontext/arch/arm/include/libucontext/bits.h @@ -0,0 +1,30 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef int libucontext_greg_t, libucontext_gregset_t[18]; + +typedef struct { + unsigned long trap_no, error_code, oldmask; + unsigned long arm_r0, arm_r1, arm_r2, arm_r3; + unsigned long arm_r4, arm_r5, arm_r6, arm_r7; + unsigned long arm_r8, arm_r9, arm_r10, arm_fp; + unsigned long arm_ip, arm_sp, arm_lr, arm_pc; + unsigned long arm_cpsr, fault_address; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; + unsigned long uc_sigmask[128 / sizeof(long)]; + unsigned long long uc_regspace[64]; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/arm/makecontext.c b/src/3rdparty/libucontext/arch/arm/makecontext.c new file mode 100644 index 000000000..b137a16f8 --- /dev/null +++ b/src/3rdparty/libucontext/arch/arm/makecontext.c @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include +#include + + +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + unsigned long *sp; + unsigned long *regp; + va_list va; + int i; + + sp = (unsigned long *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp = (unsigned long *) (((uintptr_t) sp & -16L) - 8); + + if (argc > 4) + sp -= (argc - 4); + + ucp->uc_mcontext.arm_sp = (uintptr_t) sp; + ucp->uc_mcontext.arm_pc = (uintptr_t) func; + ucp->uc_mcontext.arm_r4 = (uintptr_t) ucp->uc_link; + ucp->uc_mcontext.arm_lr = (uintptr_t) &libucontext_trampoline; + + va_start(va, argc); + + regp = &(ucp->uc_mcontext.arm_r0); + + for (i = 0; (i < argc && i < 4); i++) + *regp++ = va_arg (va, unsigned long); + + for (; i < argc; i++) + *sp++ = va_arg (va, unsigned long); + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/arm/setcontext.S b/src/3rdparty/libucontext/arch/arm/setcontext.S new file mode 100644 index 000000000..792d8f272 --- /dev/null +++ b/src/3rdparty/libucontext/arch/arm/setcontext.S @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) +#ifndef FORCE_SOFT_FLOAT +#ifndef FORCE_HARD_FLOAT + /* test for vfp magic number set by getcontext */ + ldr r2, [r0, #VFP_MAGIC_OFFSET] + ldr r3, =#0x56465001 + cmp r2, r3 + bne 1f +#endif + /* if vfp in use, restore d8-d15 from uc_regspace */ + .fpu vfp + add r14, r0, #VFP_D8_OFFSET + vldmia r14, {d8-d15} + .fpu softvfp +1: +#endif + + /* copy all of the current registers into the ucontext structure */ + add r14, r0, #REG_OFFSET(0) + ldmia r14, {r0-r12} + ldr r13, [r14, #52] + add r14, r14, #56 + + /* load link register and jump to new context */ + ldmia r14, {r14, pc} +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/arm/swapcontext.S b/src/3rdparty/libucontext/arch/arm/swapcontext.S new file mode 100644 index 000000000..f8411828f --- /dev/null +++ b/src/3rdparty/libucontext/arch/arm/swapcontext.S @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* copy all of the current registers into the ucontext structure */ + add r2, r0, #REG_OFFSET(0) + stmia r2, {r0-r12} + str r13, [r0,#REG_OFFSET(13)] + str r14, [r0,#REG_OFFSET(15)] + +#ifndef FORCE_SOFT_FLOAT +#ifndef FORCE_HARD_FLOAT + /* test for vfp magic number, copy to other ucontext */ + ldr r3, [r1, #VFP_MAGIC_OFFSET] + ldr r4, =#0x56465001 + str r3, [r0, #VFP_MAGIC_OFFSET] + cmp r3, r4 + bne 1f +#endif + /* if vfp in use, save and restore d8-d15 */ + .fpu vfp + add r2, r0, #VFP_D8_OFFSET + vstmia r2, {d8-d15} + + add r14, r1, #VFP_D8_OFFSET + vldmia r14, {d8-d15} + .fpu softvfp +1: +#endif + + /* load new registers from the second ucontext structure */ + add r14, r1, #REG_OFFSET(0) + ldmia r14, {r0-r12} + ldr r13, [r14, #52] + add r14, r14, #56 + ldmia r14, {r14, pc} +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/arm/trampoline.c b/src/3rdparty/libucontext/arch/arm/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/arm/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/arch/common/common-defs.h b/src/3rdparty/libucontext/arch/common/common-defs.h new file mode 100644 index 000000000..8ba97bfd4 --- /dev/null +++ b/src/3rdparty/libucontext/arch/common/common-defs.h @@ -0,0 +1,61 @@ +#ifndef __ARCH_COMMON_COMMON_DEFS_H +#define __ARCH_COMMON_COMMON_DEFS_H + +#ifndef SETUP_FRAME +# define SETUP_FRAME(__proc) +#endif + +#ifndef PUSH_FRAME +# define PUSH_FRAME(__proc) +#endif + +#ifndef POP_FRAME +# define POP_FRAME(__proc) +#endif + +#ifndef ENT +# define ENT(__proc) +#endif + +#ifndef TYPE +# ifdef __clang__ +# define TYPE(__proc) // .type not supported +# else +# define TYPE(__proc) .type __proc, @function; +#endif +#endif + +#ifndef PROC_NAME +# ifdef __MACH__ +# define PROC_NAME(__proc) _ ## __proc +# else +# define PROC_NAME(__proc) __proc +# endif +#endif + +#define FUNC(__proc) \ + .global PROC_NAME(__proc); \ + .align 2; \ + TYPE(__proc) \ + ENT(__proc) \ +PROC_NAME(__proc): \ + SETUP_FRAME(__proc) +#ifdef __clang__ +#define END(__proc) +#else +#define END(__proc) \ + .end __proc; \ + .size __proc,.-__proc; +#endif + +#ifdef EXPORT_UNPREFIXED +#define ALIAS(__alias, __real) \ + .weak __alias; \ + __alias = __real; +#else +#define ALIAS(...) +#endif + +#define REG_OFFSET(__reg) (MCONTEXT_GREGS + ((__reg) * REG_SZ)) + +#endif diff --git a/src/3rdparty/libucontext/arch/common/common-trampoline.c b/src/3rdparty/libucontext/arch/common/common-trampoline.c new file mode 100644 index 000000000..5b5232760 --- /dev/null +++ b/src/3rdparty/libucontext/arch/common/common-trampoline.c @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include + +__attribute__ ((visibility ("hidden"))) +void +libucontext_trampoline(void) +{ + register libucontext_ucontext_t *uc_link = NULL; + + FETCH_LINKPTR(uc_link); + + if (uc_link == NULL) + exit(0); + + libucontext_setcontext(uc_link); +} diff --git a/src/3rdparty/libucontext/arch/common/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/common/include/libucontext/bits.h new file mode 100644 index 000000000..65a039f59 --- /dev/null +++ b/src/3rdparty/libucontext/arch/common/include/libucontext/bits.h @@ -0,0 +1,13 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +#ifndef FREESTANDING + +#include + +typedef greg_t libucontext_greg_t; +typedef ucontext_t libucontext_ucontext_t; + +#endif + +#endif diff --git a/src/3rdparty/libucontext/arch/loongarch64/defs.h b/src/3rdparty/libucontext/arch/loongarch64/defs.h new file mode 100644 index 000000000..94d014a8e --- /dev/null +++ b/src/3rdparty/libucontext/arch/loongarch64/defs.h @@ -0,0 +1,76 @@ +#ifndef __ARCH_LOONGARCH64_DEFS_H +#define __ARCH_LOONGARCH64_DEFS_H + +#define REG_SZ (8) + +#define REG_R0 (0) +#define REG_R1 (1) +#define REG_R2 (2) +#define REG_R3 (3) +#define REG_R4 (4) +#define REG_R5 (5) +#define REG_R6 (6) +#define REG_R7 (7) +#define REG_R8 (8) +#define REG_R9 (9) +#define REG_R10 (10) +#define REG_R11 (11) +#define REG_R12 (12) +#define REG_R13 (13) +#define REG_R14 (14) +#define REG_R15 (15) +#define REG_R16 (16) +#define REG_R17 (17) +#define REG_R18 (18) +#define REG_R19 (19) +#define REG_R20 (20) +#define REG_R21 (21) +#define REG_R22 (22) +#define REG_R23 (23) +#define REG_R24 (24) +#define REG_R25 (25) +#define REG_R26 (26) +#define REG_R27 (27) +#define REG_R28 (28) +#define REG_R29 (29) +#define REG_R30 (30) +#define REG_R31 (31) + +/* $a0 is $4 , also $v0, same as $5, $a1 and $v1*/ +#define REG_A0 (4) + +/* stack pointer is actually $3 */ +#define REG_SP (3) + +/* frame pointer is actually $22 */ +#define REG_FP (22) + +/* offset to mc_gregs in ucontext_t */ +#define MCONTEXT_GREGS (48) + +/* offset to PC in ucontext_t */ +#define MCONTEXT_PC (40) + +/* offset to uc_link in ucontext_t */ +#define UCONTEXT_UC_LINK (8) + +/* offset to uc_stack.ss_sp in ucontext_t */ +#define UCONTEXT_STACK_PTR (16) + +/* offset to uc_stack.ss_size in ucontext_t */ +#define UCONTEXT_STACK_SIZE (32) + +/* Stack alignment, from Kernel source */ +#define ALSZ 15 +#define ALMASK ~15 +#define FRAMESZ (((LOCALSZ * REG_SZ) + ALSZ) & ALMASK) + +#define PUSH_FRAME(__proc) \ + addi.d $sp, $sp, -FRAMESZ; + +#define POP_FRAME(__proc) \ + addi.d $sp, $sp, FRAMESZ; + +#include + +#endif diff --git a/src/3rdparty/libucontext/arch/loongarch64/freestanding/bits.h b/src/3rdparty/libucontext/arch/loongarch64/freestanding/bits.h new file mode 100644 index 000000000..b567678c5 --- /dev/null +++ b/src/3rdparty/libucontext/arch/loongarch64/freestanding/bits.h @@ -0,0 +1,43 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef unsigned long long libucontext_greg_t, libucontext_gregset_t[32]; + +/* Container for all general registers. */ +typedef __loongarch_mc_gp_state gregset_t; + +/* Container for floating-point state. */ +typedef union __loongarch_mc_fp_state fpregset_t; + +union __loongarch_mc_fp_state { + unsigned int __val32[256 / 32]; + unsigned long long __val64[256 / 64]; +}; + +typedef struct mcontext_t { + unsigned long long __pc; + unsigned long long __gregs[32]; + unsigned int __flags; + + unsigned int __fcsr; + unsigned int __vcsr; + unsigned long long __fcc; + union __loongarch_mc_fp_state __fpregs[32] __attribute__((__aligned__ (32))); + + unsigned int __reserved; +} mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/loongarch64/getcontext.S b/src/3rdparty/libucontext/arch/loongarch64/getcontext.S new file mode 100644 index 000000000..49c90a9e8 --- /dev/null +++ b/src/3rdparty/libucontext/arch/loongarch64/getcontext.S @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Peng Fan + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#define LOCALSZ (1) + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + /* copy $sp, $fp to temporary registers so we don't clobber them */ + move $a2, $sp + move $a3, $fp + + PUSH_FRAME(libucontext_getcontext) + + /* set registers */ + st.d $s0, $a0, REG_OFFSET(23) + st.d $s1, $a0, REG_OFFSET(24) + st.d $s2, $a0, REG_OFFSET(25) + st.d $s3, $a0, REG_OFFSET(26) + st.d $s4, $a0, REG_OFFSET(27) + st.d $s5, $a0, REG_OFFSET(28) + st.d $s6, $a0, REG_OFFSET(29) + st.d $s7, $a0, REG_OFFSET(30) + st.d $s8, $a0, REG_OFFSET(31) + + st.d $a2, $a0, REG_OFFSET(3) + st.d $a3, $a0, REG_OFFSET(22) + st.d $ra, $a0, REG_OFFSET(1) + + st.d $ra, $a0, (MCONTEXT_PC) + + POP_FRAME(libucontext_getcontext) + + jr $ra +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/loongarch64/makecontext.S b/src/3rdparty/libucontext/arch/loongarch64/makecontext.S new file mode 100644 index 000000000..44ec9a593 --- /dev/null +++ b/src/3rdparty/libucontext/arch/loongarch64/makecontext.S @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021 Peng Fan + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +#define LOCALSZ (6) + +#define A3_OFF (FRAMESZ - (5 * REG_SZ)) +#define A4_OFF (FRAMESZ - (4 * REG_SZ)) +#define A5_OFF (FRAMESZ - (3 * REG_SZ)) +#define A6_OFF (FRAMESZ - (2 * REG_SZ)) +#define A7_OFF (FRAMESZ - (1 * REG_SZ)) + +ALIAS(makecontext, libucontext_makecontext) + +FUNC(libucontext_makecontext) + PUSH_FRAME(libucontext_makecontext) + + move $t5, $a0 + move $t4, $a1 + + /* store $a3 through $a7 to the stack frame. */ + st.d $a3, $sp, A3_OFF + st.d $a4, $sp, A4_OFF + st.d $a5, $sp, A5_OFF + st.d $a6, $sp, A6_OFF + st.d $a7, $sp, A7_OFF + + /* set $zero in the mcontext to 1. */ + addi.d $v0, $zero, 1 + st.d $v0, $t5, REG_OFFSET(0) + + /* ensure the stack is aligned on a quad-word boundary. */ + ld.d $t0, $t5, UCONTEXT_STACK_PTR + ld.d $t2, $t5, UCONTEXT_STACK_SIZE + /* the third argument(from zero), that's the first argument of func() */ + addi.d $t1, $sp, A3_OFF + add.d $t0, $t0, $t2 + + addi.d $t7, $zero, ALMASK + and $t0, $t0, $t7 + + /* number of args */ + beq $a2, $zero, no_more_arguments + bltu $a2, $zero, no_more_arguments + + /* store register arguments. */ + addi.d $t2, $t5, MCONTEXT_GREGS + (4 * REG_SZ) + move $t3, $zero + +store_register_arg: + addi.d $t3, $t3, 1 + ld.d $v1, $t1, 0 + addi.d $t1, $t1, REG_SZ + st.d $v1, $t2, 0 + addi.d $t2, $t2, REG_SZ + addi.d $t6, $zero, 8 + bltu $t3, $t6, store_register_arg + bgeu $t3, $a2, no_more_arguments + + /* make room for stack arguments. */ + sub.d $t2, $a2, $t3 + + addi.d $t6, $zero, 3 + sll.d $t2, $t2, $t6 + + sub.d $t0, $t0, $t2 + + addi.d $t6, $zero, ALMASK + and $t0, $t0, $t6 + + /* store stack arguments. */ + move $t2, $t0 + +store_stack_arg: + addi.d $t3, $t3, 1 + ld.d $v1, $t1, 0 + addi.d $t1, $t1, REG_SZ + st.d $v1, $t2, 0 + addi.d $t2, $t2, REG_SZ + bltu $t3, $a2, store_stack_arg + +no_more_arguments: + /* trampoline setup. */ + la.got $t8, libucontext_trampoline + + ld.d $v1, $t5, UCONTEXT_UC_LINK + st.d $v1, $t5, REG_OFFSET(23) + + st.d $t0, $t5, REG_OFFSET(3) + + st.d $t8, $t5, REG_OFFSET(1) + + st.d $t4, $t5, MCONTEXT_PC + + POP_FRAME(libucontext_makecontext) + + jr $ra +END(libucontext_makecontext) diff --git a/src/3rdparty/libucontext/arch/loongarch64/setcontext.S b/src/3rdparty/libucontext/arch/loongarch64/setcontext.S new file mode 100644 index 000000000..9a0e0b7ea --- /dev/null +++ b/src/3rdparty/libucontext/arch/loongarch64/setcontext.S @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 Peng Fan + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#define LOCALSZ (1) + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + PUSH_FRAME(libucontext_setcontext) + + /* move the context to $v0, in LA, $v0 = $a0 = $4 */ + move $t5, $a0 + + /* load the registers */ + ld.d $a0, $t5, REG_OFFSET(4) + ld.d $a1, $t5, REG_OFFSET(5) + ld.d $a2, $t5, REG_OFFSET(6) + ld.d $a3, $t5, REG_OFFSET(7) + ld.d $a4, $t5, REG_OFFSET(8) + ld.d $a5, $t5, REG_OFFSET(9) + ld.d $a6, $t5, REG_OFFSET(10) + ld.d $a7, $t5, REG_OFFSET(11) + + ld.d $s0, $t5, REG_OFFSET(23) + ld.d $s1, $t5, REG_OFFSET(24) + ld.d $s2, $t5, REG_OFFSET(25) + ld.d $s3, $t5, REG_OFFSET(26) + ld.d $s4, $t5, REG_OFFSET(27) + ld.d $s5, $t5, REG_OFFSET(28) + ld.d $s6, $t5, REG_OFFSET(29) + ld.d $s7, $t5, REG_OFFSET(30) + ld.d $s8, $t5, REG_OFFSET(31) + + ld.d $sp, $t5, REG_OFFSET(3) + ld.d $fp, $t5, REG_OFFSET(22) + ld.d $ra, $t5, REG_OFFSET(1) + + ld.d $t8, $t5, (MCONTEXT_PC) + + jr $t8 + move $v0, $zero + + POP_FRAME(libucontext_setcontext) +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/loongarch64/startcontext.S b/src/3rdparty/libucontext/arch/loongarch64/startcontext.S new file mode 100644 index 000000000..b26450c0c --- /dev/null +++ b/src/3rdparty/libucontext/arch/loongarch64/startcontext.S @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2021 Peng Fan + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#define LOCALSZ (4) + +#include "defs.h" + +FUNC(libucontext_trampoline) + + /* call setcontext */ + move $a0, $s0 + /* we receive our initial ucontext in $s0, so if $s0 is nil, bail */ + beqz $s0, no_linked_context + + la.got $t8, libucontext_setcontext + + jr $t8 + +no_linked_context: + move $a0, $zero + la.global $t8, exit + jr $t8 + +END(libucontext_trampoline) diff --git a/src/3rdparty/libucontext/arch/loongarch64/swapcontext.S b/src/3rdparty/libucontext/arch/loongarch64/swapcontext.S new file mode 100644 index 000000000..b5063f68b --- /dev/null +++ b/src/3rdparty/libucontext/arch/loongarch64/swapcontext.S @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021 Peng Fan + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#define LOCALSZ (4) + +#include "defs.h" + +#define A1_OFFSET (FRAMESZ - (1 * REG_SZ)) + +ALIAS(swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* copy $sp, $fp to temporary registers so we don't clobber them */ + move $a3, $sp + move $a4, $fp + + move $t5, $a0 + + PUSH_FRAME(libucontext_swapcontext) + + /* set registers */ + st.d $s0, $t5, REG_OFFSET(23) + st.d $s1, $t5, REG_OFFSET(24) + st.d $s2, $t5, REG_OFFSET(25) + st.d $s3, $t5, REG_OFFSET(26) + st.d $s4, $t5, REG_OFFSET(27) + st.d $s5, $t5, REG_OFFSET(28) + st.d $s6, $t5, REG_OFFSET(29) + st.d $s7, $t5, REG_OFFSET(30) + st.d $s8, $t5, REG_OFFSET(31) + + st.d $a3, $t5, REG_OFFSET(3) + st.d $a4, $t5, REG_OFFSET(22) + st.d $ra, $t5, REG_OFFSET(1) + + st.d $ra, $t5, (MCONTEXT_PC) + + /* copy new context address in $a1 to stack */ + st.d $a1, $sp, A1_OFFSET + + /* load new context address into $v0 */ + ld.d $t4, $sp, A1_OFFSET + + /* load the registers */ + ld.d $a0, $t4, REG_OFFSET(4) + ld.d $a1, $t4, REG_OFFSET(5) + ld.d $a2, $t4, REG_OFFSET(6) + ld.d $a3, $t4, REG_OFFSET(7) + ld.d $a4, $t4, REG_OFFSET(8) + ld.d $a5, $t4, REG_OFFSET(9) + ld.d $a6, $t4, REG_OFFSET(10) + ld.d $a7, $t4, REG_OFFSET(11) + + ld.d $s0, $t4, REG_OFFSET(23) + ld.d $s1, $t4, REG_OFFSET(24) + ld.d $s2, $t4, REG_OFFSET(25) + ld.d $s3, $t4, REG_OFFSET(26) + ld.d $s4, $t4, REG_OFFSET(27) + ld.d $s5, $t4, REG_OFFSET(28) + ld.d $s6, $t4, REG_OFFSET(29) + ld.d $s7, $t4, REG_OFFSET(30) + ld.d $s8, $t4, REG_OFFSET(31) + + ld.d $sp, $t4, REG_OFFSET(3) + ld.d $fp, $t4, REG_OFFSET(22) + ld.d $ra, $t4, REG_OFFSET(1) + + ld.d $t8, $t4, (MCONTEXT_PC) + + jr $t8 + move $v0, $zero + +fail: + la.global $t8, exit + + POP_FRAME(libucontext_swapcontext) + + jirl $ra, $t8, 0 + move $v0, $zero +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/m68k/defs.h b/src/3rdparty/libucontext/arch/m68k/defs.h new file mode 100644 index 000000000..fc1925280 --- /dev/null +++ b/src/3rdparty/libucontext/arch/m68k/defs.h @@ -0,0 +1,34 @@ +#ifndef __ARCH_M68K_DEFS_H +#define __ARCH_M68K_DEFS_H + +#define REG_SZ (4) +#define MCONTEXT_GREGS (24) + +#define REG_D0 (0) +#define REG_D1 (1) +#define REG_D2 (2) +#define REG_D3 (3) +#define REG_D4 (4) +#define REG_D5 (5) +#define REG_D6 (6) +#define REG_D7 (7) +#define REG_A0 (8) +#define REG_A1 (9) +#define REG_A2 (10) +#define REG_A3 (11) +#define REG_A4 (12) +#define REG_A5 (13) +#define REG_A6 (14) +#define REG_A7 (15) +#define REG_SP (15) +#define REG_PC (16) +#define REG_PS (17) + +#define PC_OFFSET REG_OFFSET(REG_PC) + +#define FETCH_LINKPTR(dest) \ + asm("mov.l (%%sp, %%d7.l * 4), %0" :: "r" ((dest))) + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/m68k/getcontext.S b/src/3rdparty/libucontext/arch/m68k/getcontext.S new file mode 100644 index 000000000..40f2c1490 --- /dev/null +++ b/src/3rdparty/libucontext/arch/m68k/getcontext.S @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + move.l 4(%sp), %a0 /* load ucontext_t pointer from stack */ + + movem.l %d2-%d7, REG_OFFSET(REG_D2)(%a0) /* preserve $d2 through $d7 */ + movem.l %a2-%a6, REG_OFFSET(REG_A2)(%a0) /* preserve $a2 through $a6 */ + + lea 4(%sp), %a1 /* load stack pointer into $a1 */ + move.l %a1, REG_OFFSET(REG_SP)(%a0) /* store $a1 in ucontext */ + move.l (%sp), REG_OFFSET(REG_PC)(%a0) /* store return address in ucontext's PC register */ + + clr.l %d0 /* return 0 */ + rts +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/m68k/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/m68k/include/libucontext/bits.h new file mode 100644 index 000000000..17d3bf80b --- /dev/null +++ b/src/3rdparty/libucontext/arch/m68k/include/libucontext/bits.h @@ -0,0 +1,28 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef struct sigaltstack { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef int libucontext_greg_t, libucontext_gregset_t[18]; +typedef struct { + int f_pcr, f_psr, f_fpiaddr, f_fpregs[8][3]; +} libucontext_fpregset_t; + +typedef struct { + int version; + libucontext_gregset_t gregs; + libucontext_fpregset_t fpregs; +} libucontext_mcontext_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/m68k/makecontext.c b/src/3rdparty/libucontext/arch/m68k/makecontext.c new file mode 100644 index 000000000..c1a38e27c --- /dev/null +++ b/src/3rdparty/libucontext/arch/m68k/makecontext.c @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include +#include +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + libucontext_greg_t *sp; + va_list va; + int i; + + /* set up and align the stack. */ + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= (argc + 2); + sp = (libucontext_greg_t *) (((uintptr_t) sp & ~0x3)); + + /* set up the ucontext structure */ + ucp->uc_mcontext.gregs[REG_SP] = (libucontext_greg_t) sp; + ucp->uc_mcontext.gregs[REG_A6] = 0; + ucp->uc_mcontext.gregs[REG_D7] = argc; + ucp->uc_mcontext.gregs[REG_PC] = (libucontext_greg_t) func; + + /* return address */ + *sp++ = (libucontext_greg_t) libucontext_trampoline; + + va_start(va, argc); + + /* all arguments overflow into stack */ + for (i = 0; i < argc; i++) + *sp++ = va_arg (va, libucontext_greg_t); + + va_end(va); + + /* link pointer */ + *sp++ = (libucontext_greg_t) ucp->uc_link; +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/m68k/setcontext.S b/src/3rdparty/libucontext/arch/m68k/setcontext.S new file mode 100644 index 000000000..994c37eae --- /dev/null +++ b/src/3rdparty/libucontext/arch/m68k/setcontext.S @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + move.l 4(%sp), %a0 /* load ucontext_t pointer from stack */ + + move.l REG_OFFSET(REG_SP)(%a0), %sp /* load new stack pointer */ + + movem.l REG_OFFSET(REG_D2)(%a0), %d2-%d7 /* load $d2 through $d7 */ + movem.l REG_OFFSET(REG_A2)(%a0), %a2-%a6 /* load $a2 through $a6 */ + + clr.l %d0 /* clear $d0 */ + + move.l REG_OFFSET(REG_PC)(%a0), %a1 /* load jump target */ + + jmp (%a1) /* jump to *$a1 */ +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/m68k/swapcontext.S b/src/3rdparty/libucontext/arch/m68k/swapcontext.S new file mode 100644 index 000000000..e74eca5a7 --- /dev/null +++ b/src/3rdparty/libucontext/arch/m68k/swapcontext.S @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + move.l 4(%sp), %a0 /* load save ucontext_t pointer from stack */ + + movem.l %d2-%d7, REG_OFFSET(REG_D2)(%a0) /* preserve $d2 through $d7 */ + movem.l %a2-%a6, REG_OFFSET(REG_A2)(%a0) /* preserve $a2 through $a6 */ + + lea 4(%sp), %a1 /* load stack pointer into $a1 */ + move.l %a1, REG_OFFSET(REG_SP)(%a0) /* store $a1 in ucontext */ + move.l (%sp), REG_OFFSET(REG_PC)(%a0) /* store return address in ucontext's PC register */ + + move.l 8(%sp), %a0 /* load new ucontext_t pointer from stack */ + + move.l REG_OFFSET(REG_SP)(%a0), %sp /* load new stack pointer */ + + movem.l REG_OFFSET(REG_D2)(%a0), %d2-%d7 /* load $d2 through $d7 */ + movem.l REG_OFFSET(REG_A2)(%a0), %a2-%a6 /* load $a2 through $a6 */ + + clr.l %d0 /* clear $d0 */ + + move.l REG_OFFSET(REG_PC)(%a0), %a1 /* load jump target */ + + jmp (%a1) /* jump to *$a1 */ +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/m68k/trampoline.c b/src/3rdparty/libucontext/arch/m68k/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/m68k/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/arch/mips/defs.h b/src/3rdparty/libucontext/arch/mips/defs.h new file mode 100644 index 000000000..0d605dba1 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips/defs.h @@ -0,0 +1,94 @@ +#ifndef __ARCH_MIPS64_DEFS_H +#define __ARCH_MIPS64_DEFS_H + +#define REG_SZ (4) + +#define REG_R0 (0) +#define REG_R1 (1) +#define REG_R2 (2) +#define REG_R3 (3) +#define REG_R4 (4) +#define REG_R5 (5) +#define REG_R6 (6) +#define REG_R7 (7) +#define REG_R8 (8) +#define REG_R9 (9) +#define REG_R10 (10) +#define REG_R11 (11) +#define REG_R12 (12) +#define REG_R13 (13) +#define REG_R14 (14) +#define REG_R15 (15) +#define REG_R16 (16) +#define REG_R17 (17) +#define REG_R18 (18) +#define REG_R19 (19) +#define REG_R20 (20) +#define REG_R21 (21) +#define REG_R22 (22) +#define REG_R23 (23) +#define REG_R24 (24) +#define REG_R25 (25) +#define REG_R26 (26) +#define REG_R27 (27) +#define REG_R28 (28) +#define REG_R29 (29) +#define REG_R30 (30) +#define REG_R31 (31) + +/* $a0 is $4 */ +#define REG_A0 (4) + +/* stack pointer is actually $29 */ +#define REG_SP (29) + +/* frame pointer is actually $30 */ +#define REG_FP (30) + +/* $s0 ($16) is used as link register */ +#define REG_LNK (16) + +/* $t9 ($25) is used as entry */ +#define REG_ENTRY (25) + +/* offset to mc_gregs in ucontext_t */ +#define MCONTEXT_GREGS (40) + +/* offset to PC in ucontext_t */ +#define MCONTEXT_PC (32) + +/* offset to uc_link in ucontext_t */ +#define UCONTEXT_UC_LINK (4) + +/* offset to uc_stack.ss_sp in ucontext_t */ +#define UCONTEXT_STACK_PTR (8) + +/* offset to uc_stack.ss_size in ucontext_t */ +#define UCONTEXT_STACK_SIZE (12) + +/* setup frame, from MIPS N32/N64 calling convention manual */ +#define ALSZ 15 +#define ALMASK ~15 +#define FRAMESZ (((LOCALSZ * REG_SZ) + ALSZ) & ALMASK) // 16 +#define GPOFF (FRAMESZ - (LOCALSZ * REG_SZ)) // [16 - 16] + +#define SETUP_FRAME(__proc) \ + .frame $sp, FRAMESZ, $ra; \ + .mask 0x10000000, 0; \ + .fmask 0x00000000, 0; \ + .set noreorder; \ + .cpload $25; \ + .set reorder; + +#define PUSH_FRAME(__proc) \ + addiu $sp, -FRAMESZ; \ + .cprestore GPOFF; + +#define POP_FRAME(__proc) \ + addiu $sp, FRAMESZ + +#define ENT(__proc) .ent __proc, 0; + +#include + +#endif diff --git a/src/3rdparty/libucontext/arch/mips/getcontext.S b/src/3rdparty/libucontext/arch/mips/getcontext.S new file mode 100644 index 000000000..1655612be --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips/getcontext.S @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 1 + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + /* copy $gp, $sp, $fp to temporary registers so we don't clobber them */ + move $a2, $gp + move $a3, $sp + move $a1, $fp + + PUSH_FRAME(libucontext_getcontext) + + /* set registers */ + sw $s0, REG_OFFSET(16)($a0) + sw $s1, REG_OFFSET(17)($a0) + sw $s2, REG_OFFSET(18)($a0) + sw $s3, REG_OFFSET(19)($a0) + sw $s4, REG_OFFSET(20)($a0) + sw $s5, REG_OFFSET(21)($a0) + sw $s6, REG_OFFSET(22)($a0) + sw $s7, REG_OFFSET(23)($a0) + + sw $a2, REG_OFFSET(28)($a0) + sw $a3, REG_OFFSET(29)($a0) + sw $a1, REG_OFFSET(30)($a0) + sw $ra, REG_OFFSET(31)($a0) + sw $ra, (MCONTEXT_PC)($a0) + + POP_FRAME(libucontext_getcontext) + + jr $ra +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/mips/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/mips/include/libucontext/bits.h new file mode 100644 index 000000000..41a271a82 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips/include/libucontext/bits.h @@ -0,0 +1,27 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef unsigned long long libucontext_greg_t, libucontext_gregset_t[32]; + +typedef struct { + unsigned regmask, status; + unsigned long long pc, gregs[32], fpregs[32]; + unsigned ownedfp, fpc_csr, fpc_eir, used_math, dsp; + unsigned long long mdhi, mdlo; + unsigned long hi1, lo1, hi2, lo2, hi3, lo3; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + size_t ss_size; + int ss_flags; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/mips/makecontext.S b/src/3rdparty/libucontext/arch/mips/makecontext.S new file mode 100644 index 000000000..bb841aed7 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips/makecontext.S @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 2 + +#include "defs.h" + +A3_OFF = FRAMESZ + (3 * REG_SZ) + +/* + * Because we have to fiddle with $gp, we have to implement this in + * assembly rather than C. Annoying, that... + */ + +ALIAS(makecontext, libucontext_makecontext) +ALIAS(__makecontext, libucontext_makecontext) + +FUNC(libucontext_makecontext) + PUSH_FRAME(libucontext_makecontext) + + /* store $a3 through $a7 to the stack frame. */ + sw $a3, A3_OFF($sp) + + /* set $zero in the mcontext to 1. */ + li $v0, 1 + sw $v0, REG_OFFSET(0)($a0) + + /* ensure the stack is aligned on a quad-word boundary. */ + lw $t0, UCONTEXT_STACK_PTR($a0) + lw $t2, UCONTEXT_STACK_SIZE($a0) + addiu $t1, $sp, A3_OFF + addu $t0, $t2 + and $t0, ALMASK + blez $a2, no_more_arguments + + /* store register arguments. */ + addiu $t2, $a0, MCONTEXT_GREGS + (4 * REG_SZ) + move $t3, $zero + +store_register_arg: + addiu $t3, 1 + lw $v1, ($t1) + addiu $t1, REG_SZ + sw $v1, ($t2) + addiu $t2, REG_SZ + bgeu $t3, $a2, no_more_arguments + bltu $t3, 4, store_register_arg + + /* make room for stack arguments. */ + subu $t2, $a2, $t3 + sll $t2, 3 + subu $t0, $t2 + and $t0, ALMASK + + /* store stack arguments. */ + move $t2, $t0 + +store_stack_arg: + addiu $t3, 1 + lw $v1, ($t1) + addiu $t1, REG_SZ + sw $v1, ($t2) + addiu $t2, REG_SZ + bltu $t3, $a2, store_stack_arg + +no_more_arguments: + /* make room for $a0-$a3 storage */ + addiu $t0, -(4 * REG_SZ) + + /* trampoline setup. */ + la $t9, libucontext_trampoline + + /* copy link pointer as $s0... */ + lw $v1, UCONTEXT_UC_LINK($a0) + sw $v1, REG_OFFSET(16)($a0) + + /* set our $sp */ + sw $t0, REG_OFFSET(29)($a0) + + /* $gp is copied as $s1 */ + sw $gp, REG_OFFSET(17)($a0) + + /* set our $ra */ + sw $t9, REG_OFFSET(31)($a0) + + /* set our $pc */ + sw $a1, MCONTEXT_PC($a0) + + POP_FRAME(libucontext_makecontext) + + jr $ra +END(libucontext_makecontext) diff --git a/src/3rdparty/libucontext/arch/mips/setcontext.S b/src/3rdparty/libucontext/arch/mips/setcontext.S new file mode 100644 index 000000000..6017048f4 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips/setcontext.S @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 1 + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + PUSH_FRAME(libucontext_setcontext) + + /* move the context to $v0 */ + move $v0, $a0 + + /* load the registers */ + lw $a0, REG_OFFSET(4)($v0) + lw $a1, REG_OFFSET(5)($v0) + lw $a2, REG_OFFSET(6)($v0) + lw $a3, REG_OFFSET(7)($v0) + + lw $s0, REG_OFFSET(16)($v0) + lw $s1, REG_OFFSET(17)($v0) + lw $s2, REG_OFFSET(18)($v0) + lw $s3, REG_OFFSET(19)($v0) + lw $s4, REG_OFFSET(20)($v0) + lw $s5, REG_OFFSET(21)($v0) + lw $s6, REG_OFFSET(22)($v0) + lw $s7, REG_OFFSET(23)($v0) + + lw $gp, REG_OFFSET(28)($v0) + lw $sp, REG_OFFSET(29)($v0) + lw $fp, REG_OFFSET(30)($v0) + lw $ra, REG_OFFSET(31)($v0) + lw $t9, (MCONTEXT_PC)($v0) + + move $v0, $zero + jr $t9 + + POP_FRAME(libucontext_setcontext) +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/mips/startcontext.S b/src/3rdparty/libucontext/arch/mips/startcontext.S new file mode 100644 index 000000000..2ac79ec76 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips/startcontext.S @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 1 + +#include "defs.h" + +.hidden libucontext_trampoline +FUNC(libucontext_trampoline) + move $gp, $s1 + + /* we receive our initial ucontext in $s0, so if $s0 is nil, bail */ + beqz $s0, no_linked_context + + /* call setcontext */ + move $a0, $s0 + la $t9, PROC_NAME(libucontext_setcontext) + + jr $t9 + +no_linked_context: + move $a0, $zero + la $t9, exit + jalr $t9 + nop +END(libucontext_trampoline) diff --git a/src/3rdparty/libucontext/arch/mips/swapcontext.S b/src/3rdparty/libucontext/arch/mips/swapcontext.S new file mode 100644 index 000000000..5e5647083 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips/swapcontext.S @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 2 + +#include "defs.h" + +A1_OFFSET = FRAMESZ - (1 * REG_SZ) + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* copy $gp, $sp, $fp to temporary registers so we don't clobber them */ + move $a2, $gp + move $a3, $sp + + PUSH_FRAME(libucontext_swapcontext) + + /* set registers */ + sd $s0, REG_OFFSET(16)($a0) + sd $s1, REG_OFFSET(17)($a0) + sd $s2, REG_OFFSET(18)($a0) + sd $s3, REG_OFFSET(19)($a0) + sd $s4, REG_OFFSET(20)($a0) + sd $s5, REG_OFFSET(21)($a0) + sd $s6, REG_OFFSET(22)($a0) + sd $s7, REG_OFFSET(23)($a0) + + sd $a2, REG_OFFSET(28)($a0) + sd $a3, REG_OFFSET(29)($a0) + sd $fp, REG_OFFSET(30)($a0) + sd $ra, REG_OFFSET(31)($a0) + sd $ra, (MCONTEXT_PC)($a0) + + /* copy new context address in $a1 to stack */ + sd $a1, A1_OFFSET($sp) + + /* load new context address into $v0 */ + ld $v0, A1_OFFSET($sp) + + /* load the registers */ + ld $a0, REG_OFFSET(4)($v0) + ld $a1, REG_OFFSET(5)($v0) + ld $a2, REG_OFFSET(6)($v0) + ld $a3, REG_OFFSET(7)($v0) + + ld $s0, REG_OFFSET(16)($v0) + ld $s1, REG_OFFSET(17)($v0) + ld $s2, REG_OFFSET(18)($v0) + ld $s3, REG_OFFSET(19)($v0) + ld $s4, REG_OFFSET(20)($v0) + ld $s5, REG_OFFSET(21)($v0) + ld $s6, REG_OFFSET(22)($v0) + ld $s7, REG_OFFSET(23)($v0) + + ld $gp, REG_OFFSET(28)($v0) + ld $sp, REG_OFFSET(29)($v0) + ld $fp, REG_OFFSET(30)($v0) + ld $ra, REG_OFFSET(31)($v0) + ld $t9, (MCONTEXT_PC)($v0) + + move $v0, $zero + jr $t9 + +fail: + la $t9, exit + + POP_FRAME(libucontext_swapcontext) + + move $v0, $zero + jalr $t9 +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/mips64/defs.h b/src/3rdparty/libucontext/arch/mips64/defs.h new file mode 100644 index 000000000..ade528842 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips64/defs.h @@ -0,0 +1,92 @@ +#ifndef __ARCH_MIPS64_DEFS_H +#define __ARCH_MIPS64_DEFS_H + +#define REG_SZ (8) + +#define REG_R0 (0) +#define REG_R1 (1) +#define REG_R2 (2) +#define REG_R3 (3) +#define REG_R4 (4) +#define REG_R5 (5) +#define REG_R6 (6) +#define REG_R7 (7) +#define REG_R8 (8) +#define REG_R9 (9) +#define REG_R10 (10) +#define REG_R11 (11) +#define REG_R12 (12) +#define REG_R13 (13) +#define REG_R14 (14) +#define REG_R15 (15) +#define REG_R16 (16) +#define REG_R17 (17) +#define REG_R18 (18) +#define REG_R19 (19) +#define REG_R20 (20) +#define REG_R21 (21) +#define REG_R22 (22) +#define REG_R23 (23) +#define REG_R24 (24) +#define REG_R25 (25) +#define REG_R26 (26) +#define REG_R27 (27) +#define REG_R28 (28) +#define REG_R29 (29) +#define REG_R30 (30) +#define REG_R31 (31) + +/* $a0 is $4 */ +#define REG_A0 (4) + +/* stack pointer is actually $29 */ +#define REG_SP (29) + +/* frame pointer is actually $30 */ +#define REG_FP (30) + +/* $s0 ($16) is used as link register */ +#define REG_LNK (16) + +/* $t9 ($25) is used as entry */ +#define REG_ENTRY (25) + +/* offset to mc_gregs in ucontext_t */ +#define MCONTEXT_GREGS (40) + +/* offset to PC in ucontext_t */ +#define MCONTEXT_PC (MCONTEXT_GREGS + 576) + +/* offset to uc_link in ucontext_t */ +#define UCONTEXT_UC_LINK (8) + +/* offset to uc_stack.ss_sp in ucontext_t */ +#define UCONTEXT_STACK_PTR (16) + +/* offset to uc_stack.ss_size in ucontext_t */ +#define UCONTEXT_STACK_SIZE (24) + +/* setup frame, from MIPS N32/N64 calling convention manual */ +#define ALSZ 15 +#define ALMASK ~15 +#define FRAMESZ (((LOCALSZ * REG_SZ) + ALSZ) & ALMASK) // 16 +#define GPOFF (FRAMESZ - (LOCALSZ * REG_SZ)) // [16 - 16] + +#define SETUP_FRAME(__proc) \ + .frame $sp, FRAMESZ, $ra; \ + .mask 0x10000000, 0; \ + .fmask 0x00000000, 0; + +#define PUSH_FRAME(__proc) \ + daddiu $sp, -FRAMESZ; \ + .cpsetup $25, GPOFF, __proc; + +#define POP_FRAME(__proc) \ + .cpreturn; \ + daddiu $sp, FRAMESZ + +#define ENT(__proc) .ent __proc, 0; + +#include + +#endif diff --git a/src/3rdparty/libucontext/arch/mips64/getcontext.S b/src/3rdparty/libucontext/arch/mips64/getcontext.S new file mode 100644 index 000000000..9912573cc --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips64/getcontext.S @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 1 + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + /* copy $gp, $sp, $fp to temporary registers so we don't clobber them */ + move $a2, $gp + move $a3, $sp + move $a4, $fp + + PUSH_FRAME(libucontext_getcontext) + + /* set registers */ + sd $s0, REG_OFFSET(16)($a0) + sd $s1, REG_OFFSET(17)($a0) + sd $s2, REG_OFFSET(18)($a0) + sd $s3, REG_OFFSET(19)($a0) + sd $s4, REG_OFFSET(20)($a0) + sd $s5, REG_OFFSET(21)($a0) + sd $s6, REG_OFFSET(22)($a0) + sd $s7, REG_OFFSET(23)($a0) + + sd $a2, REG_OFFSET(28)($a0) + sd $a3, REG_OFFSET(29)($a0) + sd $a4, REG_OFFSET(30)($a0) + sd $ra, REG_OFFSET(31)($a0) + sd $ra, (MCONTEXT_PC)($a0) + + POP_FRAME(libucontext_getcontext) + + jr $ra +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/mips64/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/mips64/include/libucontext/bits.h new file mode 100644 index 000000000..538dcd580 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips64/include/libucontext/bits.h @@ -0,0 +1,47 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef unsigned long long libucontext_greg_t, libucontext_gregset_t[32]; + +typedef struct { + union { + double fp_dregs[32]; + struct { + float _fp_fregs; + unsigned _fp_pad; + } fp_fregs[32]; + } fp_r; +} libucontext_fpregset_t; + +typedef struct { + libucontext_gregset_t gregs; + libucontext_fpregset_t fpregs; + libucontext_greg_t mdhi; + libucontext_greg_t hi1; + libucontext_greg_t hi2; + libucontext_greg_t hi3; + libucontext_greg_t mdlo; + libucontext_greg_t lo1; + libucontext_greg_t lo2; + libucontext_greg_t lo3; + libucontext_greg_t pc; + unsigned int fpc_csr; + unsigned int used_math; + unsigned int dsp; + unsigned int reserved; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + size_t ss_size; + int ss_flags; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/mips64/makecontext.S b/src/3rdparty/libucontext/arch/mips64/makecontext.S new file mode 100644 index 000000000..98b4b514b --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips64/makecontext.S @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +/* $gp + 5 args */ +LOCALSZ = 6 + +#include "defs.h" + +A3_OFF = FRAMESZ - (5 * REG_SZ) +A4_OFF = FRAMESZ - (4 * REG_SZ) +A5_OFF = FRAMESZ - (3 * REG_SZ) +A6_OFF = FRAMESZ - (2 * REG_SZ) +A7_OFF = FRAMESZ - (1 * REG_SZ) + +/* + * Because we have to fiddle with $gp, we have to implement this in + * assembly rather than C. Annoying, that... + */ + +ALIAS(makecontext, libucontext_makecontext) +ALIAS(__makecontext, libucontext_makecontext) + +FUNC(libucontext_makecontext) + PUSH_FRAME(libucontext_makecontext) + + /* store $a3 through $a7 to the stack frame. */ + sd $a3, A3_OFF($sp) + sd $a4, A4_OFF($sp) + sd $a5, A5_OFF($sp) + sd $a6, A6_OFF($sp) + sd $a7, A7_OFF($sp) + + /* set $zero in the mcontext to 1. */ + li $v0, 1 + sd $v0, REG_OFFSET(0)($a0) + + /* ensure the stack is aligned on a quad-word boundary. */ + ld $t0, UCONTEXT_STACK_PTR($a0) + ld $t2, UCONTEXT_STACK_SIZE($a0) + daddiu $t1, $sp, A3_OFF + daddu $t0, $t2 + and $t0, ALMASK + blez $a2, no_more_arguments + + /* store register arguments. */ + daddiu $t2, $a0, MCONTEXT_GREGS + (4 * REG_SZ) + move $t3, $zero + +store_register_arg: + daddiu $t3, 1 + ld $v1, ($t1) + daddiu $t1, REG_SZ + sd $v1, ($t2) + daddiu $t2, REG_SZ + bgeu $t3, $a2, no_more_arguments + bltu $t3, 8, store_register_arg + + /* make room for stack arguments. */ + dsubu $t2, $a2, $t3 + dsll $t2, 3 + dsubu $t0, $t2 + and $t0, ALMASK + + /* store stack arguments. */ + move $t2, $t0 + +store_stack_arg: + daddiu $t3, 1 + ld $v1, ($t1) + daddiu $t1, REG_SZ + sd $v1, ($t2) + daddiu $t2, REG_SZ + bltu $t3, $a2, store_stack_arg + +no_more_arguments: + /* trampoline setup. */ + dla $t9, libucontext_trampoline + + /* copy link pointer as $s0... */ + ld $v1, UCONTEXT_UC_LINK($a0) + sd $v1, REG_OFFSET(16)($a0) + + /* set our $sp */ + sd $t0, REG_OFFSET(29)($a0) + + /* $gp is copied as $s1 */ + sd $gp, REG_OFFSET(17)($a0) + + /* set our $ra */ + sd $t9, REG_OFFSET(31)($a0) + + /* set our $pc */ + sd $a1, MCONTEXT_PC($a0) + + POP_FRAME(libucontext_makecontext) + + jr $ra +END(libucontext_makecontext) diff --git a/src/3rdparty/libucontext/arch/mips64/setcontext.S b/src/3rdparty/libucontext/arch/mips64/setcontext.S new file mode 100644 index 000000000..17b9969fc --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips64/setcontext.S @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 1 + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + PUSH_FRAME(libucontext_setcontext) + + /* move the context to $v0 */ + move $v0, $a0 + + /* load the registers */ + ld $a0, REG_OFFSET(4)($v0) + ld $a1, REG_OFFSET(5)($v0) + ld $a2, REG_OFFSET(6)($v0) + ld $a3, REG_OFFSET(7)($v0) + ld $a4, REG_OFFSET(8)($v0) + ld $a5, REG_OFFSET(9)($v0) + ld $a6, REG_OFFSET(10)($v0) + ld $a7, REG_OFFSET(11)($v0) + + ld $s0, REG_OFFSET(16)($v0) + ld $s1, REG_OFFSET(17)($v0) + ld $s2, REG_OFFSET(18)($v0) + ld $s3, REG_OFFSET(19)($v0) + ld $s4, REG_OFFSET(20)($v0) + ld $s5, REG_OFFSET(21)($v0) + ld $s6, REG_OFFSET(22)($v0) + ld $s7, REG_OFFSET(23)($v0) + + ld $gp, REG_OFFSET(28)($v0) + ld $sp, REG_OFFSET(29)($v0) + ld $fp, REG_OFFSET(30)($v0) + ld $ra, REG_OFFSET(31)($v0) + ld $t9, (MCONTEXT_PC)($v0) + + move $v0, $zero + jr $t9 + + POP_FRAME(libucontext_setcontext) +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/mips64/startcontext.S b/src/3rdparty/libucontext/arch/mips64/startcontext.S new file mode 100644 index 000000000..a96027957 --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips64/startcontext.S @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 1 + +#include "defs.h" + +.hidden libucontext_trampoline +FUNC(libucontext_trampoline) + move $gp, $s1 + + /* we receive our initial ucontext in $s0, so if $s0 is nil, bail */ + beqz $s0, no_linked_context + + /* call setcontext */ + move $a0, $s0 + dla $t9, PROC_NAME(libucontext_setcontext) + + jr $t9 + +no_linked_context: + move $a0, $zero + dla $t9, exit + jalr $t9 + nop +END(libucontext_trampoline) diff --git a/src/3rdparty/libucontext/arch/mips64/swapcontext.S b/src/3rdparty/libucontext/arch/mips64/swapcontext.S new file mode 100644 index 000000000..22846611f --- /dev/null +++ b/src/3rdparty/libucontext/arch/mips64/swapcontext.S @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +LOCALSZ = 2 + +#include "defs.h" + +A1_OFFSET = FRAMESZ - (1 * REG_SZ) + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* copy $gp, $sp, $fp to temporary registers so we don't clobber them */ + move $a2, $gp + move $a3, $sp + move $a4, $fp + + PUSH_FRAME(libucontext_swapcontext) + + /* set registers */ + sd $s0, REG_OFFSET(16)($a0) + sd $s1, REG_OFFSET(17)($a0) + sd $s2, REG_OFFSET(18)($a0) + sd $s3, REG_OFFSET(19)($a0) + sd $s4, REG_OFFSET(20)($a0) + sd $s5, REG_OFFSET(21)($a0) + sd $s6, REG_OFFSET(22)($a0) + sd $s7, REG_OFFSET(23)($a0) + + sd $a2, REG_OFFSET(28)($a0) + sd $a3, REG_OFFSET(29)($a0) + sd $a4, REG_OFFSET(30)($a0) + sd $ra, REG_OFFSET(31)($a0) + sd $ra, (MCONTEXT_PC)($a0) + + /* copy new context address in $a1 to stack */ + sd $a1, A1_OFFSET($sp) + + /* load new context address into $v0 */ + ld $v0, A1_OFFSET($sp) + + /* load the registers */ + ld $a0, REG_OFFSET(4)($v0) + ld $a1, REG_OFFSET(5)($v0) + ld $a2, REG_OFFSET(6)($v0) + ld $a3, REG_OFFSET(7)($v0) + ld $a4, REG_OFFSET(8)($v0) + ld $a5, REG_OFFSET(9)($v0) + ld $a6, REG_OFFSET(10)($v0) + ld $a7, REG_OFFSET(11)($v0) + + ld $s0, REG_OFFSET(16)($v0) + ld $s1, REG_OFFSET(17)($v0) + ld $s2, REG_OFFSET(18)($v0) + ld $s3, REG_OFFSET(19)($v0) + ld $s4, REG_OFFSET(20)($v0) + ld $s5, REG_OFFSET(21)($v0) + ld $s6, REG_OFFSET(22)($v0) + ld $s7, REG_OFFSET(23)($v0) + + ld $gp, REG_OFFSET(28)($v0) + ld $sp, REG_OFFSET(29)($v0) + ld $fp, REG_OFFSET(30)($v0) + ld $ra, REG_OFFSET(31)($v0) + ld $t9, (MCONTEXT_PC)($v0) + + move $v0, $zero + jr $t9 + +fail: + dla $t9, exit + + POP_FRAME(libucontext_swapcontext) + + move $v0, $zero + jalr $t9 +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/or1k/defs.h b/src/3rdparty/libucontext/arch/or1k/defs.h new file mode 100644 index 000000000..fe2b31f91 --- /dev/null +++ b/src/3rdparty/libucontext/arch/or1k/defs.h @@ -0,0 +1,22 @@ +#ifndef __ARCH_OR1K_DEFS_H +#define __ARCH_OR1K_DEFS_H + +#define REG_SZ (4) +#define MCONTEXT_GREGS (20) + +#define REG_SP (1) +#define REG_FP (2) +#define REG_RA (9) +#define REG_SA (11) +#define REG_LR (14) +#define REG_PC (33) +#define REG_SR (34) + +#define PC_OFFSET REG_OFFSET(REG_PC) + +#define FETCH_LINKPTR(dest) \ + asm("l.ori %0, r14, 0" :: "r" ((dest))) + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/or1k/getcontext.S b/src/3rdparty/libucontext/arch/or1k/getcontext.S new file mode 100644 index 000000000..0d7e55c7a --- /dev/null +++ b/src/3rdparty/libucontext/arch/or1k/getcontext.S @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + l.sw REG_OFFSET(1)(r3), r1 /* store r1 and r2 */ + l.sw REG_OFFSET(2)(r3), r2 + + l.sw REG_OFFSET(9)(r3), r9 /* store r9 to both r9 and r11 in the context */ + l.sw REG_OFFSET(11)(r3), r9 + + l.sw REG_OFFSET(10)(r3), r10 /* store r10 for TLS */ + + l.sw REG_OFFSET(14)(r3), r14 /* store r14 through r30 even */ + l.sw REG_OFFSET(16)(r3), r16 + l.sw REG_OFFSET(18)(r3), r18 + l.sw REG_OFFSET(20)(r3), r20 + l.sw REG_OFFSET(22)(r3), r22 + l.sw REG_OFFSET(24)(r3), r24 + l.sw REG_OFFSET(26)(r3), r26 + l.sw REG_OFFSET(28)(r3), r28 + l.sw REG_OFFSET(30)(r3), r30 + + l.jr r9 + l.ori r11, r0, 0 +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/or1k/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/or1k/include/libucontext/bits.h new file mode 100644 index 000000000..650351ec3 --- /dev/null +++ b/src/3rdparty/libucontext/arch/or1k/include/libucontext/bits.h @@ -0,0 +1,28 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef struct sigaltstack { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef int libucontext_greg_t, libucontext_gregset_t[32]; + +typedef struct { + struct { + libucontext_gregset_t gpr; + libucontext_greg_t pc; + libucontext_greg_t sr; + } regs; + unsigned long oldmask; +} libucontext_mcontext_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/or1k/makecontext.c b/src/3rdparty/libucontext/arch/or1k/makecontext.c new file mode 100644 index 000000000..863ba777c --- /dev/null +++ b/src/3rdparty/libucontext/arch/or1k/makecontext.c @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include +#include +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + libucontext_greg_t *sp; + va_list va; + int i; + + /* set up and align the stack. */ + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= argc < 6 ? 0 : (argc - 6); + sp = (libucontext_greg_t *) (((uintptr_t) sp & ~0x3)); + + /* set up the ucontext structure */ + ucp->uc_mcontext.regs.gpr[REG_SP] = (libucontext_greg_t) sp; + ucp->uc_mcontext.regs.gpr[REG_RA] = (libucontext_greg_t) &libucontext_trampoline; + ucp->uc_mcontext.regs.gpr[REG_FP] = 0; + ucp->uc_mcontext.regs.gpr[REG_SA] = (libucontext_greg_t) func; + ucp->uc_mcontext.regs.gpr[REG_LR] = (libucontext_greg_t) ucp->uc_link; + + va_start(va, argc); + + /* args less than argv[6] have dedicated registers, else they overflow onto stack */ + for (i = 0; i < argc; i++) + { + if (i < 6) + ucp->uc_mcontext.regs.gpr[i + 3] = va_arg (va, libucontext_greg_t); + else + sp[i - 6] = va_arg (va, libucontext_greg_t); + } + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/or1k/setcontext.S b/src/3rdparty/libucontext/arch/or1k/setcontext.S new file mode 100644 index 000000000..d25c8009e --- /dev/null +++ b/src/3rdparty/libucontext/arch/or1k/setcontext.S @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + l.ori r30, r3, 0 /* avoid clobbering r3 by copying to r30 */ + + l.lwz r3, REG_OFFSET(3)(r30) /* restore r3-r8 (argument registers) */ + l.lwz r4, REG_OFFSET(4)(r30) + l.lwz r5, REG_OFFSET(5)(r30) + l.lwz r6, REG_OFFSET(6)(r30) + l.lwz r7, REG_OFFSET(7)(r30) + l.lwz r8, REG_OFFSET(8)(r30) + + l.lwz r1, REG_OFFSET(1)(r30) /* restore stack/frame pointers */ + l.lwz r2, REG_OFFSET(2)(r30) + + l.lwz r9, REG_OFFSET(9)(r30) /* restore link register and starting address register */ + l.lwz r11, REG_OFFSET(11)(r30) + + l.lwz r10, REG_OFFSET(10)(r30) /* restore TLS register */ + + l.lwz r14, REG_OFFSET(14)(r30) /* restore r14-r30, even only */ + l.lwz r16, REG_OFFSET(16)(r30) + l.lwz r18, REG_OFFSET(18)(r30) + l.lwz r20, REG_OFFSET(20)(r30) + l.lwz r22, REG_OFFSET(22)(r30) + l.lwz r24, REG_OFFSET(24)(r30) + l.lwz r26, REG_OFFSET(26)(r30) + l.lwz r28, REG_OFFSET(28)(r30) + l.lwz r30, REG_OFFSET(30)(r30) + + l.jr r11 /* jump to new starting address */ + l.ori r11, r0, 0 +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/or1k/swapcontext.S b/src/3rdparty/libucontext/arch/or1k/swapcontext.S new file mode 100644 index 000000000..07198e524 --- /dev/null +++ b/src/3rdparty/libucontext/arch/or1k/swapcontext.S @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* copy context into r3 like getcontext */ + l.sw REG_OFFSET(1)(r3), r1 /* store r1 and r2 */ + l.sw REG_OFFSET(2)(r3), r2 + + l.sw REG_OFFSET(9)(r3), r9 /* store r9 to both r9 and r11 in the context */ + l.sw REG_OFFSET(11)(r3), r9 + + l.sw REG_OFFSET(10)(r3), r10 /* store r10 for TLS */ + + l.sw REG_OFFSET(14)(r3), r14 /* store r14 through r30 even */ + l.sw REG_OFFSET(16)(r3), r16 + l.sw REG_OFFSET(18)(r3), r18 + l.sw REG_OFFSET(20)(r3), r20 + l.sw REG_OFFSET(22)(r3), r22 + l.sw REG_OFFSET(24)(r3), r24 + l.sw REG_OFFSET(26)(r3), r26 + l.sw REG_OFFSET(28)(r3), r28 + l.sw REG_OFFSET(30)(r3), r30 + + /* set the new context from r4 */ + l.ori r30, r4, 0 /* copy r4 to r30 to avoid clobbering */ + + l.lwz r3, REG_OFFSET(3)(r30) /* restore r3-r8 (argument registers) */ + l.lwz r4, REG_OFFSET(4)(r30) + l.lwz r5, REG_OFFSET(5)(r30) + l.lwz r6, REG_OFFSET(6)(r30) + l.lwz r7, REG_OFFSET(7)(r30) + l.lwz r8, REG_OFFSET(8)(r30) + + l.lwz r1, REG_OFFSET(1)(r30) /* restore stack/frame pointers */ + l.lwz r2, REG_OFFSET(2)(r30) + + l.lwz r9, REG_OFFSET(9)(r30) /* restore link register and starting address register */ + l.lwz r11, REG_OFFSET(11)(r30) + + l.lwz r10, REG_OFFSET(10)(r30) /* restore TLS register */ + + l.lwz r14, REG_OFFSET(14)(r30) /* restore r14-r30, even only */ + l.lwz r16, REG_OFFSET(16)(r30) + l.lwz r18, REG_OFFSET(18)(r30) + l.lwz r20, REG_OFFSET(20)(r30) + l.lwz r22, REG_OFFSET(22)(r30) + l.lwz r24, REG_OFFSET(24)(r30) + l.lwz r26, REG_OFFSET(26)(r30) + l.lwz r28, REG_OFFSET(28)(r30) + l.lwz r30, REG_OFFSET(30)(r30) + + l.jr r11 /* jump to new starting address */ + l.ori r11, r0, 0 +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/or1k/trampoline.c b/src/3rdparty/libucontext/arch/or1k/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/or1k/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/arch/ppc/defs.h b/src/3rdparty/libucontext/arch/ppc/defs.h new file mode 100644 index 000000000..531e55120 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc/defs.h @@ -0,0 +1,64 @@ +#ifndef __ARCH_PPC_DEFS_H +#define __ARCH_PPC_DEFS_H + +#define REG_R0 (0) +#define REG_R1 (1) +#define REG_R2 (2) +#define REG_R3 (3) +#define REG_R4 (4) +#define REG_R5 (5) +#define REG_R6 (6) +#define REG_R7 (7) +#define REG_R8 (8) +#define REG_R9 (9) +#define REG_R10 (10) +#define REG_R11 (11) +#define REG_R12 (12) +#define REG_R13 (13) +#define REG_R14 (14) +#define REG_R15 (15) +#define REG_R16 (16) +#define REG_R17 (17) +#define REG_R18 (18) +#define REG_R19 (19) +#define REG_R20 (20) +#define REG_R21 (21) +#define REG_R22 (22) +#define REG_R23 (23) +#define REG_R24 (24) +#define REG_R25 (25) +#define REG_R26 (26) +#define REG_R27 (27) +#define REG_R28 (28) +#define REG_R29 (29) +#define REG_R30 (30) +#define REG_R31 (31) +#define REG_R32 (32) +#define REG_R33 (33) +#define REG_R34 (34) +#define REG_R35 (35) +#define REG_R36 (36) +#define REG_R37 (37) +#define REG_R38 (38) +#define REG_R39 (39) +#define REG_R40 (40) +#define REG_R41 (41) +#define REG_R42 (42) +#define REG_R43 (43) +#define REG_R44 (44) +#define REG_R45 (45) +#define REG_R46 (46) +#define REG_R47 (47) + +/* sp register is actually %r1 */ +#define REG_SP REG_R1 + +/* nip register is actually %srr0 (r32) */ +#define REG_NIP REG_R32 + +/* lnk register is actually r32 */ +#define REG_LNK REG_R36 + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/ppc/getcontext.S b/src/3rdparty/libucontext/arch/ppc/getcontext.S new file mode 100644 index 000000000..4920fd3c9 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc/getcontext.S @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "common-defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +.hidden __libucontext_swapcontext +FUNC(libucontext_getcontext) + li 4, 0 + b __libucontext_swapcontext@local +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/ppc/makecontext.c b/src/3rdparty/libucontext/arch/ppc/makecontext.c new file mode 100644 index 000000000..1b820384b --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc/makecontext.c @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include + + +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(), int argc, ...) +{ + libucontext_greg_t *sp; + va_list va; + int i; + unsigned int stack_args; + + stack_args = argc > 8 ? argc - 8 : 0; + + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= stack_args + 2; + sp = (libucontext_greg_t *) ((uintptr_t) sp & -16L); + + ucp->uc_mcontext.gregs[REG_NIP] = (uintptr_t) func; + ucp->uc_mcontext.gregs[REG_LNK] = (uintptr_t) &libucontext_trampoline; + ucp->uc_mcontext.gregs[REG_R31] = (uintptr_t) ucp->uc_link; + ucp->uc_mcontext.gregs[REG_SP] = (uintptr_t) sp; + + sp[0] = 0; + + va_start(va, argc); + + for (i = 0; i < argc; i++) { + if (i < 8) + ucp->uc_mcontext.gregs[i + 3] = va_arg (va, libucontext_greg_t); + else + sp[i-8 + 2] = va_arg (va, libucontext_greg_t); + } + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/ppc/retfromsyscall.c b/src/3rdparty/libucontext/arch/ppc/retfromsyscall.c new file mode 100644 index 000000000..150c13dcd --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc/retfromsyscall.c @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include + +__attribute__ ((visibility ("hidden"))) +int __retfromsyscall(long retval) +{ + if (retval < 0) { + errno = -retval; + return -1; + } + return 0; +} + diff --git a/src/3rdparty/libucontext/arch/ppc/setcontext.S b/src/3rdparty/libucontext/arch/ppc/setcontext.S new file mode 100644 index 000000000..d634e66f2 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc/setcontext.S @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "common-defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +.hidden __libucontext_swapcontext +FUNC(libucontext_setcontext) + mr 4, 3 + li 3, 0 + b __libucontext_swapcontext@local +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/ppc/startcontext.S b/src/3rdparty/libucontext/arch/ppc/startcontext.S new file mode 100644 index 000000000..783380871 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc/startcontext.S @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "common-defs.h" + +.hidden libucontext_trampoline +FUNC(libucontext_trampoline) + /* get the proper context into position and test for NULL */ + mr. 3,31 + + /* if we have no linked context, lets get out of here */ + beq no_linked_context + + /* jump to setcontext */ + bl libucontext_setcontext@local + +no_linked_context: + b exit@GOT +END(libucontext_trampoline) diff --git a/src/3rdparty/libucontext/arch/ppc/swapcontext.S b/src/3rdparty/libucontext/arch/ppc/swapcontext.S new file mode 100644 index 000000000..3d1efe016 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc/swapcontext.S @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "common-defs.h" + +ALIAS(swapcontext, __libucontext_swapcontext) +ALIAS(__swapcontext, __libucontext_swapcontext) + +/* make sure this is visible regardless of EXPORT_UNPREFIXED */ +.weak libucontext_swapcontext +libucontext_swapcontext = __libucontext_swapcontext + +FUNC(__libucontext_swapcontext) + li 0, 249 # SYS_swapcontext + li 5, 1184 # sizeof(ucontext_t) + sc + +.hidden __retfromsyscall + b __retfromsyscall@local +END(__libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/ppc64/defs.h b/src/3rdparty/libucontext/arch/ppc64/defs.h new file mode 100644 index 000000000..936591110 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc64/defs.h @@ -0,0 +1,67 @@ +#ifndef __ARCH_PPC_DEFS_H +#define __ARCH_PPC_DEFS_H + +#define REG_R0 (0) +#define REG_R1 (1) +#define REG_R2 (2) +#define REG_R3 (3) +#define REG_R4 (4) +#define REG_R5 (5) +#define REG_R6 (6) +#define REG_R7 (7) +#define REG_R8 (8) +#define REG_R9 (9) +#define REG_R10 (10) +#define REG_R11 (11) +#define REG_R12 (12) +#define REG_R13 (13) +#define REG_R14 (14) +#define REG_R15 (15) +#define REG_R16 (16) +#define REG_R17 (17) +#define REG_R18 (18) +#define REG_R19 (19) +#define REG_R20 (20) +#define REG_R21 (21) +#define REG_R22 (22) +#define REG_R23 (23) +#define REG_R24 (24) +#define REG_R25 (25) +#define REG_R26 (26) +#define REG_R27 (27) +#define REG_R28 (28) +#define REG_R29 (29) +#define REG_R30 (30) +#define REG_R31 (31) +#define REG_R32 (32) +#define REG_R33 (33) +#define REG_R34 (34) +#define REG_R35 (35) +#define REG_R36 (36) +#define REG_R37 (37) +#define REG_R38 (38) +#define REG_R39 (39) +#define REG_R40 (40) +#define REG_R41 (41) +#define REG_R42 (42) +#define REG_R43 (43) +#define REG_R44 (44) +#define REG_R45 (45) +#define REG_R46 (46) +#define REG_R47 (47) + +/* sp register is actually %r1 */ +#define REG_SP REG_R1 + +/* nip register is actually %srr0 (r32) */ +#define REG_NIP REG_R32 + +/* entry register is actually %r12 */ +#define REG_ENTRY REG_R12 + +/* lnk register is actually %r36 */ +#define REG_LNK REG_R36 + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/ppc64/getcontext.S b/src/3rdparty/libucontext/arch/ppc64/getcontext.S new file mode 100644 index 000000000..1a67424e7 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc64/getcontext.S @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "common-defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +.hidden __libucontext_swapcontext +FUNC(libucontext_getcontext) + addis 2, 12, .TOC.-libucontext_getcontext@ha + addi 2, 12, .TOC.-libucontext_getcontext@l + + .localentry libucontext_getcontext,.-libucontext_getcontext + + li 4, 0 + b __libucontext_swapcontext +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/ppc64/makecontext.c b/src/3rdparty/libucontext/arch/ppc64/makecontext.c new file mode 100644 index 000000000..15ddbf9f8 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc64/makecontext.c @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include + + +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(), int argc, ...) +{ + libucontext_greg_t *sp; + va_list va; + int i; + unsigned int stack_args; + + stack_args = argc > 8 ? argc : 0; + + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= stack_args + 4; + sp = (libucontext_greg_t *) ((uintptr_t) sp & -16L); + + ucp->uc_mcontext.gp_regs[REG_NIP] = (uintptr_t) func; + ucp->uc_mcontext.gp_regs[REG_LNK] = (uintptr_t) &libucontext_trampoline; + ucp->uc_mcontext.gp_regs[REG_SP] = (uintptr_t) sp; + ucp->uc_mcontext.gp_regs[REG_ENTRY] = (uintptr_t) func; + ucp->uc_mcontext.gp_regs[REG_R31] = (uintptr_t) ucp->uc_link; + + sp[0] = 0; + + va_start(va, argc); + + for (i = 0; i < argc; i++) { + if (i < 8) + ucp->uc_mcontext.gp_regs[i + 3] = va_arg (va, libucontext_greg_t); + else + sp[i + 4] = va_arg (va, libucontext_greg_t); + } + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/ppc64/retfromsyscall.c b/src/3rdparty/libucontext/arch/ppc64/retfromsyscall.c new file mode 100644 index 000000000..150c13dcd --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc64/retfromsyscall.c @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include + +__attribute__ ((visibility ("hidden"))) +int __retfromsyscall(long retval) +{ + if (retval < 0) { + errno = -retval; + return -1; + } + return 0; +} + diff --git a/src/3rdparty/libucontext/arch/ppc64/setcontext.S b/src/3rdparty/libucontext/arch/ppc64/setcontext.S new file mode 100644 index 000000000..0cd918673 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc64/setcontext.S @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "common-defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +.hidden __libucontext_swapcontext +FUNC(libucontext_setcontext) + addis 2, 12, .TOC.-libucontext_setcontext@ha + addi 2, 12, .TOC.-libucontext_setcontext@l + + .localentry libucontext_setcontext,.-libucontext_setcontext + + mr 4, 3 + li 3, 0 + b __libucontext_swapcontext +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/ppc64/startcontext.S b/src/3rdparty/libucontext/arch/ppc64/startcontext.S new file mode 100644 index 000000000..367f99d2f --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc64/startcontext.S @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "common-defs.h" + +.hidden libucontext_trampoline +FUNC(libucontext_trampoline) + cmpdi 31,0 /* test if ucontext link pointer is null */ + beq no_linked_context /* if it is, exit */ + + /* now, call SYS_swapcontext */ + mr 4,31 /* ucp is in r31 */ + li 3,0 /* don't care about restoring, set oucp to NULL */ + li 5,1696 /* sizeof(ucontext_t) */ + li 0,249 /* SYS_swapcontext */ + sc + + /* we should not wind back up here, if we do, exit with -1 */ + li 3,-1 + +no_linked_context: + b exit@GOT + nop +END(libucontext_trampoline) diff --git a/src/3rdparty/libucontext/arch/ppc64/swapcontext.S b/src/3rdparty/libucontext/arch/ppc64/swapcontext.S new file mode 100644 index 000000000..facd491d1 --- /dev/null +++ b/src/3rdparty/libucontext/arch/ppc64/swapcontext.S @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019 Bobby Bingham + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "common-defs.h" + +ALIAS(swapcontext, __libucontext_swapcontext) +ALIAS(__swapcontext, __libucontext_swapcontext) + +/* make sure this is visible regardless of EXPORT_UNPREFIXED */ +.weak libucontext_swapcontext +libucontext_swapcontext = __libucontext_swapcontext + +FUNC(__libucontext_swapcontext) + addis 2, 12, .TOC.-__libucontext_swapcontext@ha + addi 2, 12, .TOC.-__libucontext_swapcontext@l + + .localentry __libucontext_swapcontext,.-__libucontext_swapcontext + + li 0, 249 # SYS_swapcontext + li 5, 1696 # sizeof(ucontext_t) + sc + +.hidden __retfromsyscall + b __retfromsyscall +END(__libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/riscv32/defs.h b/src/3rdparty/libucontext/arch/riscv32/defs.h new file mode 100644 index 000000000..3f737116b --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv32/defs.h @@ -0,0 +1,55 @@ +#ifndef __ARCH_RISCV64_DEFS_H +#define __ARCH_RISCV64_DEFS_H + +#define REG_SZ (4) +#define MCONTEXT_GREGS (160) + +/* program counter is saved in x0 as well as x1, similar to mips */ +#ifndef REG_PC +#define REG_PC (0) +#endif + +#ifndef REG_RA +#define REG_RA (1) +#endif + +#ifndef REG_SP +#define REG_SP (2) +#endif + +#ifndef REG_S0 +#define REG_S0 (8) +#endif + +#define REG_S1 (9) + +#ifndef REG_A0 +#define REG_A0 (10) +#endif + +#define REG_A1 (11) +#define REG_A2 (12) +#define REG_A3 (13) +#define REG_A4 (14) +#define REG_A5 (15) +#define REG_A6 (16) +#define REG_A7 (17) +#define REG_S2 (18) +#define REG_S3 (19) +#define REG_S4 (20) +#define REG_S5 (21) +#define REG_S6 (22) +#define REG_S7 (23) +#define REG_S8 (24) +#define REG_S9 (25) +#define REG_S10 (26) +#define REG_S11 (27) + +#define PC_OFFSET REG_OFFSET(REG_PC) + +#define FETCH_LINKPTR(dest) \ + asm("mv %0, s1" : "=r" ((dest))) + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/riscv32/getcontext.S b/src/3rdparty/libucontext/arch/riscv32/getcontext.S new file mode 100644 index 000000000..888b6ab1e --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv32/getcontext.S @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + sw ra, REG_OFFSET(REG_PC)(a0) + sw ra, REG_OFFSET(REG_RA)(a0) + sw sp, REG_OFFSET(REG_SP)(a0) + + /* first saved register block */ + sw s0, REG_OFFSET(REG_S0)(a0) + sw s1, REG_OFFSET(REG_S1)(a0) + + /* return register block */ + sw a0, REG_OFFSET(REG_A0)(a0) + sw a1, REG_OFFSET(REG_A1)(a0) + + /* second saved register block */ + sw s2, REG_OFFSET(REG_S2)(a0) + sw s3, REG_OFFSET(REG_S3)(a0) + sw s4, REG_OFFSET(REG_S4)(a0) + sw s5, REG_OFFSET(REG_S5)(a0) + sw s6, REG_OFFSET(REG_S6)(a0) + sw s7, REG_OFFSET(REG_S7)(a0) + sw s8, REG_OFFSET(REG_S8)(a0) + sw s9, REG_OFFSET(REG_S9)(a0) + sw s10, REG_OFFSET(REG_S10)(a0) + sw s11, REG_OFFSET(REG_S11)(a0) + + /* done saving, return */ + ret +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/riscv32/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/riscv32/include/libucontext/bits.h new file mode 100644 index 000000000..8d7867608 --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv32/include/libucontext/bits.h @@ -0,0 +1,48 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef unsigned long libucontext_greg_t; +typedef unsigned long libucontext__riscv_mc_gp_state[32]; + +struct libucontext__riscv_mc_f_ext_state { + unsigned int __f[32]; + unsigned int __fcsr; +}; + +struct libucontext__riscv_mc_d_ext_state { + unsigned long long __f[32]; + unsigned int __fcsr; +}; + +struct libucontext__riscv_mc_q_ext_state { + unsigned long long __f[64] __attribute__((aligned(16))); + unsigned int __fcsr; + unsigned int __reserved[3]; +}; + +union libucontext__riscv_mc_fp_state { + struct libucontext__riscv_mc_f_ext_state __f; + struct libucontext__riscv_mc_d_ext_state __d; + struct libucontext__riscv_mc_q_ext_state __q; +}; + +typedef struct libucontext_mcontext { + libucontext__riscv_mc_gp_state __gregs; + union libucontext__riscv_mc_fp_state __fpregs; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + unsigned char __pad[128]; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/riscv32/makecontext.c b/src/3rdparty/libucontext/arch/riscv32/makecontext.c new file mode 100644 index 000000000..9a43ef4e9 --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv32/makecontext.c @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include +#include +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + libucontext_greg_t *sp, *regp; + va_list va; + int i; + + /* set up and align the stack. */ + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= argc < 8 ? 0 : argc - 8; + sp = (libucontext_greg_t *) (((uintptr_t) sp & -16L)); + + /* set up the ucontext structure */ + ucp->uc_mcontext.__gregs[REG_RA] = (libucontext_greg_t) libucontext_trampoline; + ucp->uc_mcontext.__gregs[REG_S0] = 0; + ucp->uc_mcontext.__gregs[REG_S1] = (libucontext_greg_t) ucp->uc_link; + ucp->uc_mcontext.__gregs[REG_SP] = (libucontext_greg_t) sp; + ucp->uc_mcontext.__gregs[REG_PC] = (libucontext_greg_t) func; + + va_start(va, argc); + + /* first 8 args go in $a0 through $a7. */ + regp = &(ucp->uc_mcontext.__gregs[REG_A0]); + + for (i = 0; (i < argc && i < 8); i++) + *regp++ = va_arg (va, libucontext_greg_t); + + /* remainder overflows into stack */ + for (; i < argc; i++) + *sp++ = va_arg (va, libucontext_greg_t); + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/riscv32/setcontext.S b/src/3rdparty/libucontext/arch/riscv32/setcontext.S new file mode 100644 index 000000000..06727851e --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv32/setcontext.S @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + /* move $a0 to $t0 to avoid clobbering. */ + mv t0, a0 + + lw t1, PC_OFFSET(t0) + lw ra, REG_OFFSET(REG_RA)(t0) + lw sp, REG_OFFSET(REG_SP)(t0) + + /* first saved register block */ + lw s0, REG_OFFSET(REG_S0)(t0) + lw s1, REG_OFFSET(REG_S1)(t0) + + /* return register block */ + lw a0, REG_OFFSET(REG_A0)(t0) + lw a1, REG_OFFSET(REG_A1)(t0) + + /* argument register block */ + lw a2, REG_OFFSET(REG_A2)(t0) + lw a3, REG_OFFSET(REG_A3)(t0) + lw a4, REG_OFFSET(REG_A4)(t0) + lw a5, REG_OFFSET(REG_A5)(t0) + lw a6, REG_OFFSET(REG_A6)(t0) + lw a7, REG_OFFSET(REG_A7)(t0) + + /* second saved register block */ + lw s2, REG_OFFSET(REG_S2)(t0) + lw s3, REG_OFFSET(REG_S3)(t0) + lw s4, REG_OFFSET(REG_S4)(t0) + lw s5, REG_OFFSET(REG_S5)(t0) + lw s6, REG_OFFSET(REG_S6)(t0) + lw s7, REG_OFFSET(REG_S7)(t0) + lw s8, REG_OFFSET(REG_S8)(t0) + lw s9, REG_OFFSET(REG_S9)(t0) + lw s10, REG_OFFSET(REG_S10)(t0) + lw s11, REG_OFFSET(REG_S11)(t0) + + /* done restoring, jump to new pc in S1 */ + jr t1 +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/riscv32/swapcontext.S b/src/3rdparty/libucontext/arch/riscv32/swapcontext.S new file mode 100644 index 000000000..a4c7138a9 --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv32/swapcontext.S @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* move $a1 to $t0 to avoid clobbering. */ + mv t0, a1 + + sw ra, REG_OFFSET(REG_PC)(a0) + sw ra, REG_OFFSET(REG_RA)(a0) + sw sp, REG_OFFSET(REG_SP)(a0) + + /* first saved register block */ + sw s0, REG_OFFSET(REG_S0)(a0) + sw s1, REG_OFFSET(REG_S1)(a0) + + /* return register block */ + sw a0, REG_OFFSET(REG_A0)(a0) + sw a1, REG_OFFSET(REG_A1)(a0) + + /* second saved register block */ + sw s2, REG_OFFSET(REG_S2)(a0) + sw s3, REG_OFFSET(REG_S3)(a0) + sw s4, REG_OFFSET(REG_S4)(a0) + sw s5, REG_OFFSET(REG_S5)(a0) + sw s6, REG_OFFSET(REG_S6)(a0) + sw s7, REG_OFFSET(REG_S7)(a0) + sw s8, REG_OFFSET(REG_S8)(a0) + sw s9, REG_OFFSET(REG_S9)(a0) + sw s10, REG_OFFSET(REG_S10)(a0) + sw s11, REG_OFFSET(REG_S11)(a0) + + /* restore the other context from $t0. */ + lw t1, REG_OFFSET(REG_PC)(t0) + lw ra, REG_OFFSET(REG_RA)(t0) + lw sp, REG_OFFSET(REG_SP)(t0) + + /* first saved register block */ + lw s0, REG_OFFSET(REG_S0)(t0) + lw s1, REG_OFFSET(REG_S1)(t0) + + /* return register block */ + lw a0, REG_OFFSET(REG_A0)(t0) + lw a1, REG_OFFSET(REG_A1)(t0) + + /* argument register block */ + lw a2, REG_OFFSET(REG_A2)(t0) + lw a3, REG_OFFSET(REG_A3)(t0) + lw a4, REG_OFFSET(REG_A4)(t0) + lw a5, REG_OFFSET(REG_A5)(t0) + lw a6, REG_OFFSET(REG_A6)(t0) + lw a7, REG_OFFSET(REG_A7)(t0) + + /* second saved register block */ + lw s2, REG_OFFSET(REG_S2)(t0) + lw s3, REG_OFFSET(REG_S3)(t0) + lw s4, REG_OFFSET(REG_S4)(t0) + lw s5, REG_OFFSET(REG_S5)(t0) + lw s6, REG_OFFSET(REG_S6)(t0) + lw s7, REG_OFFSET(REG_S7)(t0) + lw s8, REG_OFFSET(REG_S8)(t0) + lw s9, REG_OFFSET(REG_S9)(t0) + lw s10, REG_OFFSET(REG_S10)(t0) + lw s11, REG_OFFSET(REG_S11)(t0) + + /* done swapping, jump to new PC in S1 */ + jr t1 +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/riscv32/trampoline.c b/src/3rdparty/libucontext/arch/riscv32/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv32/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/arch/riscv64/defs.h b/src/3rdparty/libucontext/arch/riscv64/defs.h new file mode 100644 index 000000000..d9b764713 --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv64/defs.h @@ -0,0 +1,55 @@ +#ifndef __ARCH_RISCV64_DEFS_H +#define __ARCH_RISCV64_DEFS_H + +#define REG_SZ (8) +#define MCONTEXT_GREGS (176) + +/* program counter is saved in x0 as well as x1, similar to mips */ +#ifndef REG_PC +#define REG_PC (0) +#endif + +#ifndef REG_RA +#define REG_RA (1) +#endif + +#ifndef REG_SP +#define REG_SP (2) +#endif + +#ifndef REG_S0 +#define REG_S0 (8) +#endif + +#define REG_S1 (9) + +#ifndef REG_A0 +#define REG_A0 (10) +#endif + +#define REG_A1 (11) +#define REG_A2 (12) +#define REG_A3 (13) +#define REG_A4 (14) +#define REG_A5 (15) +#define REG_A6 (16) +#define REG_A7 (17) +#define REG_S2 (18) +#define REG_S3 (19) +#define REG_S4 (20) +#define REG_S5 (21) +#define REG_S6 (22) +#define REG_S7 (23) +#define REG_S8 (24) +#define REG_S9 (25) +#define REG_S10 (26) +#define REG_S11 (27) + +#define PC_OFFSET REG_OFFSET(REG_PC) + +#define FETCH_LINKPTR(dest) \ + asm("mv %0, s1" : "=r" ((dest))) + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/riscv64/getcontext.S b/src/3rdparty/libucontext/arch/riscv64/getcontext.S new file mode 100644 index 000000000..99a9c9ad9 --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv64/getcontext.S @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + sd ra, REG_OFFSET(REG_PC)(a0) + sd ra, REG_OFFSET(REG_RA)(a0) + sd sp, REG_OFFSET(REG_SP)(a0) + + /* first saved register block */ + sd s0, REG_OFFSET(REG_S0)(a0) + sd s1, REG_OFFSET(REG_S1)(a0) + + /* return register block */ + sd a0, REG_OFFSET(REG_A0)(a0) + sd a1, REG_OFFSET(REG_A1)(a0) + + /* second saved register block */ + sd s2, REG_OFFSET(REG_S2)(a0) + sd s3, REG_OFFSET(REG_S3)(a0) + sd s4, REG_OFFSET(REG_S4)(a0) + sd s5, REG_OFFSET(REG_S5)(a0) + sd s6, REG_OFFSET(REG_S6)(a0) + sd s7, REG_OFFSET(REG_S7)(a0) + sd s8, REG_OFFSET(REG_S8)(a0) + sd s9, REG_OFFSET(REG_S9)(a0) + sd s10, REG_OFFSET(REG_S10)(a0) + sd s11, REG_OFFSET(REG_S11)(a0) + + /* done saving, return */ + ret +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/riscv64/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/riscv64/include/libucontext/bits.h new file mode 100644 index 000000000..8d7867608 --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv64/include/libucontext/bits.h @@ -0,0 +1,48 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef unsigned long libucontext_greg_t; +typedef unsigned long libucontext__riscv_mc_gp_state[32]; + +struct libucontext__riscv_mc_f_ext_state { + unsigned int __f[32]; + unsigned int __fcsr; +}; + +struct libucontext__riscv_mc_d_ext_state { + unsigned long long __f[32]; + unsigned int __fcsr; +}; + +struct libucontext__riscv_mc_q_ext_state { + unsigned long long __f[64] __attribute__((aligned(16))); + unsigned int __fcsr; + unsigned int __reserved[3]; +}; + +union libucontext__riscv_mc_fp_state { + struct libucontext__riscv_mc_f_ext_state __f; + struct libucontext__riscv_mc_d_ext_state __d; + struct libucontext__riscv_mc_q_ext_state __q; +}; + +typedef struct libucontext_mcontext { + libucontext__riscv_mc_gp_state __gregs; + union libucontext__riscv_mc_fp_state __fpregs; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + unsigned char __pad[128]; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/riscv64/makecontext.c b/src/3rdparty/libucontext/arch/riscv64/makecontext.c new file mode 100644 index 000000000..9a43ef4e9 --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv64/makecontext.c @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include +#include +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + libucontext_greg_t *sp, *regp; + va_list va; + int i; + + /* set up and align the stack. */ + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= argc < 8 ? 0 : argc - 8; + sp = (libucontext_greg_t *) (((uintptr_t) sp & -16L)); + + /* set up the ucontext structure */ + ucp->uc_mcontext.__gregs[REG_RA] = (libucontext_greg_t) libucontext_trampoline; + ucp->uc_mcontext.__gregs[REG_S0] = 0; + ucp->uc_mcontext.__gregs[REG_S1] = (libucontext_greg_t) ucp->uc_link; + ucp->uc_mcontext.__gregs[REG_SP] = (libucontext_greg_t) sp; + ucp->uc_mcontext.__gregs[REG_PC] = (libucontext_greg_t) func; + + va_start(va, argc); + + /* first 8 args go in $a0 through $a7. */ + regp = &(ucp->uc_mcontext.__gregs[REG_A0]); + + for (i = 0; (i < argc && i < 8); i++) + *regp++ = va_arg (va, libucontext_greg_t); + + /* remainder overflows into stack */ + for (; i < argc; i++) + *sp++ = va_arg (va, libucontext_greg_t); + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/riscv64/setcontext.S b/src/3rdparty/libucontext/arch/riscv64/setcontext.S new file mode 100644 index 000000000..36704c91a --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv64/setcontext.S @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + /* move $a0 to $t0 to avoid clobbering. */ + mv t0, a0 + + ld t1, PC_OFFSET(t0) + ld ra, REG_OFFSET(REG_RA)(t0) + ld sp, REG_OFFSET(REG_SP)(t0) + + /* first saved register block */ + ld s0, REG_OFFSET(REG_S0)(t0) + ld s1, REG_OFFSET(REG_S1)(t0) + + /* return register block */ + ld a0, REG_OFFSET(REG_A0)(t0) + ld a1, REG_OFFSET(REG_A1)(t0) + + /* argument register block */ + ld a2, REG_OFFSET(REG_A2)(t0) + ld a3, REG_OFFSET(REG_A3)(t0) + ld a4, REG_OFFSET(REG_A4)(t0) + ld a5, REG_OFFSET(REG_A5)(t0) + ld a6, REG_OFFSET(REG_A6)(t0) + ld a7, REG_OFFSET(REG_A7)(t0) + + /* second saved register block */ + ld s2, REG_OFFSET(REG_S2)(t0) + ld s3, REG_OFFSET(REG_S3)(t0) + ld s4, REG_OFFSET(REG_S4)(t0) + ld s5, REG_OFFSET(REG_S5)(t0) + ld s6, REG_OFFSET(REG_S6)(t0) + ld s7, REG_OFFSET(REG_S7)(t0) + ld s8, REG_OFFSET(REG_S8)(t0) + ld s9, REG_OFFSET(REG_S9)(t0) + ld s10, REG_OFFSET(REG_S10)(t0) + ld s11, REG_OFFSET(REG_S11)(t0) + + /* done restoring, jump to new pc in S1 */ + jr t1 +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/riscv64/swapcontext.S b/src/3rdparty/libucontext/arch/riscv64/swapcontext.S new file mode 100644 index 000000000..a2b013e1c --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv64/swapcontext.S @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* move $a1 to $t0 to avoid clobbering. */ + mv t0, a1 + + sd ra, REG_OFFSET(REG_PC)(a0) + sd ra, REG_OFFSET(REG_RA)(a0) + sd sp, REG_OFFSET(REG_SP)(a0) + + /* first saved register block */ + sd s0, REG_OFFSET(REG_S0)(a0) + sd s1, REG_OFFSET(REG_S1)(a0) + + /* return register block */ + sd a0, REG_OFFSET(REG_A0)(a0) + sd a1, REG_OFFSET(REG_A1)(a0) + + /* second saved register block */ + sd s2, REG_OFFSET(REG_S2)(a0) + sd s3, REG_OFFSET(REG_S3)(a0) + sd s4, REG_OFFSET(REG_S4)(a0) + sd s5, REG_OFFSET(REG_S5)(a0) + sd s6, REG_OFFSET(REG_S6)(a0) + sd s7, REG_OFFSET(REG_S7)(a0) + sd s8, REG_OFFSET(REG_S8)(a0) + sd s9, REG_OFFSET(REG_S9)(a0) + sd s10, REG_OFFSET(REG_S10)(a0) + sd s11, REG_OFFSET(REG_S11)(a0) + + /* restore the other context from $t0. */ + ld t1, REG_OFFSET(REG_PC)(t0) + ld ra, REG_OFFSET(REG_RA)(t0) + ld sp, REG_OFFSET(REG_SP)(t0) + + /* first saved register block */ + ld s0, REG_OFFSET(REG_S0)(t0) + ld s1, REG_OFFSET(REG_S1)(t0) + + /* return register block */ + ld a0, REG_OFFSET(REG_A0)(t0) + ld a1, REG_OFFSET(REG_A1)(t0) + + /* argument register block */ + ld a2, REG_OFFSET(REG_A2)(t0) + ld a3, REG_OFFSET(REG_A3)(t0) + ld a4, REG_OFFSET(REG_A4)(t0) + ld a5, REG_OFFSET(REG_A5)(t0) + ld a6, REG_OFFSET(REG_A6)(t0) + ld a7, REG_OFFSET(REG_A7)(t0) + + /* second saved register block */ + ld s2, REG_OFFSET(REG_S2)(t0) + ld s3, REG_OFFSET(REG_S3)(t0) + ld s4, REG_OFFSET(REG_S4)(t0) + ld s5, REG_OFFSET(REG_S5)(t0) + ld s6, REG_OFFSET(REG_S6)(t0) + ld s7, REG_OFFSET(REG_S7)(t0) + ld s8, REG_OFFSET(REG_S8)(t0) + ld s9, REG_OFFSET(REG_S9)(t0) + ld s10, REG_OFFSET(REG_S10)(t0) + ld s11, REG_OFFSET(REG_S11)(t0) + + /* done swapping, jump to new PC in S1 */ + jr t1 +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/riscv64/trampoline.c b/src/3rdparty/libucontext/arch/riscv64/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/riscv64/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/arch/s390x/defs.h b/src/3rdparty/libucontext/arch/s390x/defs.h new file mode 100644 index 000000000..cad21845f --- /dev/null +++ b/src/3rdparty/libucontext/arch/s390x/defs.h @@ -0,0 +1,15 @@ +#ifndef __ARCH_S390X_DEFS_H +#define __ARCH_S390X_DEFS_H + +#define REG_SZ (8) +#define AREG_SZ (4) + +#define MCONTEXT_GREGS (56) +#define MCONTEXT_AREGS (184) +#define MCONTEXT_FPREGS (248) + +#define AREG_OFFSET(__reg) (MCONTEXT_AREGS + ((__reg) * AREG_SZ)) + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/s390x/getcontext.S b/src/3rdparty/libucontext/arch/s390x/getcontext.S new file mode 100644 index 000000000..fd19d9b80 --- /dev/null +++ b/src/3rdparty/libucontext/arch/s390x/getcontext.S @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + lgr %r1, %r2 /* use %r1 as our working register */ + la %r2, 0 /* we will return 0 */ + + stam %a0, %a15, AREG_OFFSET(0)(%r1) /* store access registers */ + stmg %r0, %r15, REG_OFFSET(0)(%r1) /* store general-purpose registers */ + + br %r14 /* return to where we came from */ +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/s390x/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/s390x/include/libucontext/bits.h new file mode 100644 index 000000000..bce841cbc --- /dev/null +++ b/src/3rdparty/libucontext/arch/s390x/include/libucontext/bits.h @@ -0,0 +1,41 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef unsigned long libucontext_greg_t, libucontext_gregset_t[27]; + +typedef struct { + unsigned long mask; + unsigned long addr; +} libucontext_psw_t; + +typedef union { + double d; + float f; +} libucontext_fpreg_t; + +typedef struct { + unsigned fpc; + libucontext_fpreg_t fprs[16]; +} libucontext_fpregset_t; + +typedef struct { + libucontext_psw_t psw; + unsigned long gregs[16]; + unsigned aregs[16]; + libucontext_fpregset_t fpregs; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/s390x/makecontext.c b/src/3rdparty/libucontext/arch/s390x/makecontext.c new file mode 100644 index 000000000..22ce2527b --- /dev/null +++ b/src/3rdparty/libucontext/arch/s390x/makecontext.c @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include + + +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); +extern int libucontext_setcontext(const libucontext_ucontext_t *ucp); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + libucontext_greg_t *sp; + va_list va; + int i; + + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp = (libucontext_greg_t *) (((uintptr_t) sp & -8L)); + + ucp->uc_mcontext.gregs[7] = (uintptr_t) func; + ucp->uc_mcontext.gregs[8] = (uintptr_t) ucp->uc_link; + ucp->uc_mcontext.gregs[9] = (uintptr_t) &libucontext_setcontext; + ucp->uc_mcontext.gregs[14] = (uintptr_t) &libucontext_trampoline; + + va_start(va, argc); + + for (i = 0; i < argc && i < 5; i++) + ucp->uc_mcontext.gregs[i + 2] = va_arg (va, libucontext_greg_t); + + if (argc > 5) + { + sp -= argc - 5; + + for (i = 5; i < argc; i++) + sp[i - 5] = va_arg (va, libucontext_greg_t); + } + + va_end(va); + + /* make room for backchain / register save area */ + sp -= 20; + *sp = 0; + + /* set up %r15 as sp */ + ucp->uc_mcontext.gregs[15] = (uintptr_t) sp; +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/s390x/setcontext.S b/src/3rdparty/libucontext/arch/s390x/setcontext.S new file mode 100644 index 000000000..03fd4a175 --- /dev/null +++ b/src/3rdparty/libucontext/arch/s390x/setcontext.S @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + lgr %r1, %r2 /* use %r1 as our working register */ + + lam %a2, %a15, AREG_OFFSET(2)(%r1) /* load access registers, but skip %a0 and %a1 which are for TLS */ + lmg %r0, %r15, REG_OFFSET(0)(%r1) /* store general-purpose registers */ + + br %r14 /* return to new link register address */ +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/s390x/startcontext.S b/src/3rdparty/libucontext/arch/s390x/startcontext.S new file mode 100644 index 000000000..16569437d --- /dev/null +++ b/src/3rdparty/libucontext/arch/s390x/startcontext.S @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +.hidden libucontext_trampoline +FUNC(libucontext_trampoline) + basr %r14, %r7 /* run function pointer (%r7) and return here */ + ltgr %r8, %r8 /* check to see if uc_link (%r8) is null */ + + jz no_linked_context /* if we have no linked context, prepare to exit */ + + lgr %r2, %r8 /* copy the uc_link structure address to %r2 */ + br %r9 /* call setcontext */ + +no_linked_context: + la %r2, 0 /* return 0 */ + brasl %r14, exit@plt /* call exit */ + + j .+2 /* crash if exit returns */ +END(libucontext_trampoline) diff --git a/src/3rdparty/libucontext/arch/s390x/swapcontext.S b/src/3rdparty/libucontext/arch/s390x/swapcontext.S new file mode 100644 index 000000000..21a9b5ad6 --- /dev/null +++ b/src/3rdparty/libucontext/arch/s390x/swapcontext.S @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + lgr %r1, %r2 /* use %r1 to save current context to */ + lgr %r0, %r3 /* use %r0 for source context */ + + stam %a0, %a15, AREG_OFFSET(0)(%r1) /* store access registers */ + stmg %r0, %r15, REG_OFFSET(0)(%r1) /* store general-purpose registers */ + + lgr %r2, %r0 /* swap %r0 to %r2 (XXX: figure out why it hates loading from %r0) */ + lam %a2, %a15, AREG_OFFSET(2)(%r2) /* load access registers, but skip %a0 and %a1 which are for TLS */ + lmg %r0, %r15, REG_OFFSET(0)(%r2) /* load general-purpose registers */ + + br %r14 /* return to new link register address */ +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/sh/defs.h b/src/3rdparty/libucontext/arch/sh/defs.h new file mode 100644 index 000000000..7cddaeffd --- /dev/null +++ b/src/3rdparty/libucontext/arch/sh/defs.h @@ -0,0 +1,20 @@ +#ifndef __ARCH_SH4_DEFS_H +#define __ARCH_SH4_DEFS_H + +#define REG_SZ (4) +#define MCONTEXT_GREGS (24) + +#define REG_SP (15) +#define REG_PC (16) +#define REG_PR (17) +#define REG_SR (18) +#define REG_GBR (19) +#define REG_MACH (20) +#define REG_MACL (21) + +#define FETCH_LINKPTR(dest) \ + asm("mov r8, %0" : "=r" (dest)); + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/sh/getcontext.S b/src/3rdparty/libucontext/arch/sh/getcontext.S new file mode 100644 index 000000000..0978af610 --- /dev/null +++ b/src/3rdparty/libucontext/arch/sh/getcontext.S @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + mov r4, r0 /* move r4 to r0, and increment by REG_OFFSET(REG_MACL) + REG_SZ. */ + add #(REG_OFFSET(REG_MACL + 1)), r0 + + sts.l macl, @-r0 /* save macl/mach registers */ + sts.l mach, @-r0 + + stc.l gbr, @-r0 /* save gbr register */ + + movt r1 /* load T-flag into r1 */ + mov.l r1, @-r0 /* save T-flag as SR register */ + + sts.l pr, @-r0 /* save current PR */ + sts.l pr, @-r0 /* save current PR as PC as well */ + + mov.l r15, @-r0 /* preserve registers backwards, from r15 to r1 */ + mov.l r14, @-r0 + mov.l r13, @-r0 + mov.l r12, @-r0 + mov.l r11, @-r0 + mov.l r10, @-r0 + mov.l r9, @-r0 + mov.l r8, @-r0 + mov.l r7, @-r0 + mov.l r6, @-r0 + mov.l r5, @-r0 + mov.l r4, @-r0 + mov.l r3, @-r0 + mov.l r2, @-r0 + mov.l r1, @-r0 + + mov r0, r1 + mov #0, r0 + + mov.l r0, @-r1 /* preserve r0 as explicit zero */ + + mov #0, r0 /* set return value as zero */ + rts +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/sh/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/sh/include/libucontext/bits.h new file mode 100644 index 000000000..3db92add4 --- /dev/null +++ b/src/3rdparty/libucontext/arch/sh/include/libucontext/bits.h @@ -0,0 +1,29 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +typedef unsigned long libucontext_greg_t, libucontext_gregset_t[16]; +typedef unsigned long libucontext_freg_t, libucontext_fpregset_t[16]; +typedef struct sigcontext { + unsigned long oldmask; + unsigned long gregs[16]; + unsigned long pc, pr, sr; + unsigned long gbr, mach, macl; + unsigned long fpregs[16]; + unsigned long xfpregs[16]; + unsigned int fpscr, fpul, ownedfp; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/sh/makecontext.c b/src/3rdparty/libucontext/arch/sh/makecontext.c new file mode 100644 index 000000000..7e28e2aff --- /dev/null +++ b/src/3rdparty/libucontext/arch/sh/makecontext.c @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include +#include +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + libucontext_greg_t *sp, *regp; + va_list va; + int i; + + /* set up and align the stack */ + sp = (libucontext_greg_t *) (((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size) & -4L); + sp -= argc > 4 ? argc - 4 : 0; + + /* set up the context */ + ucp->uc_mcontext.gregs[REG_SP] = (libucontext_greg_t) sp; + ucp->uc_mcontext.pr = (libucontext_greg_t) libucontext_trampoline; + ucp->uc_mcontext.pc = (libucontext_greg_t) func; + ucp->uc_mcontext.gregs[8] = (libucontext_greg_t) ucp->uc_link; + + /* pass up to four args in r4-r7, rest on stack */ + va_start(va, argc); + + regp = &ucp->uc_mcontext.gregs[4]; + + for (i = 0; i < argc && i < 4; i++) + *regp++ = va_arg(va, libucontext_greg_t); + + for (; i < argc; i++) + *sp++ = va_arg(va, libucontext_greg_t); + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/sh/setcontext.S b/src/3rdparty/libucontext/arch/sh/setcontext.S new file mode 100644 index 000000000..cee065c0f --- /dev/null +++ b/src/3rdparty/libucontext/arch/sh/setcontext.S @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + mov r4, r0 + + add #(REG_OFFSET(5)), r0 /* restore GPRs r5-15 */ + mov.l @r0+, r5 + mov.l @r0+, r6 + mov.l @r0+, r7 + mov.l @r0+, r8 + mov.l @r0+, r9 + mov.l @r0+, r10 + mov.l @r0+, r11 + mov.l @r0+, r12 + mov.l @r0+, r13 + mov.l @r0+, r14 + mov.l @r0+, r15 + + mov.l @r0+, r2 /* restore PR */ + lds.l @r0+, pr + + mov.l @r0+, r1 /* restore T-flag */ + shlr r1 + + add #REG_SZ, r0 /* skip GBR (used for TLS) */ + + lds.l @r0+, mach /* load mach/macl registers */ + lds.l @r0+, macl + + mov r4, r0 /* bring r0 back to the top of the context */ + add #(REG_OFFSET(0)), r1 /* restore r0 into r1 (temporarily) */ + mov.l r1, @-r15 /* push to stack from r1 */ + mov.l r2, @-r15 /* push PC to stack */ + + mov.l @(REG_OFFSET(1), r0), r1 /* restore real r1 */ + mov.l @(REG_OFFSET(2), r0), r2 /* restore real r2 */ + mov.l @(REG_OFFSET(3), r0), r3 /* restore real r2 */ + mov.l @(REG_OFFSET(4), r0), r4 /* restore real r2 */ + + mov.l @r15+, r0 /* pop PC from stack */ + + jmp @r0 /* jump to new PC */ + + mov.l @r15+, r0 /* pop original r0 from stack */ +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/sh/swapcontext.S b/src/3rdparty/libucontext/arch/sh/swapcontext.S new file mode 100644 index 000000000..289061373 --- /dev/null +++ b/src/3rdparty/libucontext/arch/sh/swapcontext.S @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + mov r4, r0 /* move r4 to r0, and increment by REG_OFFSET(REG_MACL) + REG_SZ. */ + add #(REG_OFFSET(REG_MACL + 1)), r0 + + sts.l macl, @-r0 /* save macl/mach registers */ + sts.l mach, @-r0 + + stc.l gbr, @-r0 /* save gbr register */ + + movt r1 /* load T-flag into r1 */ + mov.l r1, @-r0 /* save T-flag as SR register */ + + sts.l pr, @-r0 /* save current PR */ + sts.l pr, @-r0 /* save current PR as PC as well */ + + mov.l r15, @-r0 /* preserve registers backwards, from r15 to r1 */ + mov.l r14, @-r0 + mov.l r13, @-r0 + mov.l r12, @-r0 + mov.l r11, @-r0 + mov.l r10, @-r0 + mov.l r9, @-r0 + mov.l r8, @-r0 + mov.l r7, @-r0 + mov.l r6, @-r0 + mov.l r5, @-r0 + mov.l r4, @-r0 + mov.l r3, @-r0 + mov.l r2, @-r0 + mov.l r1, @-r0 + + mov r0, r1 + mov #0, r0 + + mov.l r0, @-r1 /* preserve r0 as explicit zero */ + + mov r5, r0 /* now restore the new context */ + + add #(REG_OFFSET(6)), r0 /* restore GPRs r6-15 */ + mov.l @r0+, r6 + mov.l @r0+, r7 + mov.l @r0+, r8 + mov.l @r0+, r9 + mov.l @r0+, r10 + mov.l @r0+, r11 + mov.l @r0+, r12 + mov.l @r0+, r13 + mov.l @r0+, r14 + mov.l @r0+, r15 + + mov.l @r0+, r2 /* restore PR */ + lds.l @r0+, pr + + mov.l @r0+, r1 /* restore T-flag */ + shlr r1 + + add #REG_SZ, r0 /* skip GBR (used for TLS) */ + + lds.l @r0+, mach /* load mach/macl registers */ + lds.l @r0+, macl + + mov r5, r0 /* bring r0 back to the top of the context */ + add #(REG_OFFSET(0)), r1 /* restore r0 into r1 (temporarily) */ + mov.l r1, @-r15 /* push to stack from r1 */ + mov.l r2, @-r15 /* push PC to stack */ + + mov.l @(REG_OFFSET(1), r0), r1 /* restore real r1 */ + mov.l @(REG_OFFSET(2), r0), r2 /* restore real r2 */ + mov.l @(REG_OFFSET(3), r0), r3 /* restore real r3 */ + mov.l @(REG_OFFSET(4), r0), r4 /* restore real r4 */ + mov.l @(REG_OFFSET(5), r0), r5 /* restore real r5 */ + + mov.l @r15+, r0 /* pop PC from stack */ + + jmp @r0 /* jump to new PC */ + + mov.l @r15+, r0 /* pop original r0 from stack */ +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/sh/trampoline.c b/src/3rdparty/libucontext/arch/sh/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/sh/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/arch/x86/defs.h b/src/3rdparty/libucontext/arch/x86/defs.h new file mode 100644 index 000000000..9370869a3 --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86/defs.h @@ -0,0 +1,65 @@ +#ifndef __ARCH_X86_DEFS_H +#define __ARCH_X86_DEFS_H + +#ifndef REG_GS +# define REG_GS (0) +#endif + +#ifndef REG_FS +# define REG_FS (1) +#endif + +#ifndef REG_ES +# define REG_ES (2) +#endif + +#ifndef REG_DS +# define REG_DS (3) +#endif + +#ifndef REG_EDI +# define REG_EDI (4) +#endif + +#ifndef REG_ESI +# define REG_ESI (5) +#endif + +#ifndef REG_EBP +# define REG_EBP (6) +#endif + +#ifndef REG_ESP +# define REG_ESP (7) +#endif + +#ifndef REG_EBX +# define REG_EBX (8) +#endif + +#ifndef REG_EDX +# define REG_EDX (9) +#endif + +#ifndef REG_ECX +# define REG_ECX (10) +#endif + +#ifndef REG_EAX +# define REG_EAX (11) +#endif + +#ifndef REG_EIP +# define REG_EIP (14) +#endif + +#define REG_SZ (4) + +#define MCONTEXT_GREGS (20) + +#define FETCH_LINKPTR(dest) \ + asm("movl (%%esp, %%ebx, 4), %0" : "=r" ((dest))); + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/x86/getcontext.S b/src/3rdparty/libucontext/arch/x86/getcontext.S new file mode 100644 index 000000000..4b9519786 --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86/getcontext.S @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + /* load address of the ucontext structure */ + movl 4(%esp), %eax + + /* EAX is not a preserved register */ + movl $0, REG_OFFSET(REG_EAX)(%eax) + + /* copy all of the current registers into the ucontext structure */ + movl %ecx, REG_OFFSET(REG_ECX)(%eax) + movl %ebx, REG_OFFSET(REG_EBX)(%eax) + movl %edx, REG_OFFSET(REG_EDX)(%eax) + movl %edi, REG_OFFSET(REG_EDI)(%eax) + movl %esi, REG_OFFSET(REG_ESI)(%eax) + movl %ebp, REG_OFFSET(REG_EBP)(%eax) + + /* the first argument on the stack is the jump target (%eip), so we store it in the EIP + register in the ucontext structure. */ + movl (%esp), %ecx + movl %ecx, REG_OFFSET(REG_EIP)(%eax) + + /* take the stack pointer address (%esp) offsetting by 4 to skip over the jump target. */ + leal 4(%esp), %ecx + movl %ecx, REG_OFFSET(REG_ESP)(%eax) + + /* finally, save the FS segment register */ + xorl %ecx, %ecx + movw %fs, %cx + movl %ecx, REG_OFFSET(REG_FS)(%eax) + + /* we're all done here, return 0 */ + xorl %eax, %eax + ret +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/x86/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/x86/include/libucontext/bits.h new file mode 100644 index 000000000..c4b28aa29 --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86/include/libucontext/bits.h @@ -0,0 +1,48 @@ +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +#define REG_GS (0) +#define REG_FS (1) +#define REG_ES (2) +#define REG_DS (3) +#define REG_EDI (4) +#define REG_ESI (5) +#define REG_EBP (6) +#define REG_ESP (7) +#define REG_EBX (8) +#define REG_EDX (9) +#define REG_ECX (10) +#define REG_EAX (11) +#define REG_EIP (14) + +typedef int libucontext_greg_t, libucontext_gregset_t[19]; + +typedef struct libucontext_fpstate { + unsigned long cw, sw, tag, ipoff, cssel, dataoff, datasel; + struct { + unsigned short significand[4], exponent; + } _st[8]; + unsigned long status; +} *libucontext_fpregset_t; + +typedef struct { + libucontext_gregset_t gregs; + libucontext_fpregset_t fpregs; + unsigned long oldmask, cr2; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif + diff --git a/src/3rdparty/libucontext/arch/x86/makecontext.c b/src/3rdparty/libucontext/arch/x86/makecontext.c new file mode 100644 index 000000000..2f8138918 --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86/makecontext.c @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * Copyright (c) 2019 A. Wilcox + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include +#include "defs.h" +#include + + +extern void libucontext_trampoline(void); + + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + libucontext_greg_t *sp, *argp; + va_list va; + int i; + unsigned int uc_link; + + uc_link = (argc > 6 ? argc - 6 : 0) + 1; + + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= uc_link; + sp = (libucontext_greg_t *) (((uintptr_t) sp & -16L) - 8); + + ucp->uc_mcontext.gregs[REG_EIP] = (uintptr_t) func; + ucp->uc_mcontext.gregs[REG_EBX] = (uintptr_t) argc; + ucp->uc_mcontext.gregs[REG_ESP] = (uintptr_t) sp; + + argp = sp; + *argp++ = (uintptr_t) &libucontext_trampoline; + + va_start(va, argc); + + for (i = 0; i < argc; i++) + *argp++ = va_arg (va, libucontext_greg_t); + + va_end(va); + + *argp++ = (uintptr_t) ucp->uc_link; +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/x86/setcontext.S b/src/3rdparty/libucontext/arch/x86/setcontext.S new file mode 100644 index 000000000..75d2108eb --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86/setcontext.S @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + /* load address of the ucontext structure */ + movl 4(%esp), %eax + + /* set up the FS segment register */ + movl REG_OFFSET(REG_FS)(%eax), %ecx + movw %cx, %fs + + /* fetch the new EIP */ + movl REG_OFFSET(REG_EIP)(%eax), %ecx + + /* set up the new stack pointer */ + movl REG_OFFSET(REG_ESP)(%eax), %esp + + /* push the return address onto the stack */ + pushl %ecx + + /* set all of the registers */ + movl REG_OFFSET(REG_EBX)(%eax), %ebx + movl REG_OFFSET(REG_ECX)(%eax), %ecx + movl REG_OFFSET(REG_EDX)(%eax), %edx + movl REG_OFFSET(REG_EBP)(%eax), %ebp + movl REG_OFFSET(REG_EDI)(%eax), %edi + movl REG_OFFSET(REG_ESI)(%eax), %esi + movl REG_OFFSET(REG_EAX)(%eax), %eax + + ret +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/x86/swapcontext.S b/src/3rdparty/libucontext/arch/x86/swapcontext.S new file mode 100644 index 000000000..3594fc0e5 --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86/swapcontext.S @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* load address of the ucontext structure */ + movl 4(%esp), %eax + + /* EAX is not a preserved register */ + movl $0, REG_OFFSET(REG_EAX)(%eax) + + /* copy all of the current registers into the ucontext structure */ + movl %ecx, REG_OFFSET(REG_ECX)(%eax) + movl %ebx, REG_OFFSET(REG_EBX)(%eax) + movl %edx, REG_OFFSET(REG_EDX)(%eax) + movl %edi, REG_OFFSET(REG_EDI)(%eax) + movl %esi, REG_OFFSET(REG_ESI)(%eax) + movl %ebp, REG_OFFSET(REG_EBP)(%eax) + + /* the first argument on the stack is the jump target (%eip), so we store it in the EIP + register in the ucontext structure. */ + movl (%esp), %ecx + movl %ecx, REG_OFFSET(REG_EIP)(%eax) + + /* take the stack pointer address (%esp) offsetting by 4 to skip over the jump target. */ + leal 4(%esp), %ecx + movl %ecx, REG_OFFSET(REG_ESP)(%eax) + + /* finally, save the FS segment register */ + xorl %ecx, %ecx + movw %fs, %cx + movl %ecx, REG_OFFSET(REG_FS)(%eax) + + /* load address of the ucontext structure */ + movl 8(%esp), %eax + + /* set up the FS segment register */ + movl REG_OFFSET(REG_FS)(%eax), %ecx + movw %cx, %fs + + /* fetch the new EIP */ + movl REG_OFFSET(REG_EIP)(%eax), %ecx + + /* set up the new stack pointer */ + movl REG_OFFSET(REG_ESP)(%eax), %esp + + /* push the return address onto the stack */ + pushl %ecx + + /* set all of the registers */ + movl REG_OFFSET(REG_EBX)(%eax), %ebx + movl REG_OFFSET(REG_ECX)(%eax), %ecx + movl REG_OFFSET(REG_EDX)(%eax), %edx + movl REG_OFFSET(REG_EBP)(%eax), %ebp + movl REG_OFFSET(REG_EDI)(%eax), %edi + movl REG_OFFSET(REG_ESI)(%eax), %esi + movl REG_OFFSET(REG_EAX)(%eax), %eax + + ret +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/x86/trampoline.c b/src/3rdparty/libucontext/arch/x86/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/arch/x86_64/defs.h b/src/3rdparty/libucontext/arch/x86_64/defs.h new file mode 100644 index 000000000..1ebf3a93f --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86_64/defs.h @@ -0,0 +1,105 @@ +#ifndef __ARCH_X86_64_DEFS_H +#define __ARCH_X86_64_DEFS_H + +#ifndef REG_R8 +# define REG_R8 (0) +#endif + +#ifndef REG_R9 +# define REG_R9 (1) +#endif + +#ifndef REG_R10 +# define REG_R10 (2) +#endif + +#ifndef REG_R11 +# define REG_R11 (3) +#endif + +#ifndef REG_R12 +# define REG_R12 (4) +#endif + +#ifndef REG_R13 +# define REG_R13 (5) +#endif + +#ifndef REG_R14 +# define REG_R14 (6) +#endif + +#ifndef REG_R15 +# define REG_R15 (7) +#endif + +#ifndef REG_RDI +# define REG_RDI (8) +#endif + +#ifndef REG_RSI +# define REG_RSI (9) +#endif + +#ifndef REG_RBP +# define REG_RBP (10) +#endif + +#ifndef REG_RBX +# define REG_RBX (11) +#endif + +#ifndef REG_RDX +# define REG_RDX (12) +#endif + +#ifndef REG_RAX +# define REG_RAX (13) +#endif + +#ifndef REG_RCX +# define REG_RCX (14) +#endif + +#ifndef REG_RSP +# define REG_RSP (15) +#endif + +#ifndef REG_RIP +# define REG_RIP (16) +#endif + +#ifndef REG_EFL +# define REG_EFL (17) +#endif + +#ifndef REG_CSGSFS +# define REG_CSGSFS (18) +#endif + +#ifndef REG_ERR +# define REG_ERR (19) +#endif + +#ifndef REG_TRAPNO +# define REG_TRAPNO (20) +#endif + +#ifndef REG_OLDMASK +# define REG_OLDMASK (21) +#endif + +#ifndef REG_CR2 +# define REG_CR2 (22) +#endif + +#define MCONTEXT_GREGS (40) + +#define REG_SZ (8) + +#define FETCH_LINKPTR(dest) \ + asm("movq (%%rbx), %0" : "=r" ((dest))); + +#include "common-defs.h" + +#endif diff --git a/src/3rdparty/libucontext/arch/x86_64/getcontext.S b/src/3rdparty/libucontext/arch/x86_64/getcontext.S new file mode 100644 index 000000000..168fa473d --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86_64/getcontext.S @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(getcontext, libucontext_getcontext) +ALIAS(__getcontext, libucontext_getcontext) + +FUNC(libucontext_getcontext) + /* copy all of the current registers into the ucontext structure */ + movq %r8, REG_OFFSET(REG_R8)(%rdi) + movq %r9, REG_OFFSET(REG_R9)(%rdi) + movq %r10, REG_OFFSET(REG_R10)(%rdi) + movq %r11, REG_OFFSET(REG_R11)(%rdi) + movq %r12, REG_OFFSET(REG_R12)(%rdi) + movq %r13, REG_OFFSET(REG_R13)(%rdi) + movq %r14, REG_OFFSET(REG_R14)(%rdi) + movq %r15, REG_OFFSET(REG_R15)(%rdi) + movq %rdi, REG_OFFSET(REG_RDI)(%rdi) + movq %rsi, REG_OFFSET(REG_RSI)(%rdi) + movq %rbp, REG_OFFSET(REG_RBP)(%rdi) + movq %rbx, REG_OFFSET(REG_RBX)(%rdi) + movq %rdx, REG_OFFSET(REG_RDX)(%rdi) + movq %rax, REG_OFFSET(REG_RAX)(%rdi) + movq %rcx, REG_OFFSET(REG_RCX)(%rdi) + + /* the first argument on the stack is the jump target (%rip), so we store it in the RIP + register in the ucontext structure. */ + movq (%rsp), %rcx + movq %rcx, REG_OFFSET(REG_RIP)(%rdi) + + /* finally take the stack pointer address (%rsp) offsetting by 8 to skip over the jump + target. */ + leaq 8(%rsp), %rcx + movq %rcx, REG_OFFSET(REG_RSP)(%rdi) + + /* we're all done here, return 0 */ + xorl %eax, %eax + ret +END(libucontext_getcontext) diff --git a/src/3rdparty/libucontext/arch/x86_64/include/libucontext/bits.h b/src/3rdparty/libucontext/arch/x86_64/include/libucontext/bits.h new file mode 100644 index 000000000..79567663b --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86_64/include/libucontext/bits.h @@ -0,0 +1,64 @@ +#include + +#ifndef LIBUCONTEXT_BITS_H +#define LIBUCONTEXT_BITS_H + +#define REG_R8 (0) +#define REG_R9 (1) +#define REG_R10 (2) +#define REG_R11 (3) +#define REG_R12 (4) +#define REG_R13 (5) +#define REG_R14 (6) +#define REG_R15 (7) +#define REG_RDI (8) +#define REG_RSI (9) +#define REG_RBP (10) +#define REG_RBX (11) +#define REG_RDX (12) +#define REG_RAX (13) +#define REG_RCX (14) +#define REG_RSP (15) +#define REG_RIP (16) +#define REG_EFL (17) +#define REG_CSGSFS (18) +#define REG_ERR (19) +#define REG_TRAPNO (20) +#define REG_OLDMASK (21) +#define REG_CR2 (22) + +typedef long long libucontext_greg_t, libucontext_gregset_t[23]; + +typedef struct libucontext_fpstate { + unsigned short cwd, swd, ftw, fop; + unsigned long long rip, rdp; + unsigned mxcsr, mxcr_mask; + struct { + unsigned short significand[4], exponent, padding[3]; + } _st[8]; + struct { + unsigned element[4]; + } _xmm[16]; + unsigned padding[24]; +} *libucontext_fpregset_t; + +typedef struct { + libucontext_gregset_t gregs; + libucontext_fpregset_t fpregs; + unsigned long long __reserved1[8]; +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned long uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; + +#endif diff --git a/src/3rdparty/libucontext/arch/x86_64/makecontext.c b/src/3rdparty/libucontext/arch/x86_64/makecontext.c new file mode 100644 index 000000000..cba9cb1b4 --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86_64/makecontext.c @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2018 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include + +#include "defs.h" +#include + +extern void libucontext_trampoline(void); + +_Static_assert(offsetof(libucontext_ucontext_t, uc_mcontext.gregs) == MCONTEXT_GREGS, "MCONTEXT_GREGS is invalid"); + +void +libucontext_makecontext(libucontext_ucontext_t *ucp, void (*func)(void), int argc, ...) +{ + libucontext_greg_t *sp; + va_list va; + int i; + unsigned int uc_link; + + uc_link = (argc > 6 ? argc - 6 : 0) + 1; + + sp = (libucontext_greg_t *) ((uintptr_t) ucp->uc_stack.ss_sp + ucp->uc_stack.ss_size); + sp -= uc_link; + sp = (libucontext_greg_t *) (((uintptr_t) sp & -16L) - 8); + + ucp->uc_mcontext.gregs[REG_RIP] = (uintptr_t) func; + ucp->uc_mcontext.gregs[REG_RBX] = (uintptr_t) &sp[uc_link]; + ucp->uc_mcontext.gregs[REG_RSP] = (uintptr_t) sp; + + sp[0] = (uintptr_t) &libucontext_trampoline; + sp[uc_link] = (uintptr_t) ucp->uc_link; + + va_start(va, argc); + + for (i = 0; i < argc; i++) + switch (i) + { + case 0: + ucp->uc_mcontext.gregs[REG_RDI] = va_arg (va, libucontext_greg_t); + break; + case 1: + ucp->uc_mcontext.gregs[REG_RSI] = va_arg (va, libucontext_greg_t); + break; + case 2: + ucp->uc_mcontext.gregs[REG_RDX] = va_arg (va, libucontext_greg_t); + break; + case 3: + ucp->uc_mcontext.gregs[REG_RCX] = va_arg (va, libucontext_greg_t); + break; + case 4: + ucp->uc_mcontext.gregs[REG_R8] = va_arg (va, libucontext_greg_t); + break; + case 5: + ucp->uc_mcontext.gregs[REG_R9] = va_arg (va, libucontext_greg_t); + break; + default: + sp[i - 5] = va_arg (va, libucontext_greg_t); + break; + } + + va_end(va); +} + +#ifdef EXPORT_UNPREFIXED +extern __typeof(libucontext_makecontext) makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +extern __typeof(libucontext_makecontext) __makecontext __attribute__((weak, __alias__("libucontext_makecontext"))); +#endif diff --git a/src/3rdparty/libucontext/arch/x86_64/setcontext.S b/src/3rdparty/libucontext/arch/x86_64/setcontext.S new file mode 100644 index 000000000..4dc270242 --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86_64/setcontext.S @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(setcontext, libucontext_setcontext) +ALIAS(__setcontext, libucontext_setcontext) + +FUNC(libucontext_setcontext) + /* set all of the registers */ + movq REG_OFFSET(REG_R8)(%rdi), %r8 + movq REG_OFFSET(REG_R9)(%rdi), %r9 + movq REG_OFFSET(REG_R10)(%rdi), %r10 + movq REG_OFFSET(REG_R11)(%rdi), %r11 + movq REG_OFFSET(REG_R12)(%rdi), %r12 + movq REG_OFFSET(REG_R13)(%rdi), %r13 + movq REG_OFFSET(REG_R14)(%rdi), %r14 + movq REG_OFFSET(REG_R15)(%rdi), %r15 + movq REG_OFFSET(REG_RSI)(%rdi), %rsi + movq REG_OFFSET(REG_RBP)(%rdi), %rbp + movq REG_OFFSET(REG_RBX)(%rdi), %rbx + movq REG_OFFSET(REG_RDX)(%rdi), %rdx + movq REG_OFFSET(REG_RAX)(%rdi), %rax + movq REG_OFFSET(REG_RCX)(%rdi), %rcx + movq REG_OFFSET(REG_RSP)(%rdi), %rsp + + /* set the jump target by pushing it to the stack. + ret will pop the new %rip from the stack, causing us to jump there. */ + pushq REG_OFFSET(REG_RIP)(%rdi) + + /* finally, set %rdi correctly. */ + movq REG_OFFSET(REG_RDI)(%rdi), %rdi + + /* we're all done here, return 0 */ + xorl %eax, %eax + ret +END(libucontext_setcontext) diff --git a/src/3rdparty/libucontext/arch/x86_64/swapcontext.S b/src/3rdparty/libucontext/arch/x86_64/swapcontext.S new file mode 100644 index 000000000..43dafb594 --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86_64/swapcontext.S @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2018, 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include "defs.h" + +ALIAS(swapcontext, libucontext_swapcontext) +ALIAS(__swapcontext, libucontext_swapcontext) + +FUNC(libucontext_swapcontext) + /* copy all of the current registers into the ucontext structure pointed by + the first argument */ + movq %r8, REG_OFFSET(REG_R8)(%rdi) + movq %r9, REG_OFFSET(REG_R9)(%rdi) + movq %r10, REG_OFFSET(REG_R10)(%rdi) + movq %r11, REG_OFFSET(REG_R11)(%rdi) + movq %r12, REG_OFFSET(REG_R12)(%rdi) + movq %r13, REG_OFFSET(REG_R13)(%rdi) + movq %r14, REG_OFFSET(REG_R14)(%rdi) + movq %r15, REG_OFFSET(REG_R15)(%rdi) + movq %rdi, REG_OFFSET(REG_RDI)(%rdi) + movq %rsi, REG_OFFSET(REG_RSI)(%rdi) + movq %rbp, REG_OFFSET(REG_RBP)(%rdi) + movq %rbx, REG_OFFSET(REG_RBX)(%rdi) + movq %rdx, REG_OFFSET(REG_RDX)(%rdi) + movq %rax, REG_OFFSET(REG_RAX)(%rdi) + movq %rcx, REG_OFFSET(REG_RCX)(%rdi) + + /* the first argument on the stack is the jump target (%rip), so we store it in the RIP + register in the ucontext structure. */ + movq (%rsp), %rcx + movq %rcx, REG_OFFSET(REG_RIP)(%rdi) + + /* finally take the stack pointer address (%rsp) offsetting by 8 to skip over the jump + target. */ + leaq 8(%rsp), %rcx + movq %rcx, REG_OFFSET(REG_RSP)(%rdi) + + /* set all of the registers to their new states, stored in the second + ucontext structure */ + movq REG_OFFSET(REG_R8)(%rsi), %r8 + movq REG_OFFSET(REG_R9)(%rsi), %r9 + movq REG_OFFSET(REG_R10)(%rsi), %r10 + movq REG_OFFSET(REG_R11)(%rsi), %r11 + movq REG_OFFSET(REG_R12)(%rsi), %r12 + movq REG_OFFSET(REG_R13)(%rsi), %r13 + movq REG_OFFSET(REG_R14)(%rsi), %r14 + movq REG_OFFSET(REG_R15)(%rsi), %r15 + movq REG_OFFSET(REG_RDI)(%rsi), %rdi + movq REG_OFFSET(REG_RBP)(%rsi), %rbp + movq REG_OFFSET(REG_RBX)(%rsi), %rbx + movq REG_OFFSET(REG_RDX)(%rsi), %rdx + movq REG_OFFSET(REG_RAX)(%rsi), %rax + movq REG_OFFSET(REG_RCX)(%rsi), %rcx + movq REG_OFFSET(REG_RSP)(%rsi), %rsp + + /* set the jump target by pushing it to the stack. + ret will pop the new %rip from the stack, causing us to jump there. */ + pushq REG_OFFSET(REG_RIP)(%rsi) + + /* finally, set %rsi correctly since we do not need it anymore. */ + movq REG_OFFSET(REG_RSI)(%rsi), %rsi + + /* we're all done here, return 0 */ + xorl %eax, %eax + ret +END(libucontext_swapcontext) diff --git a/src/3rdparty/libucontext/arch/x86_64/trampoline.c b/src/3rdparty/libucontext/arch/x86_64/trampoline.c new file mode 100644 index 000000000..699a0505d --- /dev/null +++ b/src/3rdparty/libucontext/arch/x86_64/trampoline.c @@ -0,0 +1,3 @@ +#include "defs.h" +#include +#include "common-trampoline.c" diff --git a/src/3rdparty/libucontext/doc/libucontext.scd b/src/3rdparty/libucontext/doc/libucontext.scd new file mode 100644 index 000000000..1f6c07acb --- /dev/null +++ b/src/3rdparty/libucontext/doc/libucontext.scd @@ -0,0 +1,174 @@ +libucontext(3) + +# NAME + +libucontext - a library for userspace context swapping + +# SYNOPSIS + +*#include * + +``` +typedef struct { + /* depends on target architecture */ +} libucontext_mcontext_t; + +typedef struct { + void *ss_sp; + int ss_flags; + size_t ss_size; +} libucontext_stack_t; + +typedef struct libucontext_ucontext { + unsigned int uc_flags; + struct libucontext_ucontext *uc_link; + libucontext_stack_t uc_stack; + libucontext_mcontext_t uc_mcontext; +} libucontext_ucontext_t; +``` + +*int libucontext_getcontext(libucontext_ucontext_t* \*_ucp_*);* + +*int libucontext_setcontext(const libucontext_ucontext_t* \*_ucp_*);* + +*void libucontext_makecontext(libucontext_ucontext_t* \*_ucp_*, void* _(\*func)()_*, int* _argc_*,* _..._*);* + +*int libucontext_swapcontext(libucontext_ucontext_t* \*_oucp_*, const libucontext_ucontext_t* \*_ucp_*);* + +# DESCRIPTION + +The *libucontext* library provides an implementation of the SysV ucontext functions. These +are traditionally used to implement user-space context swapping. This is achieved by using +the *libucontext_getcontext*, *libucontext_setcontext*, *libucontext_makecontext* and +*libucontext_swapcontext* functions as appropriate. + +The *libucontext_getcontext* function initializes a structure pointed to by _ucp_ with the +current user context. + +The *libucontext_setcontext* function sets the current user context to the structure pointed +to by _ucp_. It discards the current user context. + +The *libucontext_swapcontext* function saves the current user context in a structure pointed +to by _oucp_ and then sets the current user context to the new context in a structure pointed +to by _ucp_. + +The *libucontext_makecontext* function modifies a user context in a structure pointed to by +_ucp_ to run a function pointed to by _func_ and sets up an argument list of _argc_ values. + +# CAVEATS + +In SysV, the ucontext functions save and restore signal masks. The *libucontext* library, +however, does not. In practice, this does not usually matter, as users of these functions +rarely change the signal mask between contexts. + +Other implementations may or may not save and restore additional processor registers that +this implementation does not. The *libucontext* library only saves and restores the general +purpose registers. In practice, this has proven sufficient. + +# EXAMPLE + +A practical example showing cooperative multithreading. This program is intended for +illustrative purpose only and has been written in a way favoring simplicity over performance +and robustness: + +``` +#include +#include +#include +#include +#include +#include +#include + +libucontext_ucontext_t mainctx = {}; +libucontext_ucontext_t *curthr = &mainctx; +libucontext_ucontext_t *threads = NULL; +size_t thrcount = 0; + +void +yieldto(libucontext_ucontext_t *target) +{ + libucontext_ucontext_t *oldthr = curthr; + curthr = target; + + libucontext_swapcontext(oldthr, curthr); +} + +void +yield(void) +{ + libucontext_ucontext_t *newthr; + + /* we set uc_flags to non-zero to signal thread completion. */ + do + newthr = &threads[random() % thrcount]; + while (newthr == curthr || newthr->uc_flags); + + srandom(time(NULL)); + + yieldto(newthr); +} + +void +worker(size_t multiple) +{ + size_t accum = 1; + + for (size_t i = 0; i < 10; i++) + { + accum += (multiple * i); + + printf("[%p] accumulated %zu\n", curthr, accum); + yield(); + } + + /* mark thread as completed, so we don't return here */ + curthr->uc_flags = 1; +} + +void +create(size_t multiple) +{ + libucontext_ucontext_t *cursor; + + thrcount += 1; + threads = realloc(threads, sizeof(*threads) * thrcount); + + cursor = &threads[thrcount - 1]; + memset(cursor, '\0', sizeof *cursor); + + /* initialize the new thread's values to our current context */ + libucontext_getcontext(cursor); + + /* set up uc_link */ + cursor->uc_link = thrcount > 1 ? &threads[thrcount - 2] : &mainctx; + + /* set up a stack */ + cursor->uc_stack.ss_size = 8192; + cursor->uc_stack.ss_sp = calloc(1, cursor->uc_stack.ss_size); + + /* set up the function call */ + libucontext_makecontext(cursor, worker, 1, multiple); +} + +int +main(int argc, const char *argv[]) +{ + srandom(time(NULL)); + + libucontext_getcontext(&mainctx); + + for (size_t i = 1; i < 4; i++) + create(i); + + /* start the threads off by yielding to the last one */ + yieldto(&threads[thrcount - 1]); + + return EXIT_SUCCESS; +} +``` + +# AUTHORS + +Ariadne Conill + diff --git a/src/3rdparty/libucontext/doc/meson.build b/src/3rdparty/libucontext/doc/meson.build new file mode 100644 index 000000000..8f5ac7964 --- /dev/null +++ b/src/3rdparty/libucontext/doc/meson.build @@ -0,0 +1,21 @@ +scdoc = find_program('scdoc', required: true) + +custom_target( + 'libucontext.3', + output: 'libucontext.3', + input: 'libucontext.scd', + command: [ scdoc ], + feed: true, + capture: true, + install: true, + install_dir: get_option('mandir') / 'man3' +) + +if meson.version().version_compare('>=0.61.0') + foreach link : [ 'get', 'make', 'set', 'swap' ] + install_symlink('libucontext_' + link + 'context.3', + pointing_to: 'libucontext.3', + install_dir: get_option('mandir') / 'man3' + ) + endforeach +endif diff --git a/src/3rdparty/libucontext/examples/cooperative_threading.c b/src/3rdparty/libucontext/examples/cooperative_threading.c new file mode 100644 index 000000000..123e9eaa6 --- /dev/null +++ b/src/3rdparty/libucontext/examples/cooperative_threading.c @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include +#include +#include + +libucontext_ucontext_t mainctx = {}; +libucontext_ucontext_t *curthr = &mainctx; +libucontext_ucontext_t *threads = NULL; +size_t thrcount = 0; + +void +yieldto(libucontext_ucontext_t *target) +{ + libucontext_ucontext_t *oldthr = curthr; + curthr = target; + + libucontext_swapcontext(oldthr, curthr); +} + +void +yield(void) +{ + libucontext_ucontext_t *newthr; + + /* we set uc_flags to non-zero to signal thread completion. */ + do + newthr = &threads[random() % thrcount]; + while (newthr == curthr || newthr->uc_flags); + + srandom(time(NULL)); + + yieldto(newthr); +} + +void +worker(size_t multiple) +{ + size_t accum = 1; + + for (size_t i = 0; i < 10; i++) + { + accum += (multiple * i); + + printf("[%p] accumulated %zu\n", curthr, accum); + yield(); + } + + /* mark thread as completed, so we don't return here */ + curthr->uc_flags = 1; +} + +void +create(size_t multiple) +{ + libucontext_ucontext_t *cursor; + + thrcount += 1; + threads = realloc(threads, sizeof(*threads) * thrcount); + + cursor = &threads[thrcount - 1]; + memset(cursor, '\0', sizeof *cursor); + + /* initialize the new thread's values to our current context */ + libucontext_getcontext(cursor); + + /* set up uc_link */ + cursor->uc_link = thrcount > 1 ? &threads[thrcount - 2] : &mainctx; + + /* set up a stack */ + cursor->uc_stack.ss_size = 8192; + cursor->uc_stack.ss_sp = calloc(1, cursor->uc_stack.ss_size); + + /* set up the function call */ + libucontext_makecontext(cursor, worker, 1, multiple); +} + +int +main(int argc, const char *argv[]) +{ + srandom(time(NULL)); + + libucontext_getcontext(&mainctx); + + for (size_t i = 1; i < 4; i++) + create(i); + + /* start the threads off by yielding to the last one */ + yieldto(&threads[thrcount - 1]); + + return EXIT_SUCCESS; +} diff --git a/src/3rdparty/libucontext/include/libucontext/libucontext.h b/src/3rdparty/libucontext/include/libucontext/libucontext.h new file mode 100644 index 000000000..aac33f6ac --- /dev/null +++ b/src/3rdparty/libucontext/include/libucontext/libucontext.h @@ -0,0 +1,20 @@ +#ifndef LIBUCONTEXT_LIBUCONTEXT_H +#define LIBUCONTEXT_LIBUCONTEXT_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int libucontext_getcontext(libucontext_ucontext_t *); +void libucontext_makecontext(libucontext_ucontext_t *, void (*)(), int, ...); +int libucontext_setcontext(const libucontext_ucontext_t *); +int libucontext_swapcontext(libucontext_ucontext_t *, const libucontext_ucontext_t *); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/3rdparty/libucontext/libucontext.pc.in b/src/3rdparty/libucontext/libucontext.pc.in new file mode 100644 index 000000000..10962d859 --- /dev/null +++ b/src/3rdparty/libucontext/libucontext.pc.in @@ -0,0 +1,9 @@ +libdir=@LIBUCONTEXT_SHARED_LIBDIR@ +static_libdir=@LIBUCONTEXT_STATIC_LIBDIR@ +includedir=@LIBUCONTEXT_INCLUDEDIR@ + +Name: libucontext +Version: @LIBUCONTEXT_VERSION@ +Description: ucontext library implementation (standalone) +Libs: -L${libdir} -L${static_libdir} -lucontext +Cflags: -I${includedir} diff --git a/src/3rdparty/libucontext/libucontext_posix.c b/src/3rdparty/libucontext/libucontext_posix.c new file mode 100644 index 000000000..859b40767 --- /dev/null +++ b/src/3rdparty/libucontext/libucontext_posix.c @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 Ariadne Conill + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * This software is provided 'as is' and without any warranty, express or + * implied. In no event shall the authors be liable for any damages arising + * from the use of this software. + */ + +#include +#include +#include +#include + +#ifdef FREESTANDING +# error libucontext_posix cannot be built in FREESTANDING mode. +#endif + +#ifdef DEBUG +# define TRACE(...) fprintf(stderr, "TRACE: " __VA_ARGS__) +#else +# define TRACE(...) +#endif + +int +getcontext(libucontext_ucontext_t *ucp) +{ + TRACE("getcontext(%p)\n", ucp); + + if (sigprocmask(SIG_SETMASK, NULL, &ucp->uc_sigmask)) + return -1; + + return libucontext_getcontext(ucp); +} + +int +setcontext(const libucontext_ucontext_t *ucp) +{ + TRACE("setcontext(%p)\n", ucp); + + if (sigprocmask(SIG_SETMASK, &ucp->uc_sigmask, NULL)) + return -1; + + return libucontext_setcontext(ucp); +} + +int +swapcontext(libucontext_ucontext_t *oucp, const libucontext_ucontext_t *ucp) +{ + TRACE("swapcontext(%p, %p)\n", oucp, ucp); + + if (sigprocmask(SIG_SETMASK, &ucp->uc_sigmask, &oucp->uc_sigmask)) + return -1; + + return libucontext_swapcontext(oucp, ucp); +} diff --git a/src/3rdparty/libucontext/meson.build b/src/3rdparty/libucontext/meson.build new file mode 100644 index 000000000..7dc9f8be2 --- /dev/null +++ b/src/3rdparty/libucontext/meson.build @@ -0,0 +1,172 @@ +project( + 'libucontext', + 'c', + meson_version : '>=0.59.0', + default_options: ['c_std=gnu11', 'default_library=both'], + version : run_command('head', files('VERSION')).stdout() +) + +cpu = get_option('cpu') +if cpu == '' + cpu = host_machine.cpu_family() +endif + +if cpu == 'sh4' + cpu = 'sh' +endif + +project_description = 'Portable implementation of ucontext' + +project_headers = [ + 'include/libucontext/libucontext.h' +] + +project_source_files = [ + 'arch' / cpu / 'getcontext.S', + 'arch' / cpu / 'setcontext.S', + 'arch' / cpu / 'swapcontext.S', +] +if cpu in ['mips', 'mips64'] + project_source_files += [ + 'arch' / cpu / 'makecontext.S' + ] +else + project_source_files += [ + 'arch' / cpu / 'makecontext.c' + ] +endif +if cpu in ['ppc', 'ppc64'] + project_source_files += [ + 'arch' / cpu / 'retfromsyscall.c' + ] +endif +if cpu not in ['mips', 'mips64', 'ppc', 'ppc64', 's390x'] + project_source_files += [ + 'arch' / cpu / 'trampoline.c' + ] +else + project_source_files += [ + 'arch' / cpu / 'startcontext.S' + ] +endif + +project_includes = [ + 'include', + 'arch/common' +] + +build_args = [ + '-D_BSD_SOURCE' +] + + +# =================================================================== + +# ====== +# Options +# ====== + +freestanding = get_option('freestanding') +export_unprefixed = get_option('export_unprefixed') +build_posix = true + +if freestanding + build_args += '-DFREESTANDING' + build_posix = false + export_unprefixed = false + project_headers += ['arch' / cpu / 'include/libucontext/bits.h'] + project_includes += ['arch' / cpu / 'include'] +else + project_headers += ['arch/common/include/libucontext/bits.h'] + project_includes += ['arch/common/include'] +endif + +if export_unprefixed + build_args += '-DEXPORT_UNPREFIXED' +endif + +# ====== +# Target +# ====== + +headers = include_directories(project_includes) + +libucontext_target = library( + 'ucontext', + project_source_files, + version: '1', + install : not meson.is_subproject(), + c_args : build_args, + pic: true, + include_directories : headers, +) +libucontext_dep = declare_dependency( + include_directories: headers, + link_with : libucontext_target +) + +if build_posix + libucontext_posix_target = library( + 'ucontext_posix', + project_source_files + ['libucontext_posix.c'], + version: '1', + install : not meson.is_subproject(), + c_args : build_args, + pic: true, + include_directories : headers, + ) + libucontext_posix_dep = declare_dependency( + include_directories: headers, + link_with : libucontext_posix_target + ) +endif + +# ======= +# Project +# ======= + +if not meson.is_subproject() + # Make this library usable from the system's + # package manager. + install_headers(project_headers, subdir : meson.project_name()) + + pkg_mod = import('pkgconfig') + pkg_mod.generate( + name : meson.project_name(), + filebase : meson.project_name(), + description : project_description, + subdirs : meson.project_name(), + libraries : libucontext_target, + ) +endif + +# ==== +# Docs +# ==== + +if not meson.is_subproject() and get_option('docs') + subdir('doc') +endif + +# ========== +# Unit Tests +# ========== + +test('test_libucontext', + executable( + 'test_libucontext', + files('test_libucontext.c'), + dependencies : libucontext_dep, + install : false + ) +) +if build_posix + test('test_libucontext_posix', + executable( + 'test_libucontext_posix', + files('test_libucontext_posix.c'), + dependencies : [libucontext_dep, libucontext_posix_dep], + install : false + ) + ) +endif diff --git a/src/3rdparty/libucontext/meson_options.txt b/src/3rdparty/libucontext/meson_options.txt new file mode 100644 index 000000000..e37217df4 --- /dev/null +++ b/src/3rdparty/libucontext/meson_options.txt @@ -0,0 +1,8 @@ +option('freestanding', type : 'boolean', value : false, + description: 'Do not use system headers') +option('export_unprefixed', type : 'boolean', value : true, + description: 'Export POSIX 2004 ucontext names as alises') +option('cpu', type : 'string', value : '', + description: 'Target CPU architecture for cross compile') +option('docs', type : 'boolean', value : false, + description: 'Build and install man pages') diff --git a/src/3rdparty/libucontext/test_libucontext.c b/src/3rdparty/libucontext/test_libucontext.c new file mode 100644 index 000000000..72b000e6f --- /dev/null +++ b/src/3rdparty/libucontext/test_libucontext.c @@ -0,0 +1,102 @@ +/* + * libucontext test program based on POSIX example program. + * Public domain. + */ + +#include +#include +#include +#include +#include + +static libucontext_ucontext_t ctx[3]; + + +static void check_arg(int actual, int expected) { + if (actual == expected) return; + fprintf(stderr, "argument has wrong value. got %d, expected %d.\n", actual, expected); + abort(); +} + + +static void f1 (int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) { + printf("start f1\n"); + + printf("checking provided arguments to function f1\n"); + check_arg(a, 1); + check_arg(b, 2); + check_arg(c, 3); + check_arg(d, 4); + check_arg(e, 5); + check_arg(f, 6); + check_arg(g, 7); + check_arg(h, 8); + check_arg(i, 9); + check_arg(j, 10); + printf("looks like all arguments are passed correctly\n"); + + printf("swap back to f2\n"); + libucontext_swapcontext(&ctx[1], &ctx[2]); + printf("finish f1\n"); +} + + +static void f2 (void) { + printf("start f2\n"); + printf("swap to f1\n"); + libucontext_swapcontext(&ctx[2], &ctx[1]); + printf("finish f2, should swap to f1\n"); +} + + +int main (int argc, const char *argv[]) { + char st1[8192]; + char st2[8192]; + volatile int done = 0; + + + /* poison each coroutine's stack memory for debugging purposes */ + memset(st1, 'A', sizeof st1); + memset(st2, 'B', sizeof st2); + + + printf("setting up context 1\n"); + + + libucontext_getcontext(&ctx[1]); + ctx[1].uc_stack.ss_sp = st1; + ctx[1].uc_stack.ss_size = sizeof st1; + ctx[1].uc_link = &ctx[0]; + libucontext_makecontext(&ctx[1], f1, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + + printf("setting up context 2\n"); + + + libucontext_getcontext(&ctx[2]); + ctx[2].uc_stack.ss_sp = st2; + ctx[2].uc_stack.ss_size = sizeof st2; + ctx[2].uc_link = &ctx[1]; + libucontext_makecontext(&ctx[2], f2, 0); + + + printf("doing initial swapcontext\n"); + + + libucontext_swapcontext(&ctx[0], &ctx[2]); + + + printf("returned from initial swapcontext\n"); + + + /* test ability to use getcontext/setcontext without makecontext */ + libucontext_getcontext(&ctx[1]); + printf("done = %d\n", done); + if (done++ == 0) libucontext_setcontext(&ctx[1]); + if (done != 2) { + fprintf(stderr, "wrong value for done. got %d, expected 2\n", done); + abort(); + } + + return 0; +} diff --git a/src/3rdparty/libucontext/test_libucontext_posix.c b/src/3rdparty/libucontext/test_libucontext_posix.c new file mode 100644 index 000000000..9b8c01d52 --- /dev/null +++ b/src/3rdparty/libucontext/test_libucontext_posix.c @@ -0,0 +1,102 @@ +/* + * libucontext test program based on POSIX example program. + * Public domain. + */ + +#include +#include +#include +#include +#include + +static ucontext_t ctx[3]; + + +static void check_arg(int actual, int expected) { + if (actual == expected) return; + fprintf(stderr, "argument has wrong value. got %d, expected %d.\n", actual, expected); + abort(); +} + + +static void f1 (int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) { + printf("start f1\n"); + + printf("checking provided arguments to function f1\n"); + check_arg(a, 1); + check_arg(b, 2); + check_arg(c, 3); + check_arg(d, 4); + check_arg(e, 5); + check_arg(f, 6); + check_arg(g, 7); + check_arg(h, 8); + check_arg(i, 9); + check_arg(j, 10); + printf("looks like all arguments are passed correctly\n"); + + printf("swap back to f2\n"); + swapcontext(&ctx[1], &ctx[2]); + printf("finish f1\n"); +} + + +static void f2 (void) { + printf("start f2\n"); + printf("swap to f1\n"); + swapcontext(&ctx[2], &ctx[1]); + printf("finish f2, should swap to f1\n"); +} + + +int main (int argc, const char *argv[]) { + char st1[8192]; + char st2[8192]; + volatile int done = 0; + + + /* poison each coroutine's stack memory for debugging purposes */ + memset(st1, 'A', sizeof st1); + memset(st2, 'B', sizeof st2); + + + printf("setting up context 1\n"); + + + getcontext(&ctx[1]); + ctx[1].uc_stack.ss_sp = st1; + ctx[1].uc_stack.ss_size = sizeof st1; + ctx[1].uc_link = &ctx[0]; + makecontext(&ctx[1], f1, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + + printf("setting up context 2\n"); + + + getcontext(&ctx[2]); + ctx[2].uc_stack.ss_sp = st2; + ctx[2].uc_stack.ss_size = sizeof st2; + ctx[2].uc_link = &ctx[1]; + makecontext(&ctx[2], f2, 0); + + + printf("doing initial swapcontext\n"); + + + swapcontext(&ctx[0], &ctx[2]); + + + printf("returned from initial swapcontext\n"); + + + /* test ability to use getcontext/setcontext without makecontext */ + getcontext(&ctx[1]); + printf("done = %d\n", done); + if (done++ == 0) setcontext(&ctx[1]); + if (done != 2) { + fprintf(stderr, "wrong value for done. got %d, expected 2\n", done); + abort(); + } + + return 0; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 000000000..00589b92f --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(lib) +add_subdirectory(bin) +add_subdirectory(samples) diff --git a/src/bin/CMakeLists.txt b/src/bin/CMakeLists.txt new file mode 100644 index 000000000..5ff236870 --- /dev/null +++ b/src/bin/CMakeLists.txt @@ -0,0 +1,7 @@ +add_subdirectory(tools) + +if (NOT ${CMAKE_BUILD_TYPE} MATCHES "Coverage") + add_subdirectory(lwan) +endif() + +add_subdirectory(testrunner) diff --git a/src/bin/fuzz/config_fuzzer.cc b/src/bin/fuzz/config_fuzzer.cc new file mode 100644 index 000000000..55c89bd03 --- /dev/null +++ b/src/bin/fuzz/config_fuzzer.cc @@ -0,0 +1,68 @@ +#include +#include +#include + +extern "C" { +#include "lwan-config.h" +} + +#define LWAN_NO_DISCARD(...) \ + do { \ + __typeof__(__VA_ARGS__) no_discard_ = __VA_ARGS__; \ + __asm__ __volatile__("" ::"g"(no_discard_) : "memory"); \ + } while (0) + +static bool dump(struct config *config, int indent_level) +{ + const struct config_line *line; + + if (indent_level > 64) + return false; + + while ((line = config_read_line(config))) { + switch (line->type) { + case CONFIG_LINE_TYPE_LINE: + LWAN_NO_DISCARD(parse_bool(line->value, false)); + LWAN_NO_DISCARD(parse_long(line->value, 0)); + LWAN_NO_DISCARD(parse_int(line->value, 0)); + LWAN_NO_DISCARD(parse_time_period(line->value, 0)); + break; + + case CONFIG_LINE_TYPE_SECTION_END: + if (indent_level == 0) + return false; + + return true; + + case CONFIG_LINE_TYPE_SECTION: + if (!dump(config, indent_level + 1)) + return false; + + break; + } + } + + const char *error = config_last_error(config); + + if (error) { + printf("Error: %s\n", error); + return false; + } + + return true; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct config *config; + + config = config_open_for_fuzzing(data, size); + if (!config) + return 1; + + bool dumped = dump(config, 0); + + config_close(config); + + return dumped ? 1 : 0; +} diff --git a/src/bin/fuzz/h2_huffman_fuzzer.cc b/src/bin/fuzz/h2_huffman_fuzzer.cc new file mode 100644 index 000000000..ef0de8106 --- /dev/null +++ b/src/bin/fuzz/h2_huffman_fuzzer.cc @@ -0,0 +1,12 @@ +#include +#include + +extern "C" { +bool lwan_h2_huffman_decode_for_fuzzing(const uint8_t *input, + size_t input_len); + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + return lwan_h2_huffman_decode_for_fuzzing(data, size) == true; +} +} diff --git a/src/bin/fuzz/pattern_fuzzer.cc b/src/bin/fuzz/pattern_fuzzer.cc new file mode 100644 index 000000000..68d45ff53 --- /dev/null +++ b/src/bin/fuzz/pattern_fuzzer.cc @@ -0,0 +1,35 @@ +#include +#include +#include + +extern "C" { +#include "patterns.h" +#include "lwan-private.h" +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct str_find sf[16]; + static uint8_t static_data[32768]; + struct config *config; + struct config_line line; + int indent_level = 0; + const char *errmsg; + + if (size == 0) + return 1; + + if (size > sizeof(static_data)) + size = sizeof(static_data); + memcpy(static_data, data, size); + static_data[size - 1] = '\0'; + + LWAN_NO_DISCARD(str_find((char *)static_data, "foo/(%d+)(%a)(%d+)", sf, + N_ELEMENTS(sf), &errmsg)); + LWAN_NO_DISCARD(str_find((char *)static_data, "bar/(%d+)/test", sf, + N_ELEMENTS(sf), &errmsg)); + LWAN_NO_DISCARD(str_find((char *)static_data, "lua/rewrite/(%d+)x(%d+)", sf, + N_ELEMENTS(sf), &errmsg)); + + return 0; +} diff --git a/src/bin/fuzz/request_fuzzer.cc b/src/bin/fuzz/request_fuzzer.cc new file mode 100644 index 000000000..a6e66a5b1 --- /dev/null +++ b/src/bin/fuzz/request_fuzzer.cc @@ -0,0 +1,8 @@ +#include + +extern "C" int fuzz_parse_http_request(const uint8_t *, size_t); + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + return fuzz_parse_http_request(data, size); +} diff --git a/src/bin/fuzz/template_fuzzer.cc b/src/bin/fuzz/template_fuzzer.cc new file mode 100644 index 000000000..9f3b24ecb --- /dev/null +++ b/src/bin/fuzz/template_fuzzer.cc @@ -0,0 +1,70 @@ +#include +#include +#include + +extern "C" { +#include "lwan-private.h" +#include "lwan-template.h" +} + +struct file_list { + const char *full_path; + const char *rel_path; + const char *readme; + struct { + coro_function_t generator; + + const char *icon; + const char *icon_alt; + const char *name; + const char *type; + + int size; + const char *unit; + + const char *zebra_class; + } file_list; +}; + +int directory_list_generator(struct coro *, void *) +{ + return 0; +} + +#undef TPL_STRUCT +#define TPL_STRUCT struct file_list +static const struct lwan_var_descriptor file_list_desc[] = { + TPL_VAR_STR_ESCAPE(full_path), + TPL_VAR_STR_ESCAPE(rel_path), + TPL_VAR_STR_ESCAPE(readme), + TPL_VAR_SEQUENCE(file_list, + directory_list_generator, + ((const struct lwan_var_descriptor[]){ + TPL_VAR_STR(file_list.icon), + TPL_VAR_STR(file_list.icon_alt), + TPL_VAR_STR(file_list.name), + TPL_VAR_STR(file_list.type), + TPL_VAR_INT(file_list.size), + TPL_VAR_STR(file_list.unit), + TPL_VAR_STR(file_list.zebra_class), + TPL_VAR_SENTINEL, + })), + TPL_VAR_SENTINEL, +}; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + static char copy[32768]; + struct lwan_tpl *tpl; + + size = LWAN_MIN(sizeof(copy) - 1, size); + memcpy(copy, data, size); + copy[size] = '\0'; + + tpl = lwan_tpl_compile_string_full(copy, file_list_desc, + LWAN_TPL_FLAG_CONST_TEMPLATE); + if (tpl) + lwan_tpl_free(tpl); + + return tpl ? 1 : 0; +} diff --git a/src/bin/lwan/CMakeLists.txt b/src/bin/lwan/CMakeLists.txt new file mode 100644 index 000000000..6931ee045 --- /dev/null +++ b/src/bin/lwan/CMakeLists.txt @@ -0,0 +1,10 @@ +include_directories(BEFORE ${CMAKE_BINARY_DIR}) + +add_executable(lwan main.c) + +target_link_libraries(lwan + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) + +INSTALL(TARGETS lwan DESTINATION ${CMAKE_INSTALL_FULL_BINDIR}) diff --git a/src/bin/lwan/main.c b/src/bin/lwan/main.c new file mode 100644 index 000000000..f6ab95af1 --- /dev/null +++ b/src/bin/lwan/main.c @@ -0,0 +1,351 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include "lwan-private.h" +#include "lwan-mod-serve-files.h" + +enum args { + ARGS_FAILED, + ARGS_USE_CONFIG, + ARGS_SERVE_FILES +}; + +static void print_module_info(void) +{ + const struct lwan_module_info *module; + int count = 0; + + printf("Built-in modules:"); + LWAN_SECTION_FOREACH(lwan_module, module) { + printf(" %s", module->name); + count++; + } + printf(count ? ".\n" : " (none)\n"); +} + +static void +print_handler_info(void) +{ + const struct lwan_handler_info *handler; + int count = 0; + + printf("Built-in handlers:"); + LWAN_SECTION_FOREACH(lwan_handler, handler) { + if (!handler->route) + continue; + + printf(" %s", handler->name); + count++; + } + printf(count ? ".\n" : " (none)\n"); +} + +static void +print_build_time_configuration(void) +{ + printf("Build-time configuration:"); + +#if defined(LWAN_HAVE_LUA_JIT) + printf(" LuaJIT"); +#elif defined(LWAN_HAVE_LUA) + printf(" Lua"); +#endif + +#if defined(LWAN_HAVE_BROTLI) + printf(" Brotli"); +#endif +#if defined(LWAN_HAVE_ZSTD) + printf(" zstd"); +#endif + +#if defined(LWAN_HAVE_MBEDTLS) + printf(" mbedTLS"); +#endif + +#if defined(LWAN_HAVE_LIBUCONTEXT) + printf(" libucontext-coroutine"); +#else + printf(" builtin-coroutine"); +#endif + +#if defined(LWAN_HAVE_EPOLL) + printf(" epoll"); +#elif defined(LWAN_HAVE_KQUEUE) + printf(" kqueue"); +#endif + +#if defined(LWAN_HAVE_SO_ATTACH_REUSEPORT_CBPF) + printf(" sockopt-reuseport-CBPF"); +#elif defined(LWAN_HAVE_SO_INCOMING_CPU) + printf(" sockopt-reuseport-incoming-cpu"); +#endif + +#if !defined(NDEBUG) && defined(LWAN_HAVE_VALGRIND) + printf(" valgrind"); +#endif + +#if defined(LWAN_HAVE_SYSLOG) + printf(" syslog"); +#endif + +#if defined(LWAN_HAVE_UNDEFINED_SANITIZER) + printf(" ubsan"); +#endif + +#if defined(LWAN_HAVE_ADDRESS_SANITIZER) + printf(" asan"); +#endif + +#if defined(LWAN_HAVE_THREAD_SANITIZER) + printf(" tsan"); +#endif + +#if !defined(NDEBUG) + printf(" debug"); +#endif + + printf(".\n"); +} + +static void +print_help(const char *argv0, const struct lwan_config *config) +{ + char path_buf[PATH_MAX]; + char *current_dir = get_current_dir_name(); + const char *config_file = lwan_get_config_path(path_buf, sizeof(path_buf)); + + printf("Usage: %s [--root /path/to/root/dir] [--listen addr:port]\n", argv0); +#if defined(LWAN_HAVE_MBEDTLS) + printf(" [--tls-listen addr:port] [--cert-path /cert/path] [--cert-key /key/path]\n"); +#endif + printf(" [--config /path/to/config/file] [--user username]\n"); + printf(" [--chroot /path/to/chroot/directory]\n"); + printf("\n"); +#if defined(LWAN_HAVE_MBEDTLS) + printf("Serve files through HTTP or HTTPS.\n\n"); +#else + printf("Serve files through HTTP.\n\n"); +#endif + printf("Options:\n"); + printf(" -r, --root Path to serve files from (default: ./wwwroot).\n"); + printf("\n"); + printf(" -l, --listen Listener (default: %s).\n", config->listener); +#if defined(LWAN_HAVE_MBEDTLS) + printf(" -L, --tls-listen TLS Listener (default: %s).\n", + config->tls_listener ? + config->tls_listener : "not listening"); +#endif + printf("\n"); + printf(" -c, --config Path to config file path.\n"); + printf(" -u, --user Username to drop privileges to (root required).\n"); + printf(" -C, --chroot Chroot to path passed to --root (root required).\n"); +#if defined(LWAN_HAVE_MBEDTLS) + printf("\n"); + printf(" -P, --cert-path Path to TLS certificate.\n"); + printf(" -K, --cert-key Path to TLS key.\n"); +#endif + printf("\n"); + printf(" -h, --help This.\n"); + printf(" -v, --version Print the Lwan version & build info, and exits.\n"); + printf("\n"); + printf("Examples:\n"); + if (!access("/usr/share/doc", R_OK)) { + printf(" Serve system-wide documentation:\n"); + printf(" %s -r /usr/share/doc\n", argv0); + } + printf(" Serve on a different port:\n"); + printf(" %s -l '*:1337'\n", argv0); + printf(" Use %s from %s:\n", config_file, current_dir); + printf(" %s\n", argv0); + printf(" Use /etc/%s:\n", config_file); + printf(" %s -c /etc/%s\n", argv0, config_file); +#if defined(LWAN_HAVE_MBEDTLS) + printf(" Serve system docs with HTTP and HTTPS:\n"); + printf(" %s -P /path/to/cert.pem -K /path/to/cert.key \\\n" + " -l '*:8080' -L '*:8081' -r /usr/share/doc\n", argv0); +#endif + printf("\n"); + printf("Report bugs at .\n"); + printf("For security-related reports, mail them to .\n"); + + free(current_dir); +} + +static void print_version(void) +{ + printf("Lwan version %s\n", LWAN_VERSION); + printf("\n"); + print_build_time_configuration(); + print_module_info(); + print_handler_info(); +} + +static enum args +parse_args(int argc, char *argv[], struct lwan_config *config, char *root, + struct lwan_straitjacket *sj) +{ + static const struct option opts[] = { + { .name = "root", .has_arg = 1, .val = 'r' }, + { .name = "listen", .has_arg = 1, .val = 'l' }, + { .name = "help", .val = 'h' }, + { .name = "version", .val = 'v' }, + { .name = "config", .has_arg = 1, .val = 'c' }, + { .name = "chroot", .val = 'C' }, + { .name = "user", .val = 'u', .has_arg = 1 }, +#if defined(LWAN_HAVE_MBEDTLS) + { .name = "tls-listen", .val = 'L', .has_arg = 1 }, + { .name = "cert-path", .val = 'P', .has_arg = 1 }, + { .name = "cert-key", .val = 'K', .has_arg = 1 }, +#endif + { } + }; + int c, optidx = 0; + enum args result = ARGS_USE_CONFIG; + + while ((c = getopt_long(argc, argv, "L:P:K:hvr:l:c:u:C", opts, &optidx)) != -1) { + switch (c) { +#if defined(LWAN_HAVE_MBEDTLS) + case 'L': + free(config->tls_listener); + config->tls_listener = strdup(optarg); + result = ARGS_SERVE_FILES; + break; + + case 'P': + free(config->ssl.cert); + config->ssl.cert = strdup(optarg); + break; + + case 'K': + free(config->ssl.key); + config->ssl.key = strdup(optarg); + break; +#endif + case 'u': + free((char *)sj->user_name); + sj->user_name = (const char *)strdup(optarg); + break; + + case 'C': + sj->chroot_path = root; + break; + + case 'c': + free(config->config_file_path); + config->config_file_path = strdup(optarg); + result = ARGS_USE_CONFIG; + break; + + case 'l': + free(config->listener); + config->listener = strdup(optarg); + result = ARGS_SERVE_FILES; + break; + + case 'r': { + size_t len = strlen(optarg); + + if (len >= PATH_MAX) { + fprintf(stderr, "Root path length exeeds %d characters\n", PATH_MAX); + return ARGS_FAILED; + } + + memcpy(root, optarg, len + 1); + result = ARGS_SERVE_FILES; + break; + } + + case 'h': + print_help(argv[0], config); + return ARGS_FAILED; + + case 'v': + print_version(); + return ARGS_FAILED; + + default: + printf("Run %s --help for usage information.\n", argv[0]); + return ARGS_FAILED; + } + } + + return result; +} + +int +main(int argc, char *argv[]) +{ + struct lwan l; + struct lwan_config c; + struct lwan_straitjacket sj = {}; + char root_buf[PATH_MAX]; + char *root = root_buf; + int ret = EXIT_SUCCESS; + + if (!getcwd(root, PATH_MAX)) + return 1; + + c = *lwan_get_default_config(); + c.listener = strdup("*:8080"); + + switch (parse_args(argc, argv, &c, root, &sj)) { + case ARGS_SERVE_FILES: + lwan_status_info("Serving files from %s", root); + + if (sj.chroot_path) { + root = "/"; + } + lwan_straitjacket_enforce(&sj); + + lwan_init_with_config(&l, &c); + + const struct lwan_url_map map[] = { + { .prefix = "/", SERVE_FILES_SETTINGS(root, "index.html", true) }, + { } + }; + lwan_set_url_map(&l, map); + break; + case ARGS_USE_CONFIG: + lwan_straitjacket_enforce(&sj); + if (c.config_file_path) + lwan_init_with_config(&l, &c); + else + lwan_init(&l); + break; + case ARGS_FAILED: + ret = EXIT_FAILURE; + goto out; + } + + lwan_main_loop(&l); + lwan_shutdown(&l); + +out: + free(c.listener); + free(c.config_file_path); + free((char *)sj.user_name); + + return ret; +} diff --git a/src/bin/testrunner/CMakeLists.txt b/src/bin/testrunner/CMakeLists.txt new file mode 100644 index 000000000..f815078cf --- /dev/null +++ b/src/bin/testrunner/CMakeLists.txt @@ -0,0 +1,24 @@ +include_directories(BEFORE ${CMAKE_BINARY_DIR}) + +add_executable(testrunner main.c) + +target_link_libraries(testrunner + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) + +if (${CMAKE_BUILD_TYPE} MATCHES "Coverage") + include(CodeCoverage) + + if (Python3_Interpreter_FOUND) + setup_target_for_coverage_lcov( + NAME generate-coverage + EXECUTABLE ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/src/scripts/testsuite.py -v ${CMAKE_BINARY_DIR} + DEPENDENCIES testrunner + BASE_DIRECTORY "${CMAKE_SOURCE_DIR}" + ) + message(STATUS "Python found; generate-coverage target enabled") + else () + message(STATUS "Python not found; coverage report disabled") + endif() +endif () diff --git a/src/bin/testrunner/main.c b/src/bin/testrunner/main.c new file mode 100644 index 000000000..7c5e7a7ab --- /dev/null +++ b/src/bin/testrunner/main.c @@ -0,0 +1,289 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define _GNU_SOURCE +#include +#include + +#include "lwan-private.h" + +LWAN_HANDLER(quit_lwan) +{ + exit(0); + return HTTP_OK; +} + +LWAN_HANDLER(gif_beacon) +{ + /* + * 1x1 transparent GIF image generated with tinygif + * http://www.perlmonks.org/?node_id=7974 + */ + static const unsigned char gif_beacon_data[] = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x90, + 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, + 0x05, 0x10, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x04, 0x01 + }; + + response->mime_type = "image/gif"; + lwan_strbuf_set_static(response->buffer, (char*)gif_beacon_data, sizeof(gif_beacon_data)); + + return HTTP_OK; +} + +LWAN_HANDLER(test_chunked_encoding) +{ + int i; + + response->mime_type = "text/plain"; + + lwan_strbuf_printf(response->buffer, "Testing chunked encoding! First chunk\n"); + lwan_response_send_chunk(request); + + for (i = 0; i <= 10; i++) { + lwan_strbuf_printf(response->buffer, "*This is chunk %d*\n", i); + lwan_response_send_chunk(request); + } + + lwan_strbuf_printf(response->buffer, "Last chunk\n"); + lwan_response_send_chunk(request); + + return HTTP_OK; +} + +LWAN_HANDLER(test_server_sent_event) +{ + int i; + + for (i = 0; i <= 10; i++) { + lwan_strbuf_printf(response->buffer, "Current value is %d", i); + lwan_response_send_event(request, "currval"); + } + + return HTTP_OK; +} + +LWAN_HANDLER(get_query_string) +{ + const char *q = lwan_request_get_query_param(request, "q"); + if (!q) + return HTTP_BAD_REQUEST; + + const char *v = lwan_request_get_query_param(request, q); + if (!v) + return HTTP_NOT_FOUND; + + response->mime_type = "text/plain"; + lwan_strbuf_printf(response->buffer, "%s = %s", q, v); + return HTTP_OK; +} + +LWAN_HANDLER(test_proxy) +{ + struct lwan_key_value *headers = coro_malloc(request->conn->coro, sizeof(*headers) * 2); + if (UNLIKELY(!headers)) + return HTTP_INTERNAL_ERROR; + + char *buffer = coro_malloc(request->conn->coro, INET6_ADDRSTRLEN); + if (UNLIKELY(!buffer)) + return HTTP_INTERNAL_ERROR; + + headers[0].key = "X-Proxy"; + headers[0].value = (char*) lwan_request_get_remote_address(request, buffer); + headers[1].key = NULL; + headers[1].value = NULL; + + response->headers = headers; + + return HTTP_OK; +} + +LWAN_HANDLER(test_post_will_it_blend) +{ + static const char type[] = "application/json"; + static const char request_body[] = "{\"will-it-blend\": true}"; + static const char response_body[] = "{\"did-it-blend\": \"oh-hell-yeah\"}"; + const struct lwan_value *content_type = + lwan_request_get_content_type(request); + const struct lwan_value *body = lwan_request_get_request_body(request); + + if (!content_type->value) + return HTTP_BAD_REQUEST; + if (content_type->len != sizeof(type) - 1) + return HTTP_BAD_REQUEST; + if (memcmp(content_type->value, type, sizeof(type) - 1) != 0) + return HTTP_BAD_REQUEST; + + if (!body->value) + return HTTP_BAD_REQUEST; + if (body->len != sizeof(request_body) - 1) + return HTTP_BAD_REQUEST; + if (memcmp(body->value, request_body, sizeof(request_body) - 1) != 0) + return HTTP_BAD_REQUEST; + + response->mime_type = type; + lwan_strbuf_set_static(response->buffer, response_body, + sizeof(response_body) - 1); + + return HTTP_OK; +} + +LWAN_HANDLER(test_post_big) +{ + static const char type[] = "x-test/trololo"; + size_t i, sum = 0; + const struct lwan_value *content_type = + lwan_request_get_content_type(request); + const struct lwan_value *body = lwan_request_get_request_body(request); + + if (!content_type->value) + return HTTP_BAD_REQUEST; + if (content_type->len != sizeof(type) - 1) + return HTTP_BAD_REQUEST; + if (memcmp(content_type->value, type, sizeof(type) - 1) != 0) + return HTTP_BAD_REQUEST; + + for (i = 0; i < body->len; i++) + sum += (size_t)body->value[i]; + + response->mime_type = "application/json"; + lwan_strbuf_printf(response->buffer, "{\"received\": %zu, \"sum\": %zu}", + body->len, sum); + + return HTTP_OK; +} + +LWAN_HANDLER(hello_world) +{ + struct lwan_key_value *iter; + static struct lwan_key_value headers[] = { + { .key = "X-The-Answer-To-The-Universal-Question", .value = "42" }, + { NULL, NULL } + }; + response->headers = headers; + response->mime_type = "text/plain"; + + request->flags |= RESPONSE_INCLUDE_REQUEST_ID; + + const char *name = lwan_request_get_query_param(request, "name"); + if (name) + lwan_strbuf_printf(response->buffer, "Hello, %s!", name); + else + lwan_strbuf_set_static(response->buffer, "Hello, world!", sizeof("Hello, world!") -1); + + const char *dump_vars = lwan_request_get_query_param(request, "dump_vars"); + if (!dump_vars) + goto end; + + lwan_strbuf_append_strz(response->buffer, "\n\nCookies\n"); + lwan_strbuf_append_strz(response->buffer, "-------\n\n"); + + LWAN_ARRAY_FOREACH(lwan_request_get_cookies(request), iter) { + lwan_strbuf_append_printf(response->buffer, + "Key = \"%s\"; Value = \"%s\"\n", iter->key, iter->value); + } + + lwan_strbuf_append_strz(response->buffer, "\n\nQuery String Variables\n"); + lwan_strbuf_append_strz(response->buffer, "----------------------\n\n"); + + LWAN_ARRAY_FOREACH(lwan_request_get_query_params(request), iter) { + lwan_strbuf_append_printf(response->buffer, + "Key = \"%s\"; Value = \"%s\"\n", iter->key, iter->value); + } + + if (lwan_request_get_method(request) == REQUEST_METHOD_POST) { + lwan_strbuf_append_strz(response->buffer, "\n\nPOST data\n"); + lwan_strbuf_append_strz(response->buffer, "---------\n\n"); + + LWAN_ARRAY_FOREACH(lwan_request_get_post_params(request), iter) { + lwan_strbuf_append_printf(response->buffer, + "Key = \"%s\"; Value = \"%s\"\n", iter->key, iter->value); + } + } + + const char *dump_request_id = lwan_request_get_query_param(request, "dump_request_id"); + if (dump_request_id && streq(dump_request_id, "1")) { + lwan_strbuf_append_printf(response->buffer, "\nRequest ID: <<%016lx>>", + lwan_request_get_id(request)); + } + +end: + return HTTP_OK; +} + +LWAN_HANDLER(sleep) +{ + const char *ms_param = lwan_request_get_query_param(request, "ms"); + uint64_t ms; + + if (!ms_param) + return HTTP_INTERNAL_ERROR; + + ms = (uint64_t)parse_long(ms_param, 0); + response->mime_type = "text/plain"; + + if (ms) { + struct timespec t1, t2; + int64_t diff_ms; + + clock_gettime(CLOCK_MONOTONIC, &t1); + + lwan_request_sleep(request, ms); + + clock_gettime(CLOCK_MONOTONIC, &t2); + diff_ms = (t2.tv_sec - t1.tv_sec) * 1000; + diff_ms += (t2.tv_nsec - t1.tv_nsec) / 1000000; + + lwan_strbuf_printf(response->buffer, + "Returned from sleep. diff_ms = %"PRIi64, diff_ms); + } else { + lwan_strbuf_set_staticz(response->buffer, "Did not sleep"); + } + + return HTTP_OK; +} + +LWAN_HANDLER(custom_header) +{ + const char *hdr = lwan_request_get_query_param(request, "hdr"); + + if (!hdr) + return HTTP_NOT_FOUND; + + const char *val = lwan_request_get_header(request, hdr); + if (!val) + return HTTP_NOT_FOUND; + + response->mime_type = "text/plain"; + lwan_strbuf_printf(response->buffer, "Header value: '%s'", val); + return HTTP_OK; +} + +int +main() +{ + struct lwan l; + + lwan_init(&l); + lwan_main_loop(&l); + lwan_shutdown(&l); + + return EXIT_SUCCESS; +} diff --git a/test.lua b/src/bin/testrunner/test.lua similarity index 92% rename from test.lua rename to src/bin/testrunner/test.lua index 2dd8553aa..dfb2d06d9 100644 --- a/test.lua +++ b/src/bin/testrunner/test.lua @@ -41,6 +41,14 @@ function handle_get_random(req) req:set_response("Random number: " .. math.random()) end +function handle_get_brew_coffee(req) + return 418 +end + +function handle_get_invalid_code(req) + return 42 +end + function string.starts(String, Start) -- From http://lua-users.org/wiki/StringRecipes return string.sub(String, 1, string.len(Start)) == Start diff --git a/src/bin/testrunner/testrunner.conf b/src/bin/testrunner/testrunner.conf new file mode 100644 index 000000000..97e254479 --- /dev/null +++ b/src/bin/testrunner/testrunner.conf @@ -0,0 +1,182 @@ +constants { + buffer_size = 1000000 + cache_for = ${CACHE_FOR:5} +} + +# Timeout in seconds to keep a connection alive. +keep_alive_timeout = ${KEEP_ALIVE_TIMEOUT:15} + +# Set to true to not print any debugging messages. (Only effective in +# release builds.) +quiet = false + +# Value of "Expires" header. Default is 1 month and 1 week. +expires = 1M 1w + +# Number of I/O threads. Default (0) is number of online CPUs. +threads = 0 + +# This flag is enabled here so that the automated tests can be executed +# properly, but should be disabled unless absolutely needed (an example +# would be haproxy). +proxy_protocol = true + +# Maximum post data size of slightly less than 1MiB. The default is too +# small for testing purposes. +max_post_data_size = ${buffer_size} + +request_buffer_size = ${buffer_size} + +# Enable straitjacket by default. The `drop_capabilities` option is `true` +# by default. Other options may require more privileges. +straitjacket + +listener *:8080 + +site { + &custom_header /customhdr + + &sleep /sleep + + &hello_world /hello + + &quit_lwan /quit-lwan + + &test_proxy /proxy + + &test_chunked_encoding /chunked + + &test_server_sent_event /sse + + &gif_beacon /beacon + + &gif_beacon /favicon.ico + + &test_post_will_it_blend /post/blend + + &test_post_big /post/big + + &get_query_string /get-query-string + + redirect /elsewhere { to = http://lwan.ws } + + redirect /redirect307 { + to = http://lwan.ws + code = 307 + } + + rewrite /read-env { + pattern user { rewrite as = /hello?name=${USER} } + } + + rewrite /css/ { + pattern test.css { + condition cookie { style = dark } + rewrite as = /hello?name=dark + } + pattern test.css { + condition environment { COLUMNS = 80 } + condition stat { + path = /tmp/maoe.txt + is_file = true + } + condition lua = '''function matches(req) + return false + end + ''' + rewrite as = /hello?name=maoe + } + pattern test.css { + rewrite as = /hello?name=light + } + } + + response /brew-coffee { code = 418 } + + &hello_world /admin { + authorization basic { + realm = Administration Page + password file = htpasswd + } + } + lua /inline { + default type = text/html + cache period = 30s + script = '''function handle_get_root(req) + req:say('Hello') + end''' + } + lua /multiply { + # Implementation of https://github.com/berwynhoyt/nginx-lua-benchmark + + default type = text/html + + cache period = 30s + + script = '''function handle_get_root(req) + local param_a = req:query_param[[a]] + local param_b = req:query_param[[b]] + req:set_response("

RESULT: " .. param_a .. "*" .. param_b .. + "=" .. param_a * param_b .. "

") + end''' + } + lua /lua { + default type = text/html + script file = test.lua + cache period = 30s + } + lua /luawait { + script='''function handle_get_root(req) + local ms = req:query_param[[ms]] + if not ms then ms = 1234 end + req:say("sleeping "..ms.."ms") + req:sleep(ms) + req:say("slept") + end''' + } + rewrite /pattern { + pattern (%d+)/backref { + condition backref { + 1 = 42 + } + rewrite as = /hello?name=fourtytwo + } + pattern foo/(%d+)(%a)(%d+) { + redirect to = /hello?name=pre%2middle%3othermiddle%1post + } + pattern bar/(%d+)/test { + rewrite as = /hello?name=rewritten%1 + } + pattern lua/redir/(%d+)x(%d+) { + expand_with_lua = true + redirect to = ''' + function handle_rewrite(req, captures) + local r = captures[1] * captures[2] + return '/hello?name=redirected' .. r + end + ''' + } + pattern lua/rewrite/(%d+)x(%d+) { + expand_with_lua = true + rewrite as = """function handle_rewrite(req, captures) + local r = captures[1] * captures[2] + return '/hello?name=rewritten' .. r + end""" + } + } + serve_files / { + path = ./wwwroot + + # When requesting for file.ext, look for a smaller/newer file.ext.gz, + # and serve that instead if `Accept-Encoding: gzip` is in the + # request headers. + serve precompressed files = true + + cache for = ${cache_for} + } +} + +headers { + server = lwan/testrunner + x-global-header = present +} diff --git a/src/bin/tools/CMakeLists.txt b/src/bin/tools/CMakeLists.txt new file mode 100644 index 000000000..4a047e210 --- /dev/null +++ b/src/bin/tools/CMakeLists.txt @@ -0,0 +1,48 @@ +if (CMAKE_CROSSCOMPILING) + set(IMPORT_EXECUTABLES "IMPORTFILE-NOTFILE" CACHE FILEPATH "Point it to the export file from a native build") + include(${IMPORT_EXECUTABLES}) +else () + add_executable(mimegen + mimegen.c + ${CMAKE_SOURCE_DIR}/src/lib/hash.c + ${CMAKE_SOURCE_DIR}/src/lib/missing.c + ${CMAKE_SOURCE_DIR}/src/lib/lwan-status.c + ) + if (LWAN_HAVE_BROTLI) + message(STATUS "Using Brotli for mimegen") + target_link_libraries(mimegen ${BROTLI_LDFLAGS}) + elseif (LWAN_HAVE_ZSTD) + message(STATUS "Using Zstd for mimegen") + target_link_libraries(mimegen ${ZSTD_LDFLAGS}) + else () + find_library(ZOPFLI_LIBRARY NAMES zopfli PATHS /usr/lib /usr/local/lib) + if (ZOPFLI_LIBRARY) + message(STATUS "Using Zopfli (${ZOPFLI_LIBRARY}) for mimegen") + target_link_libraries(mimegen ${ZOPFLI_LIBRARY}) + target_compile_definitions(mimegen PUBLIC -DLWAN_HAVE_ZOPFLI=1) + else () + message(STATUS "Using zlib (${ZLIB_LIBRARIES}) for mimegen") + target_link_libraries(mimegen ${ZLIB_LIBRARIES}) + endif () + endif () + + add_executable(bin2hex + bin2hex.c + ) + + add_executable(configdump + configdump.c + ${CMAKE_SOURCE_DIR}/src/lib/lwan-config.c + ${CMAKE_SOURCE_DIR}/src/lib/lwan-status.c + ${CMAKE_SOURCE_DIR}/src/lib/lwan-strbuf.c + ${CMAKE_SOURCE_DIR}/src/lib/missing.c + ${CMAKE_SOURCE_DIR}/src/lib/hash.c + ) + + add_executable(weighttp weighttp.c) + target_link_libraries(weighttp ${CMAKE_THREAD_LIBS_INIT}) + + add_executable(statuslookupgen statuslookupgen.c) + + export(TARGETS statuslookupgen weighttp configdump mimegen bin2hex FILE ${CMAKE_BINARY_DIR}/ImportExecutables.cmake) +endif () diff --git a/src/bin/tools/COPYING.weighttp b/src/bin/tools/COPYING.weighttp new file mode 100644 index 000000000..7328b4f41 --- /dev/null +++ b/src/bin/tools/COPYING.weighttp @@ -0,0 +1,33 @@ +Copyright (c) 2016, Glue Logic LLC. All rights reserved. code()gluelogic.com + +License: MIT License (see below) + +================ + +This rewrite is based on weighttp by Thomas Porzelt + git://git.lighttpd.net/weighttp + https://github.com/lighttpd/weighttp/ + +================ + +The MIT License + +Copyright (c) 2009-2011 Thomas Porzelt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/bin/tools/bin2hex.c b/src/bin/tools/bin2hex.c new file mode 100644 index 000000000..7dd498b43 --- /dev/null +++ b/src/bin/tools/bin2hex.c @@ -0,0 +1,158 @@ +/* + * bin2hex - convert binary files to hexdump + * Copyright (c) 2017 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +static int bin2hex_mmap(const char *path, const char *identifier) +{ + int fd = open(path, O_RDONLY | O_CLOEXEC); + struct stat st; + char *ptr; + off_t i; + + if (fd < 0) + return -errno; + + if (fstat(fd, &st) < 0) { + close(fd); + return -errno; + } + + ptr = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (ptr == (void *)MAP_FAILED) + return -errno; + + printf("static const unsigned char %s[] = {\n", identifier); + printf(" "); + + int bytes_in_this_line = 0; + for (i = 0; i < st.st_size; i++) { + printf("0x%02x,", ptr[i] & 0xff); + + bytes_in_this_line++; + if (bytes_in_this_line == 11) { + printf("\n "); + bytes_in_this_line = 0; + } else { + printf(" "); + } + } + + printf("\n};\n"); + + printf("static const struct lwan_value %s_value = {.value = (char *)%s, .len = " + "sizeof(%s)};\n", + identifier, identifier, identifier); + + printf("\n"); + + munmap(ptr, (size_t)st.st_size); + + return 0; +} + +static int bin2hex_incbin(const char *path, const char *identifier) +{ + printf("__asm__(\".section \\\".rodata\\\"\\n\"\n"); + printf(" \"%s_start:\\n\"\n", identifier); + printf(" \".incbin \\\"%s\\\"\\n\"\n", path); + printf(" \"%s_end:\\n\"\n", identifier); + printf(" \".previous\\n\");\n"); + printf("static struct lwan_value %s_value;\n", identifier); + printf("__attribute__((visibility(\"internal\"))) extern char %s_start[], %s_end[];\n", identifier, identifier); + + return 0; +} + +static int bin2hex(const char *path, const char *identifier) +{ + int r = 0; + + printf("\n/* Contents of %s available through %s_value */\n", path, identifier); + + printf("#if !defined(NO_INCBIN)\n"); + r |= bin2hex_incbin(path, identifier); + printf("#else\n"); + r |= bin2hex_mmap(path, identifier); + printf("#endif\n\n"); + + return r; +} + +int main(int argc, char *argv[]) +{ + int arg; + + if (argc < 2) { + fprintf(stderr, "Usage: %s /path/to/file file_identifier [...]\n", + argv[0]); + return 1; + } + + if ((argc - 1) % 2) { + fprintf(stderr, "%s: Even number of arguments required\n", argv[0]); + return 1; + } + + printf("/* Auto generated by %s, do not edit. */\n", argv[0]); + printf("#pragma once\n\n"); + printf("#include \"lwan-private.h\"\n"); + + printf("#if defined(NO_INCBIN)\n"); + printf(" /* do nothing */\n"); + printf("#elif defined(__GNUC__) || defined(__clang__)\n"); + printf("#define WANT_INCBIN\n"); + printf("#endif\n"); + + for (arg = 1; arg < argc; arg += 2) { + const char *path = argv[arg]; + const char *identifier = argv[arg + 1]; + int r = bin2hex(path, identifier); + + if (r < 0) { + fprintf(stderr, "Could not dump %s: %s\n", path, strerror(errno)); + return 1; + } + } + + printf("#if !defined(NO_INCBIN)\n"); + printf("LWAN_CONSTRUCTOR(bin2hex_%016lx, 0)\n", (uintptr_t)argv); + printf("{\n"); + for (arg = 1; arg < argc; arg += 2) { + const char *identifier = argv[arg + 1]; + + printf(" %s_value = (struct lwan_value) {.value = (char *)%s_start, " + ".len = (size_t)(%s_end - %s_start)};\n", + identifier, identifier, identifier, identifier); + } + printf("}\n"); + printf("#endif\n"); + + return 0; +} diff --git a/src/bin/tools/configdump.c b/src/bin/tools/configdump.c new file mode 100644 index 000000000..d1893d4d1 --- /dev/null +++ b/src/bin/tools/configdump.c @@ -0,0 +1,101 @@ +/* + * lwan - web server + * Copyright (c) 2019 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include + +#include "lwan-config.h" +#include "lwan-status.h" + +static void indent(int level) +{ + for (int i = 0; i < level; i++) { + putchar(' '); + putchar(' '); + } +} + +static void +dump(struct config *config, int indent_level) +{ + const struct config_line *line; + + if (indent_level > 64) { + lwan_status_critical("Indent level %d above limit, aborting", + indent_level); + return; + } + + while ((line = config_read_line(config))) { + switch (line->type) { + case CONFIG_LINE_TYPE_LINE: + indent(indent_level); + + if (strchr(line->value, '\n')) + printf("%s = '''%s'''\n", line->key, line->value); + else + printf("%s = %s\n", line->key, line->value); + break; + + case CONFIG_LINE_TYPE_SECTION_END: + if (indent_level == 0) + lwan_status_critical("Section ended before it started"); + return; + + case CONFIG_LINE_TYPE_SECTION: + indent(indent_level); + printf("%s %s {\n", line->key, line->value); + + dump(config, indent_level + 1); + + indent(indent_level); + printf("}\n"); + break; + } + } +} + +int main(int argc, char *argv[]) +{ + struct config *config; + + if (argc < 2) { + lwan_status_critical("Usage: %s /path/to/config/file.conf", argv[0]); + return 1; + } + + config = config_open(argv[1]); + if (!config) { + lwan_status_critical_perror("Could not open configuration file %s", + argv[1]); + return 1; + } + + dump(config, 0); + + if (config_last_error(config)) { + lwan_status_critical("Error while reading configuration file (line %d): %s\n", + config_cur_line(config), + config_last_error(config)); + } + + config_close(config); + + return 0; +} diff --git a/tools/mime.types b/src/bin/tools/mime.types similarity index 86% rename from tools/mime.types rename to src/bin/tools/mime.types index b90b16587..adf47992b 100644 --- a/tools/mime.types +++ b/src/bin/tools/mime.types @@ -14,43 +14,76 @@ # MIME type (lowercased) Extensions # ============================================ ========== # application/1d-interleaved-parityfec +# application/3gpdash-qoe-report+xml # application/3gpp-ims+xml +# application/a2l # application/activemessage +# application/alto-costmap+json +# application/alto-costmapfilter+json +# application/alto-directory+json +# application/alto-endpointcost+json +# application/alto-endpointcostparams+json +# application/alto-endpointprop+json +# application/alto-endpointpropparams+json +# application/alto-error+json +# application/alto-networkmap+json +# application/alto-networkmapfilter+json +# application/aml application/andrew-inset ez # application/applefile application/applixware aw +# application/atf +# application/atfx application/atom+xml atom application/atomcat+xml atomcat +# application/atomdeleted+xml # application/atomicmail application/atomsvc+xml atomsvc +# application/atxml # application/auth-policy+xml +# application/bacnet-xdd+zip # application/batch-smtp # application/beep+xml +# application/calendar+json # application/calendar+xml +# application/call-completion # application/cals-1840 +# application/cbor # application/ccmp+xml application/ccxml+xml ccxml +# application/cdfx+xml application/cdmi-capability cdmia application/cdmi-container cdmic application/cdmi-domain cdmid application/cdmi-object cdmio application/cdmi-queue cdmiq +# application/cdni +# application/cea # application/cea-2018+xml # application/cellml+xml # application/cfw +# application/cms # application/cnrp+xml +# application/coap-group+json # application/commonground # application/conference-info+xml # application/cpl+xml +# application/csrattrs # application/csta+xml # application/cstadata+xml +# application/csvm+json application/cu-seeme cu # application/cybercash +# application/dash+xml +# application/dashdelta application/davmount+xml davmount # application/dca-rft +# application/dcd # application/dec-dx # application/dialog-info+xml # application/dicom +# application/dii +# application/dit # application/dns application/docbook+xml dbk # application/dskpp+xml @@ -61,7 +94,15 @@ application/ecmascript ecma # application/edi-consent # application/edi-x12 # application/edifact +# application/efi +# application/emergencycalldata.comment+xml +# application/emergencycalldata.deviceinfo+xml +# application/emergencycalldata.providerinfo+xml +# application/emergencycalldata.serviceinfo+xml +# application/emergencycalldata.subscriberinfo+xml application/emma+xml emma +# application/emotionml+xml +# application/encaprtp # application/epp+xml application/epub+zip epub # application/eshop @@ -69,12 +110,15 @@ application/epub+zip epub application/exi exi # application/fastinfoset # application/fastsoap +# application/fdt+xml # application/fits application/font-tdpfr pfr # application/framework-attributes+xml +# application/geo+json application/gml+xml gml application/gpx+xml gpx application/gxf gxf +# application/gzip # application/h224 # application/held+xml # application/http @@ -94,15 +138,29 @@ application/inkml+xml ink inkml application/ipfix ipfix # application/ipp # application/isup +# application/its+xml application/java-archive jar application/java-serialized-object ser application/java-vm class -application/javascript js +# application/jose +# application/jose+json +# application/jrd+json application/json json +# application/json-patch+json +# application/json-seq application/jsonml+json jsonml +# application/jwk+json +# application/jwk-set+json +# application/jwt # application/kpml-request+xml # application/kpml-response+xml +# application/ld+json +# application/lgr+xml +# application/link-format +# application/load-control+xml application/lost+xml lostxml +# application/lostsync+xml +# application/lxf application/mac-binhex40 hqx application/mac-compactpro cpt # application/macwriteii @@ -110,9 +168,9 @@ application/mads+xml mads application/marc mrc application/marcxml+xml mrcx application/mathematica ma nb mb +application/mathml+xml mathml # application/mathml-content+xml # application/mathml-presentation+xml -application/mathml+xml mathml # application/mbms-associated-procedure-description+xml # application/mbms-deregister+xml # application/mbms-envelope+xml @@ -122,13 +180,17 @@ application/mathml+xml mathml # application/mbms-reception-report+xml # application/mbms-register+xml # application/mbms-register-response+xml +# application/mbms-schedule+xml # application/mbms-user-service-description+xml application/mbox mbox +# application/media-policy-dataset+xml # application/media_control+xml application/mediaservercontrol+xml mscml +# application/merge-patch+json application/metalink+xml metalink application/metalink4+xml meta4 application/mets+xml mets +# application/mf4 # application/mikey application/mods+xml mods # application/moss-keys @@ -140,6 +202,8 @@ application/mp4 mp4s # application/mpeg4-generic # application/mpeg4-iod # application/mpeg4-iod-xmt +# application/mrb-consumer+xml +# application/mrb-publish+xml # application/msc-ivr+xml # application/msc-mixer+xml application/msword doc dot @@ -148,19 +212,23 @@ application/mxf mxf # application/news-checkgroups # application/news-groupinfo # application/news-transmission +# application/nlsml+xml # application/nss # application/ocsp-request # application/ocsp-response application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy application/oda oda +# application/odx application/oebps-package+xml opf application/ogg ogx application/omdoc+xml omdoc application/onenote onetoc onetoc2 onetmp onepkg application/oxps oxps +# application/p2p-overlay+xml # application/parityfec application/patch-ops-error+xml xer application/pdf pdf +# application/pdx application/pgp-encrypted pgp # application/pgp-keys application/pgp-signature asc sig @@ -168,6 +236,7 @@ application/pics-rules prf # application/pidf+xml # application/pidf-diff+xml application/pkcs10 p10 +# application/pkcs12 application/pkcs7-mime p7m p7c application/pkcs7-signature p7s application/pkcs8 p8 @@ -179,20 +248,29 @@ application/pkixcmp pki application/pls+xml pls # application/poc-settings+xml application/postscript ai eps ps +# application/ppsp-tracker+json +# application/problem+json +# application/problem+xml +# application/provenance+xml # application/prs.alvestrand.titrax-sheet application/prs.cww cww +# application/prs.hpub+zip # application/prs.nprend # application/prs.plucker # application/prs.rdf-xml-crypt # application/prs.xsf+xml application/pskc+xml pskcxml # application/qsig +# application/raptorfec +# application/rdap+json application/rdf+xml rdf application/reginfo+xml rif application/relax-ng-compact-syntax rnc # application/remote-printing +# application/reputon+json application/resource-lists+xml rl application/resource-lists-diff+xml rld +# application/rfc+xml # application/riscos # application/rlmi+xml application/rls-services+xml rs @@ -203,15 +281,21 @@ application/rpki-roa roa application/rsd+xml rsd application/rss+xml rss application/rtf rtf +# application/rtploopback # application/rtx # application/samlassertion+xml # application/samlmetadata+xml application/sbml+xml sbml +# application/scaip+xml +# application/scim+json application/scvp-cv-request scq application/scvp-cv-response scs application/scvp-vp-request spq application/scvp-vp-response spp application/sdp sdp +# application/sep+xml +# application/sep-exi +# application/session-info # application/set-payment application/set-payment-initiation setpay # application/set-registration @@ -226,11 +310,13 @@ application/shf+xml shf # application/slate # application/smil application/smil+xml smi smil +# application/smpte336m # application/soap+fastinfoset # application/soap+xml application/sparql-query rq application/sparql-results+xml srx # application/spirits-event+xml +# application/sql application/srgs gram application/srgs+xml grxml application/sru+xml sru @@ -252,25 +338,42 @@ application/thraud+xml tfi # application/timestamp-query # application/timestamp-reply application/timestamped-data tsd +# application/ttml+xml # application/tve-trigger # application/ulpfec +# application/urc-grpsheet+xml +# application/urc-ressheet+xml +# application/urc-targetdesc+xml +# application/urc-uisocketdesc+xml +# application/vcard+json # application/vcard+xml # application/vemmi # application/vividence.scriptfile +# application/vnd.3gpp-prose+xml +# application/vnd.3gpp-prose-pc3ch+xml +# application/vnd.3gpp.access-transfer-events+xml # application/vnd.3gpp.bsf+xml +# application/vnd.3gpp.mid-call+xml application/vnd.3gpp.pic-bw-large plb application/vnd.3gpp.pic-bw-small psb application/vnd.3gpp.pic-bw-var pvb # application/vnd.3gpp.sms +# application/vnd.3gpp.sms+xml +# application/vnd.3gpp.srvcc-ext+xml +# application/vnd.3gpp.srvcc-info+xml +# application/vnd.3gpp.state-and-event-info+xml +# application/vnd.3gpp.ussd+xml # application/vnd.3gpp2.bcmcsinfo+xml # application/vnd.3gpp2.sms application/vnd.3gpp2.tcap tcap +# application/vnd.3lightssoftware.imagescal application/vnd.3m.post-it-notes pwn application/vnd.accpac.simply.aso aso application/vnd.accpac.simply.imp imp application/vnd.acucobol acu application/vnd.acucorp atc acutc application/vnd.adobe.air-application-installer-package+zip air +# application/vnd.adobe.flash.movie application/vnd.adobe.formscentral.fcdt fcdt application/vnd.adobe.fxp fxp fxpl # application/vnd.adobe.partial-upload @@ -282,42 +385,62 @@ application/vnd.ahead.space ahead application/vnd.airzip.filesecure.azf azf application/vnd.airzip.filesecure.azs azs application/vnd.amazon.ebook azw +# application/vnd.amazon.mobi8-ebook application/vnd.americandynamics.acc acc application/vnd.amiga.ami ami # application/vnd.amundsen.maze+xml application/vnd.android.package-archive apk +# application/vnd.anki application/vnd.anser-web-certificate-issue-initiation cii application/vnd.anser-web-funds-transfer-initiation fti application/vnd.antix.game-component atx +# application/vnd.apache.thrift.binary +# application/vnd.apache.thrift.compact +# application/vnd.apache.thrift.json +# application/vnd.api+json application/vnd.apple.installer+xml mpkg application/vnd.apple.mpegurl m3u8 # application/vnd.arastra.swi application/vnd.aristanetworks.swi swi +# application/vnd.artsquare application/vnd.astraea-software.iota iota application/vnd.audiograph aep # application/vnd.autopackage # application/vnd.avistar+xml +# application/vnd.balsamiq.bmml+xml +# application/vnd.balsamiq.bmpr +# application/vnd.bekitzur-stech+json +# application/vnd.biopax.rdf+xml application/vnd.blueice.multipass mpm # application/vnd.bluetooth.ep.oob +# application/vnd.bluetooth.le.oob application/vnd.bmi bmi application/vnd.businessobjects rep # application/vnd.cab-jscript # application/vnd.canon-cpdl # application/vnd.canon-lips # application/vnd.cendio.thinlinc.clientconf +# application/vnd.century-systems.tcp_stream application/vnd.chemdraw+xml cdxml +# application/vnd.chess-pgn application/vnd.chipnuts.karaoke-mmd mmd application/vnd.cinderella cdy # application/vnd.cirpack.isdn-ext +# application/vnd.citationstyles.style+xml application/vnd.claymore cla application/vnd.cloanto.rp9 rp9 application/vnd.clonk.c4group c4g c4d c4f c4p c4u application/vnd.cluetrust.cartomobile-config c11amc application/vnd.cluetrust.cartomobile-config-pkg c11amz +# application/vnd.coffeescript # application/vnd.collection+json +# application/vnd.collection.doc+json +# application/vnd.collection.next+json +# application/vnd.comicbook+zip # application/vnd.commerce-battelle application/vnd.commonspace csp application/vnd.contact.cmsg cdbcmsg +# application/vnd.coreos.ignition+json application/vnd.cosmocaller cmc application/vnd.crick.clicker clkx application/vnd.crick.clicker.keyboard clkk @@ -335,22 +458,32 @@ application/vnd.cups-ppd ppd # application/vnd.curl application/vnd.curl.car car application/vnd.curl.pcurl pcurl +# application/vnd.cyan.dean.root+xml # application/vnd.cybank application/vnd.dart dart application/vnd.data-vision.rdz rdz +# application/vnd.debian.binary-package application/vnd.dece.data uvf uvvf uvd uvvd application/vnd.dece.ttml+xml uvt uvvt application/vnd.dece.unspecified uvx uvvx application/vnd.dece.zip uvz uvvz application/vnd.denovo.fcselayout-link fe_launch +# application/vnd.desmume.movie # application/vnd.dir-bi.plate-dl-nosuffix +# application/vnd.dm.delegation+xml application/vnd.dna dna +# application/vnd.document+json application/vnd.dolby.mlp mlp # application/vnd.dolby.mobile.1 # application/vnd.dolby.mobile.2 +# application/vnd.doremir.scorecloud-binary-document application/vnd.dpgraph dpg application/vnd.dreamfactory dfac +# application/vnd.drive+json application/vnd.ds-keypoint kpxx +# application/vnd.dtg.local +# application/vnd.dtg.local.flash +# application/vnd.dtg.local.html application/vnd.dvb.ait ait # application/vnd.dvb.dvbj # application/vnd.dvb.esgcontainer @@ -372,6 +505,7 @@ application/vnd.dvb.ait ait application/vnd.dvb.service svc # application/vnd.dxr application/vnd.dynageo geo +# application/vnd.dzr # application/vnd.easykaraoke.cdgdownload # application/vnd.ecdis-update application/vnd.ecowin.chart mag @@ -382,6 +516,7 @@ application/vnd.ecowin.chart mag # application/vnd.ecowin.seriesupdate # application/vnd.emclient.accessrequest+xml application/vnd.enliven nml +# application/vnd.enphase.envoy # application/vnd.eprints.data+xml application/vnd.epson.esf esf application/vnd.epson.msf msf @@ -391,6 +526,8 @@ application/vnd.epson.ssf ssf # application/vnd.ericsson.quickcall application/vnd.eszigno3+xml es3 et3 # application/vnd.etsi.aoc+xml +# application/vnd.etsi.asic-e+zip +# application/vnd.etsi.asic-s+zip # application/vnd.etsi.cug+xml # application/vnd.etsi.iptvcommand+xml # application/vnd.etsi.iptvdiscovery+xml @@ -402,20 +539,26 @@ application/vnd.eszigno3+xml es3 et3 # application/vnd.etsi.iptvsync+xml # application/vnd.etsi.iptvueprofile+xml # application/vnd.etsi.mcid+xml +# application/vnd.etsi.mheg5 # application/vnd.etsi.overload-control-policy-dataset+xml +# application/vnd.etsi.pstn+xml # application/vnd.etsi.sci+xml # application/vnd.etsi.simservs+xml +# application/vnd.etsi.timestamp-token # application/vnd.etsi.tsl+xml # application/vnd.etsi.tsl.der # application/vnd.eudora.data application/vnd.ezpix-album ez2 application/vnd.ezpix-package ez3 # application/vnd.f-secure.mobile +# application/vnd.fastcopy-disk-image application/vnd.fdf fdf application/vnd.fdsn.mseed mseed application/vnd.fdsn.seed seed dataless # application/vnd.ffsns +# application/vnd.filmit.zfc # application/vnd.fints +# application/vnd.firemonkeys.cloudcell application/vnd.flographit gph application/vnd.fluxtime.clip ftc # application/vnd.font-fontforge-sfd @@ -430,13 +573,15 @@ application/vnd.fujitsu.oasysgp fg5 application/vnd.fujitsu.oasysprs bh2 # application/vnd.fujixerox.art-ex # application/vnd.fujixerox.art4 -# application/vnd.fujixerox.hbpl application/vnd.fujixerox.ddd ddd application/vnd.fujixerox.docuworks xdw application/vnd.fujixerox.docuworks.binder xbd +# application/vnd.fujixerox.docuworks.container +# application/vnd.fujixerox.hbpl # application/vnd.fut-misnet application/vnd.fuzzysheet fzs application/vnd.genomatix.tuxedo txd +# application/vnd.geo+json # application/vnd.geocube+xml application/vnd.geogebra.file ggb application/vnd.geogebra.tool ggt @@ -444,11 +589,15 @@ application/vnd.geometry-explorer gex gre application/vnd.geonext gxt application/vnd.geoplan g2w application/vnd.geospace g3w +# application/vnd.gerber # application/vnd.globalplatform.card-content-mgt # application/vnd.globalplatform.card-content-mgt-response application/vnd.gmx gmx application/vnd.google-earth.kml+xml kml application/vnd.google-earth.kmz kmz +# application/vnd.gov.sk.e-form+xml +# application/vnd.gov.sk.e-form+zip +# application/vnd.gov.sk.xmldatacontainer+xml application/vnd.grafeq gqf gqs # application/vnd.gridmp application/vnd.groove-account gac @@ -463,6 +612,8 @@ application/vnd.hal+xml hal application/vnd.handheld-entertainment+xml zmm application/vnd.hbci hbci # application/vnd.hcl-bireports +# application/vnd.hdt +# application/vnd.heroku+json application/vnd.hhe.lesson-player les application/vnd.hp-hpgl hpgl application/vnd.hp-hpid hpid @@ -472,6 +623,7 @@ application/vnd.hp-pcl pcl application/vnd.hp-pclxl pclxl # application/vnd.httphone application/vnd.hydrostatix.sof-data sfd-hdstx +# application/vnd.hyperdrive+json # application/vnd.hzn-3d-crossword # application/vnd.ibm.afplinedata # application/vnd.ibm.electronic-media @@ -480,9 +632,19 @@ application/vnd.ibm.modcap afp listafp list3820 application/vnd.ibm.rights-management irm application/vnd.ibm.secure-container sc application/vnd.iccprofile icc icm +# application/vnd.ieee.1905 application/vnd.igloader igl application/vnd.immervision-ivp ivp application/vnd.immervision-ivu ivu +# application/vnd.ims.imsccv1p1 +# application/vnd.ims.imsccv1p2 +# application/vnd.ims.imsccv1p3 +# application/vnd.ims.lis.v2.result+json +# application/vnd.ims.lti.v2.toolconsumerprofile+json +# application/vnd.ims.lti.v2.toolproxy+json +# application/vnd.ims.lti.v2.toolproxy.id+json +# application/vnd.ims.lti.v2.toolsettings+json +# application/vnd.ims.lti.v2.toolsettings.simple+json # application/vnd.informedcontrol.rms+xml # application/vnd.informix-visionary # application/vnd.infotech.project @@ -495,6 +657,7 @@ application/vnd.intergeo i2g # application/vnd.intertrust.nncp application/vnd.intu.qbo qbo application/vnd.intu.qfx qfx +# application/vnd.iptc.g2.catalogitem+xml # application/vnd.iptc.g2.conceptitem+xml # application/vnd.iptc.g2.knowledgeitem+xml # application/vnd.iptc.g2.newsitem+xml @@ -517,6 +680,7 @@ application/vnd.jam jam application/vnd.jcp.javame.midlet-rms rms application/vnd.jisp jisp application/vnd.joost.joda-archive joda +# application/vnd.jsk.isdn-ngn application/vnd.kahootz ktz ktr application/vnd.kde.karbon karbon application/vnd.kde.kchart chrt @@ -543,18 +707,24 @@ application/vnd.lotus-organizer org application/vnd.lotus-screencam scm application/vnd.lotus-wordpro lwp application/vnd.macports.portpkg portpkg +# application/vnd.mapbox-vector-tile # application/vnd.marlin.drm.actiontoken+xml # application/vnd.marlin.drm.conftoken+xml # application/vnd.marlin.drm.license+xml # application/vnd.marlin.drm.mdcf +# application/vnd.mason+json +# application/vnd.maxmind.maxmind-db application/vnd.mcd mcd application/vnd.medcalcdata mc1 application/vnd.mediastation.cdkey cdkey # application/vnd.meridian-slingshot application/vnd.mfer mwf application/vnd.mfmp mfm +# application/vnd.micro+json application/vnd.micrografx.flo flo application/vnd.micrografx.igx igx +# application/vnd.microsoft.portable-executable +# application/vnd.miele+json application/vnd.mif mif # application/vnd.minisoft-hp3000-save # application/vnd.mitsubishi.misty-guard.trustweb @@ -576,6 +746,7 @@ application/vnd.mophun.certificate mpc # application/vnd.motorola.flexsuite.wem # application/vnd.motorola.iprm application/vnd.mozilla.xul+xml xul +# application/vnd.ms-3mfdocument application/vnd.ms-artgalry cil # application/vnd.ms-asf application/vnd.ms-cab-compressed cab @@ -602,9 +773,15 @@ application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm application/vnd.ms-powerpoint.slide.macroenabled.12 sldm application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm application/vnd.ms-powerpoint.template.macroenabled.12 potm +# application/vnd.ms-printdevicecapabilities+xml # application/vnd.ms-printing.printticket+xml +# application/vnd.ms-printschematicket+xml application/vnd.ms-project mpp mpt # application/vnd.ms-tnef +# application/vnd.ms-windows.devicepairing +# application/vnd.ms-windows.nwprinting.oob +# application/vnd.ms-windows.printerpairing +# application/vnd.ms-windows.wsd.oob # application/vnd.ms-wmdrm.lic-chlg-req # application/vnd.ms-wmdrm.lic-resp # application/vnd.ms-wmdrm.meter-chlg-req @@ -614,6 +791,7 @@ application/vnd.ms-word.template.macroenabled.12 dotm application/vnd.ms-works wps wks wcm wdb application/vnd.ms-wpl wpl application/vnd.ms-xpsdocument xps +# application/vnd.msa-disk-image application/vnd.mseq mseq # application/vnd.msign # application/vnd.multiad.creator @@ -627,6 +805,8 @@ application/vnd.mynfc taglet # application/vnd.nervana # application/vnd.netfpx application/vnd.neurolanguage.nlu nlu +# application/vnd.nintendo.nitro.rom +# application/vnd.nintendo.snes.rom application/vnd.nitf ntf nitf application/vnd.noblenet-directory nnd application/vnd.noblenet-sealer nns @@ -634,8 +814,8 @@ application/vnd.noblenet-web nnw # application/vnd.nokia.catalogs # application/vnd.nokia.conml+wbxml # application/vnd.nokia.conml+xml -# application/vnd.nokia.isds-radio-presets # application/vnd.nokia.iptv.config+xml +# application/vnd.nokia.isds-radio-presets # application/vnd.nokia.landmark+wbxml # application/vnd.nokia.landmark+xml # application/vnd.nokia.landmarkcollection+xml @@ -650,7 +830,9 @@ application/vnd.nokia.radio-presets rpss application/vnd.novadigm.edm edm application/vnd.novadigm.edx edx application/vnd.novadigm.ext ext +# application/vnd.ntt-local.content-share # application/vnd.ntt-local.file-transfer +# application/vnd.ntt-local.ogw_remote-access # application/vnd.ntt-local.sip-ta_remote # application/vnd.ntt-local.sip-ta_tcp_stream application/vnd.oasis.opendocument.chart odc @@ -703,12 +885,15 @@ application/vnd.olpc-sugar xo # application/vnd.oma.cab-address-book+xml # application/vnd.oma.cab-feature-handler+xml # application/vnd.oma.cab-pcc+xml +# application/vnd.oma.cab-subs-invite+xml # application/vnd.oma.cab-user-prefs+xml # application/vnd.oma.dcd # application/vnd.oma.dcdc application/vnd.oma.dd2+xml dd2 # application/vnd.oma.drm.risd+xml # application/vnd.oma.group-usage-list+xml +# application/vnd.oma.lwm2m+json +# application/vnd.oma.lwm2m+tlv # application/vnd.oma.pal+xml # application/vnd.oma.poc.detailed-progress-report+xml # application/vnd.oma.poc.final-report+xml @@ -722,6 +907,10 @@ application/vnd.oma.dd2+xml dd2 # application/vnd.omads-file+xml # application/vnd.omads-folder+xml # application/vnd.omaloc-supl-init +# application/vnd.onepager +# application/vnd.openblox.game+xml +# application/vnd.openblox.game-binary +# application/vnd.openeye.oeb application/vnd.openofficeorg.extension oxt # application/vnd.openxmlformats-officedocument.custom-properties+xml # application/vnd.openxmlformats-officedocument.customxmlproperties+xml @@ -797,16 +986,21 @@ application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx # application/vnd.openxmlformats-package.core-properties+xml # application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml # application/vnd.openxmlformats-package.relationships+xml -# application/vnd.quobject-quoxdocument +# application/vnd.oracle.resource+json +# application/vnd.orange.indata # application/vnd.osa.netdeploy application/vnd.osgeo.mapguide.package mgp # application/vnd.osgi.bundle application/vnd.osgi.dp dp application/vnd.osgi.subsystem esa # application/vnd.otps.ct-kip+xml +# application/vnd.oxli.countgraph +# application/vnd.pagerduty+json application/vnd.palm pdb pqa oprc +# application/vnd.panoply # application/vnd.paos.xml application/vnd.pawaafile paw +# application/vnd.pcos application/vnd.pg.format str application/vnd.pg.osasli ei6 # application/vnd.piaccess.application-licence @@ -828,7 +1022,9 @@ application/vnd.pvi.ptid1 ptid # application/vnd.pwg-multiplexed # application/vnd.pwg-xhtml-print+xml # application/vnd.qualcomm.brew-app-res +# application/vnd.quarantainenet application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb +# application/vnd.quobject-quoxdocument # application/vnd.radisys.moml+xml # application/vnd.radisys.msml+xml # application/vnd.radisys.msml-audit+xml @@ -846,6 +1042,7 @@ application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb # application/vnd.radisys.msml-dialog-transform+xml # application/vnd.rainstor.data # application/vnd.rapid +# application/vnd.rar application/vnd.realvnc.bed bed application/vnd.recordare.musicxml mxl application/vnd.recordare.musicxml+xml musicxml @@ -882,6 +1079,7 @@ application/vnd.shana.informed.formtemplate itp application/vnd.shana.informed.interchange iif application/vnd.shana.informed.package ipk application/vnd.simtech-mindmapper twd twds +# application/vnd.siren+json application/vnd.smaf mmf # application/vnd.smart.notebook application/vnd.smart.teacher teacher @@ -902,6 +1100,7 @@ application/vnd.stardivision.writer-global sgl application/vnd.stepmania.package smzip application/vnd.stepmania.stepchart sm # application/vnd.street-stream +# application/vnd.sun.wadl+xml application/vnd.sun.xml.calc sxc application/vnd.sun.xml.calc.template stc application/vnd.sun.xml.draw sxd @@ -912,7 +1111,6 @@ application/vnd.sun.xml.math sxm application/vnd.sun.xml.writer sxw application/vnd.sun.xml.writer.global sxg application/vnd.sun.xml.writer.template stw -# application/vnd.sun.wadl+xml application/vnd.sus-calendar sus susp application/vnd.svd svd # application/vnd.swiftview-ics @@ -921,9 +1119,15 @@ application/vnd.syncml+xml xsm application/vnd.syncml.dm+wbxml bdm application/vnd.syncml.dm+xml xdm # application/vnd.syncml.dm.notification +# application/vnd.syncml.dmddf+wbxml +# application/vnd.syncml.dmddf+xml +# application/vnd.syncml.dmtnds+wbxml +# application/vnd.syncml.dmtnds+xml # application/vnd.syncml.ds.notification application/vnd.tao.intent-module-archive tao application/vnd.tcpdump.pcap pcap cap dmp +# application/vnd.tmd.mediaflex.api+xml +# application/vnd.tml application/vnd.tmobile-livetv tmo application/vnd.trid.tpt tpt application/vnd.triscape.mxs mxs @@ -948,9 +1152,12 @@ application/vnd.uoml+xml uoml # application/vnd.uplanet.listcmd # application/vnd.uplanet.listcmd-wbxml # application/vnd.uplanet.signal +# application/vnd.uri-map +# application/vnd.valve.source.material application/vnd.vcx vcx # application/vnd.vd-study # application/vnd.vectorworks +# application/vnd.vel+json # application/vnd.verimatrix.vcas # application/vnd.vidsoft.vidconference application/vnd.visio vsd vst vss vsw @@ -963,7 +1170,9 @@ application/vnd.wap.wbxml wbxml application/vnd.wap.wmlc wmlc application/vnd.wap.wmlscriptc wmlsc application/vnd.webturbo wtb +# application/vnd.wfa.p2p # application/vnd.wfa.wsc +# application/vnd.windows.devicepairing # application/vnd.wmc # application/vnd.wmf.bootstrap # application/vnd.wolfram.mathematica @@ -976,6 +1185,7 @@ application/vnd.wt.stf stf # application/vnd.wv.csp+wbxml # application/vnd.wv.csp+xml # application/vnd.wv.ssp+xml +# application/vnd.xacml+json application/vnd.xara xar application/vnd.xfdl xfdl # application/vnd.xfdl.webform @@ -995,11 +1205,13 @@ application/vnd.yamaha.smaf-audio saf application/vnd.yamaha.smaf-phrase spf # application/vnd.yamaha.through-ngn # application/vnd.yamaha.tunnel-udpencap +# application/vnd.yaoweme application/vnd.yellowriver-custom-menu cmp application/vnd.zul zir zirz application/vnd.zzazz.deck+xml zaz application/voicexml+xml vxml # application/vq-rtcpxr +application/wasm wasm # application/watcherinfo+xml # application/whoispp-query # application/whoispp-response @@ -1027,8 +1239,8 @@ application/x-cdlink vcd application/x-cfs-compressed cfs application/x-chat chat application/x-chess-pgn pgn -application/x-conference nsc # application/x-compress +application/x-conference nsc application/x-cpio cpio application/x-csh csh application/x-debian-package deb udeb @@ -1047,14 +1259,11 @@ application/x-font-bdf bdf application/x-font-ghostscript gsf # application/x-font-libgrx application/x-font-linux-psf psf -application/x-font-otf otf application/x-font-pcf pcf application/x-font-snf snf # application/x-font-speedo # application/x-font-sunos-news -application/x-font-ttf ttf ttc application/x-font-type1 pfa pfb pfm afm -application/x-font-woff woff # application/x-font-vfont application/x-freearc arc application/x-futuresplash spl @@ -1116,6 +1325,7 @@ application/x-texinfo texinfo texi application/x-tgif obj application/x-ustar ustar application/x-wais-source src +# application/x-www-form-urlencoded application/x-x509-ca-cert der crt application/x-xfig fig application/x-xliff+xml xlf @@ -1123,6 +1333,7 @@ application/x-xpinstall xpi application/x-xz xz application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8 # application/x400-bp +# application/xacml+xml application/xaml+xml xaml # application/xcap-att+xml # application/xcap-caps+xml @@ -1130,14 +1341,15 @@ application/xcap-diff+xml xdf # application/xcap-el+xml # application/xcap-error+xml # application/xcap-ns+xml -# application/xcon-conference-info-diff+xml # application/xcon-conference-info+xml +# application/xcon-conference-info-diff+xml application/xenc+xml xenc application/xhtml+xml xhtml xht # application/xhtml-voice+xml application/xml xml xsl application/xml-dtd dtd # application/xml-external-parsed-entity +# application/xml-patch+xml # application/xmpp+xml application/xop+xml xop application/xproc+xml xpl @@ -1147,6 +1359,7 @@ application/xv+xml mxml xhvml xvml xvm application/yang yang application/yin+xml yin application/zip zip +# application/zlib # audio/1d-interleaved-parityfec # audio/32kadpcm # audio/3gpp @@ -1156,6 +1369,7 @@ audio/adpcm adp # audio/amr # audio/amr-wb # audio/amr-wb+ +# audio/aptx # audio/asc # audio/atrac-advanced-lossless # audio/atrac-x @@ -1174,6 +1388,7 @@ audio/basic au snd # audio/dv # audio/dvi4 # audio/eac3 +# audio/encaprtp # audio/evrc # audio/evrc-qcp # audio/evrc0 @@ -1181,11 +1396,16 @@ audio/basic au snd # audio/evrcb # audio/evrcb0 # audio/evrcb1 +# audio/evrcnw +# audio/evrcnw0 +# audio/evrcnw1 # audio/evrcwb # audio/evrcwb0 # audio/evrcwb1 +# audio/evs # audio/example # audio/fwdred +# audio/g711-0 # audio/g719 # audio/g722 # audio/g7221 @@ -1212,31 +1432,33 @@ audio/basic au snd # audio/lpc audio/midi mid midi kar rmi # audio/mobile-xmf -audio/mp4 mp4a +audio/mp4 m4a mp4a # audio/mp4a-latm # audio/mpa # audio/mpa-robust audio/mpeg mpga mp2 mp2a mp3 m2a m3a # audio/mpeg4-generic # audio/musepack -audio/ogg oga ogg spx +audio/ogg oga ogg spx opus # audio/opus # audio/parityfec # audio/pcma # audio/pcma-wb -# audio/pcmu-wb # audio/pcmu +# audio/pcmu-wb # audio/prs.sid # audio/qcelp +# audio/raptorfec # audio/red # audio/rtp-enc-aescm128 # audio/rtp-midi +# audio/rtploopback # audio/rtx audio/s3m s3m audio/silk sil # audio/smv -# audio/smv0 # audio/smv-qcp +# audio/smv0 # audio/sp-midi # audio/speex # audio/t140c @@ -1308,13 +1530,22 @@ chemical/x-cml cml chemical/x-csml csml # chemical/x-pdb chemical/x-xyz xyz +font/collection ttc +font/otf otf +# font/sfnt +font/ttf ttf +font/woff woff +font/woff2 woff2 image/bmp bmp image/cgm cgm +# image/dicom-rle +# image/emf # image/example # image/fits image/g3fax g3 image/gif gif image/ief ief +# image/jls # image/jp2 image/jpeg jpeg jpg jpe # image/jpm @@ -1324,16 +1555,18 @@ image/ktx ktx image/png png image/prs.btif btif # image/prs.pti +# image/pwg-raster image/sgi sgi image/svg+xml svg svgz # image/t38 image/tiff tiff tif # image/tiff-fx image/vnd.adobe.photoshop psd +# image/vnd.airzip.accelerator.azv # image/vnd.cns.inf2 image/vnd.dece.graphic uvi uvvi uvg uvvg -image/vnd.dvb.subtitle sub image/vnd.djvu djvu djv +image/vnd.dvb.subtitle sub image/vnd.dwg dwg image/vnd.dxf dxf image/vnd.fastbidsheet fbs @@ -1344,6 +1577,7 @@ image/vnd.fujixerox.edmics-rlc rlc # image/vnd.globalgraphics.pgb # image/vnd.microsoft.icon # image/vnd.mix +# image/vnd.mozilla.apng image/vnd.ms-modi mdi image/vnd.ms-photo wdp image/vnd.net-fpx npx @@ -1352,9 +1586,13 @@ image/vnd.net-fpx npx # image/vnd.sealedmedia.softseal.gif # image/vnd.sealedmedia.softseal.jpg # image/vnd.svf +# image/vnd.tencent.tap +# image/vnd.valve.source.texture image/vnd.wap.wbmp wbmp image/vnd.xiff xif +# image/vnd.zbrush.pcx image/webp webp +# image/wmf image/x-3ds 3ds image/x-cmu-raster ras image/x-cmx cmx @@ -1392,7 +1630,9 @@ message/rfc822 eml mime # message/sipfrag # message/tracking-status # message/vnd.si.simp +# message/vnd.wfa.wsc # model/example +# model/gltf+json model/iges igs iges model/mesh msh mesh silo model/vnd.collada+xml dae @@ -1404,13 +1644,18 @@ model/vnd.gdl gdl model/vnd.gtw gtw # model/vnd.moml+xml model/vnd.mts mts +# model/vnd.opengex # model/vnd.parasolid.transmit.binary # model/vnd.parasolid.transmit.text +# model/vnd.rosette.annotated-data-model +# model/vnd.valve.source.compiled-map model/vnd.vtu vtu model/vrml wrl vrml model/x3d+binary x3db x3dbz +# model/x3d+fastinfoset model/x3d+vrml x3dv x3dvz model/x3d+xml x3d x3dz +# model/x3d-vrml # multipart/alternative # multipart/appledouble # multipart/byteranges @@ -1425,30 +1670,41 @@ model/x3d+xml x3d x3dz # multipart/report # multipart/signed # multipart/voice-message +# multipart/x-mixed-replace # text/1d-interleaved-parityfec text/cache-manifest appcache text/calendar ics ifb text/css css text/csv csv +# text/csv-schema # text/directory # text/dns # text/ecmascript +# text/encaprtp # text/enriched # text/example # text/fwdred +# text/grammar-ref-list text/html html htm -# text/javascript +text/javascript js mjs +# text/jcr-cnd +# text/markdown +# text/mizar text/n3 n3 +# text/parameters # text/parityfec text/plain txt text conf def list log in +# text/provenance-notation # text/prs.fallenstein.rst text/prs.lines.tag dsc -# text/vnd.radisys.msml-basic-layout +# text/prs.prop.logic +# text/raptorfec # text/red # text/rfc822-headers text/richtext rtx # text/rtf # text/rtp-enc-aescm128 +# text/rtploopback # text/rtx text/sgml sgml sgm # text/t140 @@ -1458,11 +1714,13 @@ text/turtle ttl # text/ulpfec text/uri-list uri uris urls text/vcard vcard +# text/vnd.a # text/vnd.abc text/vnd.curl curl text/vnd.curl.dcurl dcurl -text/vnd.curl.scurl scurl text/vnd.curl.mcurl mcurl +text/vnd.curl.scurl scurl +# text/vnd.debian.copyright # text/vnd.dmclientscript text/vnd.dvb.subtitle sub # text/vnd.esmertec.theme-descriptor @@ -1477,6 +1735,7 @@ text/vnd.in3d.spot spot # text/vnd.motorola.reflex # text/vnd.ms-mediapackage # text/vnd.net2phone.commcenter.command +# text/vnd.radisys.msml-basic-layout # text/vnd.si.uricatalogue text/vnd.sun.j2me.app-descriptor jad # text/vnd.trolltech.linguist @@ -1488,9 +1747,9 @@ text/x-asm s asm text/x-c c cc cxx cpp h hh dic text/x-fortran f for f77 f90 text/x-java-source java +text/x-nfo nfo text/x-opml opml text/x-pascal p pas -text/x-nfo nfo text/x-setext etx text/x-sfv sfv text/x-uuencode uu @@ -1506,6 +1765,7 @@ video/3gpp2 3g2 # video/bt656 # video/celb # video/dv +# video/encaprtp # video/example video/h261 h261 video/h263 h263 @@ -1514,6 +1774,8 @@ video/h263 h263 video/h264 h264 # video/h264-rcdo # video/h264-svc +# video/h265 +# video/iso.segment video/jpeg jpgv # video/jpeg2000 video/jpm jpm jpgm @@ -1531,8 +1793,10 @@ video/ogg ogv # video/parityfec # video/pointer video/quicktime qt mov +# video/raptorfec # video/raw # video/rtp-enc-aescm128 +# video/rtploopback # video/rtx # video/smpte292m # video/ulpfec @@ -1563,12 +1827,15 @@ video/vnd.ms-playready.media.pyv pyv # video/vnd.nokia.interleaved-multimedia # video/vnd.nokia.videovoip # video/vnd.objectvideo +# video/vnd.radgamettools.bink +# video/vnd.radgamettools.smacker # video/vnd.sealed.mpeg1 # video/vnd.sealed.mpeg4 # video/vnd.sealed.swf # video/vnd.sealedmedia.softseal.mov video/vnd.uvvu.mp4 uvu uvvu video/vnd.vivo viv +# video/vp8 video/webm webm video/x-f4v f4v video/x-fli fli diff --git a/src/bin/tools/mimegen.c b/src/bin/tools/mimegen.c new file mode 100644 index 000000000..264eb6847 --- /dev/null +++ b/src/bin/tools/mimegen.c @@ -0,0 +1,372 @@ +/* + * lwan - web server + * Copyright (c) 2016 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(LWAN_HAVE_BROTLI) +#include +#elif defined(LWAN_HAVE_ZSTD) +#include +#elif defined(LWAN_HAVE_ZOPFLI) +#include +#else +#include +#endif + +#include "../../lib/hash.h" + +struct output { + char *ptr; + size_t used, capacity; +}; + +static int +output_append_full(struct output *output, const char *str, size_t str_len) +{ + size_t total_size = output->used + str_len; + + if (total_size >= output->capacity) { + char *tmp; + + while (total_size >= output->capacity) + output->capacity *= 2; + + tmp = realloc(output->ptr, output->capacity); + if (!tmp) + return -errno; + + output->ptr = tmp; + } + + memcpy(output->ptr + output->used, str, str_len); + output->used = total_size; + + return 0; +} + +static int output_append_u64(struct output *output, uint64_t value) +{ + return output_append_full(output, (char *)&value, 8); +} + +static int output_append(struct output *output, const char *str) +{ + return output_append_full(output, str, strlen(str) + 1); +} + +static int compare_ext(const void *a, const void *b) +{ + const char **exta = (const char **)a; + const char **extb = (const char **)b; + + return strcasecmp(*exta, *extb); +} + +static char *strend(char *str, char ch) +{ + str = strchr(str, ch); + if (str) { + *str = '\0'; + return str + 1; + } + return NULL; +} + +static char *compress_output(const struct output *output, size_t *outlen) +{ + char *compressed; + +#if defined(LWAN_HAVE_BROTLI) + *outlen = BrotliEncoderMaxCompressedSize(output->used); + + compressed = malloc(*outlen); + if (!compressed) { + fprintf(stderr, "Could not allocate memory for compressed data\n"); + exit(1); + } + + if (BrotliEncoderCompress(BROTLI_MAX_QUALITY, BROTLI_MAX_WINDOW_BITS, + BROTLI_MODE_TEXT, output->used, + (const unsigned char *)output->ptr, outlen, + (unsigned char *)compressed) != BROTLI_TRUE) { + fprintf(stderr, "Could not compress mime type table with Brotli\n"); + exit(1); + } +#elif defined(LWAN_HAVE_ZSTD) + *outlen = ZSTD_compressBound(output->used); + + compressed = malloc(*outlen); + if (!compressed) { + fprintf(stderr, "Could not allocate memory for compressed data\n"); + exit(1); + } + + *outlen = ZSTD_compress(compressed, *outlen, output->ptr, output->used, + ZSTD_maxCLevel()); + if (ZSTD_isError(*outlen)) { + fprintf(stderr, "Could not compress mime type table with ZSTD\n"); + exit(1); + } +#elif defined(LWAN_HAVE_ZOPFLI) + ZopfliOptions opts; + + *outlen = 0; + + ZopfliInitOptions(&opts); + ZopfliCompress(&opts, ZOPFLI_FORMAT_ZLIB, + (const unsigned char *)output->ptr, output->used, + (unsigned char **)&compressed, outlen); +#else + *outlen = compressBound((uLong)output->used); + compressed = malloc(*outlen); + if (!compressed) { + fprintf(stderr, "Could not allocate memory for compressed data\n"); + exit(1); + } + if (compress2((Bytef *)compressed, outlen, (const Bytef *)output->ptr, + output->used, 9) != Z_OK) { + fprintf(stderr, "Could not compress data with zlib\n"); + exit(1); + } +#endif + if (!*outlen) { + free(compressed); + return NULL; + } + + return compressed; +} + +int main(int argc, char *argv[]) +{ + /* 32k is sufficient for the provided mime.types, but we can reallocate + * if necessary. This is just to avoid unneccessary reallocs. */ + struct output output = { .capacity = 32768 }; + FILE *fp; + char buffer[256]; + size_t compressed_size; + char *compressed, *ext; + struct hash *ext_mime; + struct hash_iter iter; + const char **exts, *key; + size_t i; + + if (argc < 2) { + fprintf(stderr, "Usage: %s /path/to/mime.types\n", argv[0]); + return 1; + } + + fp = fopen(argv[1], "re"); + if (!fp) { + fprintf(stderr, "Could not open %s: %s\n", argv[1], strerror(errno)); + return 1; + } + + ext_mime = hash_str_new(free, free); + if (!ext_mime) { + fprintf(stderr, "Could not allocate hash table\n"); + fclose(fp); + return 1; + } + + while (fgets(buffer, sizeof(buffer), fp)) { + char *start = buffer, *end, *tab, *mime_type; + + while (*start && isspace(*start)) /* Strip spaces at the start. */ + start++; + if (*start == '#') /* Ignore commented-out lines. */ + continue; + + strend(start, '\n'); /* Strip line endings. */ + strend(start, '#'); /* Strip comments from the middle. */ + tab = strend(start, '\t'); + if (!tab) /* Find mime-type/extension separator. */ + continue; + + mime_type = start; + /* "application/octet-stream" is the fallback, so no need to store + * it in the table. It's just one line, though, so maybe not really + * necessary? */ + if (streq(mime_type, "application/octet-stream")) + continue; + + while (*tab && *tab == '\t') /* Find first extension. */ + tab++; + + for (ext = tab; *ext; ext += end - ext + 1) { + char *k, *v; + int r; + + end = strchr(ext, ' '); /* Stop at next extension. */ + if (!end) + end = strchr(ext, '\0'); /* If not found, find last extension. */ + *end = '\0'; + + /* Check if we have empty extensions. Shouldn't happen with the provided + * mime.types file, but check on debug builds if this ever happens. */ + assert(end != ext); + + if (end - ext > 8) { + /* Truncate extensions over 8 characters. See commit 2050759297. */ + ext[8] = '\0'; + } + + k = strdup(ext); + v = strdup(mime_type); + + if (!k || !v) { + fprintf(stderr, "Could not allocate memory\n"); + fclose(fp); + return 1; + } + + r = hash_add_unique(ext_mime, k, v); + if (r < 0) { + free(k); + free(v); + + if (r != -EEXIST) { + fprintf(stderr, "Could not add extension to hash table\n"); + fclose(fp); + return 1; + } + } + } + } + + { + char *k = strdup("bin"); + char *v = strdup("application/octet-stream"); + if (!k || !v) { + fprintf(stderr, "Could not allocate memory\n"); + fclose(fp); + return 1; + } + int r = hash_add_unique(ext_mime, k, v); + if (r != 0 && r != -EEXIST) { + fprintf(stderr, "Could not add fallback mime entry\n"); + fclose(fp); + return 1; + } + } + + /* Get sorted list of extensions. */ + exts = calloc(hash_get_count(ext_mime), sizeof(char *)); + if (!exts) { + fprintf(stderr, "Could not allocate extension array\n"); + fclose(fp); + return 1; + } + hash_iter_init(ext_mime, &iter); + for (i = 0; hash_iter_next(&iter, (const void **)&key, NULL); i++) + exts[i] = key; + qsort(exts, hash_get_count(ext_mime), sizeof(char *), compare_ext); + + /* Generate uncompressed blob. */ + output.ptr = malloc(output.capacity); + if (!output.ptr) { + fprintf(stderr, "Could not allocate temporary memory\n"); + fclose(fp); + return 1; + } + ssize_t bin_index = -1; + for (i = 0; i < hash_get_count(ext_mime); i++) { + uint64_t ext_lower = 0; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-truncation" + /* See lwan_determine_mime_type_for_file_name() in lwan-tables.c */ + strncpy((char *)&ext_lower, exts[i], 8); +#pragma GCC diagnostic pop + + ext_lower &= ~0x2020202020202020ull; + ext_lower = htobe64(ext_lower); + + if (output_append_u64(&output, ext_lower) < 0) { + fprintf(stderr, "Could not append to output\n"); + fclose(fp); + return 1; + } + + if (bin_index < 0 && streq(exts[i], "bin")) + bin_index = (ssize_t)i; + } + for (i = 0; i < hash_get_count(ext_mime); i++) { + if (output_append(&output, hash_find(ext_mime, exts[i])) < 0) { + fprintf(stderr, "Could not append to output\n"); + fclose(fp); + return 1; + } + } + + if (bin_index < 0) { + fprintf(stderr, "Could not find fallback item after sorting!\n"); + fclose(fp); + return 1; + } + + /* Compress blob. */ + compressed = compress_output(&output, &compressed_size); + if (!compressed) { + fprintf(stderr, "Could not compress data\n"); + fclose(fp); + return 1; + } + + /* Print output. */ +#if defined(LWAN_HAVE_BROTLI) + printf("/* Compressed with brotli */\n"); +#elif defined(LWAN_HAVE_ZSTD) + printf("/* Compressed with zstd */\n"); +#elif defined(LWAN_HAVE_ZOPFLI) + printf("/* Compressed with zopfli (deflate) */\n"); +#else + printf("/* Compressed with zlib (deflate) */\n"); +#endif + + unsigned int entries_floor = 1u << (31 - __builtin_clz(hash_get_count(ext_mime))); + + printf("#pragma once\n"); + printf("#define MIME_UNCOMPRESSED_LEN %zu\n", output.used); + printf("#define MIME_COMPRESSED_LEN %lu\n", compressed_size); + printf("#define MIME_ENTRIES %d\n", hash_get_count(ext_mime)); + printf("#define MIME_ENTRIES_FLOOR %d\n", entries_floor); + printf("#define MIME_ENTRY_FALLBACK %ld\n", bin_index); + printf("#define MIME_EXT_FALLBACK \".%s\"\n", exts[bin_index]); + printf("static const unsigned char mime_entries_compressed[] = {\n"); + for (i = 1; compressed_size; compressed_size--, i++) + printf("0x%02x,%c", compressed[i - 1] & 0xff, " \n"[i % 13 == 0]); + printf("};\n"); + + free(compressed); + free(output.ptr); + free(exts); + hash_unref(ext_mime); + fclose(fp); + + return 0; +} diff --git a/src/bin/tools/statuslookupgen.c b/src/bin/tools/statuslookupgen.c new file mode 100644 index 000000000..37070a747 --- /dev/null +++ b/src/bin/tools/statuslookupgen.c @@ -0,0 +1,115 @@ +/* Naïve brute-force perfect hash table generator for HTTP status lookup */ + +#include +#include +#include +#include +#include + +#include "../../lib/lwan-http-status.h" + +static inline uint32_t rotate(uint32_t v, int n) +{ + return v << (32 - n) | v >> n; +} + +static inline uint32_t map_0_to_n(uint32_t value, uint32_t n) +{ + return (uint32_t)(((uint64_t)value * (uint64_t)n) >> 32); +} + +int main(void) +{ + uint32_t max_key = 0; + int min_key = INT_MAX; +#define COMPARE_MAX(ignore1, key, ignore2, ignore3) \ + do { \ + if (key > max_key) \ + max_key = key; \ + if (key < min_key) \ + min_key = key; \ + } while (0); + FOR_EACH_HTTP_STATUS(COMPARE_MAX) +#undef COMPARE_MAX + +#define SELECT_KEY(ignore1, key, ignore2, ignore3) key, + const int keys[] = {FOR_EACH_HTTP_STATUS(SELECT_KEY)}; +#undef SELECT_KEY + +#define N_KEYS ((int)(sizeof(keys) / sizeof(keys[0]))) + + int best_rot = INT_MAX; + uint32_t best_mod = 64; + uint32_t best_subtract = UINT_MAX; + + if (N_KEYS >= best_mod) { + fprintf(stderr, "table too large!\n"); + return 1; + } + + for (uint32_t subtract = 0; subtract < max_key; subtract++) { + for (int rot = 0; rot < 32; rot++) { + for (uint32_t mod = N_KEYS; mod < best_mod; mod++) { + uint64_t set = 0; + int set_bits = 0; + + for (int key = 0; key < N_KEYS; key++) { + uint32_t k = map_0_to_n( + rotate((uint32_t)keys[key] - subtract, rot), mod); + + if (set & 1ull<> %d);\n", 32 - best_rot, best_rot); + printf(" const char *ret = table[(uint32_t)(((uint64_t)hash * (uint64_t)%d) >> 32)];\n", best_mod); + printf(" assert((uint32_t)(ret[2] - '0') == ((uint32_t)status %% 10));\n"); + printf(" assert((uint32_t)(ret[1] - '0') == ((uint32_t)(status / 10) %% 10));\n"); + printf(" assert((uint32_t)(ret[0] - '0') == ((uint32_t)(status / 100) %% 10));\n"); + printf(" return ret;\n"); + + printf("}\n"); +} diff --git a/src/bin/tools/weighttp.c b/src/bin/tools/weighttp.c new file mode 100644 index 000000000..374171566 --- /dev/null +++ b/src/bin/tools/weighttp.c @@ -0,0 +1,2003 @@ +/* + * weighttp - a lightweight and simple webserver benchmarking tool + * + * Copyright (c) 2016, Glue Logic LLC. All rights reserved. code()gluelogic.com + * + * This rewrite is based on weighttp by Thomas Porzelt + * Copyright (c) 2009-2011 Thomas Porzelt + * git://git.lighttpd.net/weighttp + * https://github.com/lighttpd/weighttp/ + * + * License: + * MIT, see COPYING.weighttp file + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif + +#pragma GCC diagnostic ignored "-Wsign-conversion" +#pragma GCC diagnostic ignored "-Wconversion" +#pragma GCC diagnostic ignored "-Wvla" + +#include +#include /* socket() connect() SOCK_NONBLOCK sockaddr_storage */ +#include /* fstat() */ +#include /* gettimeofday() */ +#include /* errno EINTR EAGAIN EWOULDBLOCK EINPROGRESS EALREADY */ +#include /* open() fcntl() pipe2() F_SETFL (O_* flags) */ +#include /* PRIu64 PRId64 */ +#include /* USHRT_MAX */ +#include /* setlocale() */ +#include /* getaddrinfo() freeaddrinfo() */ +#include /* poll() POLLIN POLLOUT POLLERR POLLHUP */ +#include /* pthread_create() pthread_join() */ +#include /* va_start() va_end() vfprintf() */ +#include +#include /* calloc() free() exit() strtoul() strtoull() */ +#include /* UINT32_MAX */ +#include /* signal() */ +#include +#include /* strcasecmp() strncasecmp() */ +#include /* read() write() close() getopt() optarg optind optopt*/ + +#include +#include +#include +#include + +#ifndef MSG_FASTOPEN +#define MSG_FASTOPEN 0 +#endif +#ifndef MSG_DONTWAIT +#define MSG_DONTWAIT 0 +#endif +#ifndef MSG_NOSIGNAL +#define MSG_NOSIGNAL 0 +#endif +#ifndef SOCK_NONBLOCK +#define SOCK_NONBLOCK 0 +#endif + +#ifndef PACKAGE_VERSION +#define PACKAGE_VERSION "" +#endif + + +/*(oversimplified; these attributes are supported by some other compilers)*/ +#if defined(__GNUC__) || defined(__clang__) +#ifndef __attribute_cold__ +#define __attribute_cold__ __attribute__((__cold__)) +#endif +#ifndef __attribute_hot__ +#define __attribute_hot__ __attribute__((__hot__)) +#endif +#ifndef __attribute_noinline__ +#define __attribute_noinline__ __attribute__((__noinline__)) +#endif +#ifndef __attribute_noreturn__ +#define __attribute_noreturn__ __attribute__((__noreturn__)) +#endif +#ifndef __attribute_pure__ +#define __attribute_pure__ __attribute__((__pure__)) +#endif +#ifndef __attribute_format__ +#define __attribute_format__(x) __attribute__((__format__ x)) +#endif +#else +#ifndef __builtin_expect +#define __builtin_expect(x, y) (x) +#endif +#ifndef __attribute_cold__ +#define __attribute_cold__ +#endif +#ifndef __attribute_hot__ +#define __attribute_hot__ +#endif +#ifndef __attribute_noinline__ +#define __attribute_noinline__ +#endif +#ifndef __attribute_noreturn__ +#define __attribute_noreturn__ +#endif +#ifndef __attribute_pure__ +#define __attribute_pure__ +#endif +#ifndef __attribute_format__ +#define __attribute_format__(x) +#endif +#endif + + +__attribute_cold__ +__attribute_noinline__ +static void +show_version (void) +{ + puts("\nweighttp " PACKAGE_VERSION + " - a lightweight and simple webserver benchmarking tool\n"); +} + + +__attribute_cold__ +__attribute_noinline__ +static void +show_help (void) +{ + puts( + "weighttp \n" + " -n num number of requests (mandatory)\n" + " -t num thread count (default: 1)\n" + " -c num concurrent clients (default: 1)\n" + " -k keep alive (default: no)\n" + " -K num num pipelined requests (default: 1)\n" + " -6 use ipv6 (default: no)\n" + " -i use HTTP HEAD method (default: GET)\n" + " -m method use custom HTTP method (default: GET)\n" + " -H str add header to request (\"label: value\"); repeatable\n" + " -b size socket buffer sizes (SO_SNDBUF, SO_RCVBUF)\n" + " -B addr local address to bind to when making outgoing connections\n" + " -C cookie add cookie to request (\"cookie-name=value\"); repeatable\n" + " -F use TCP Fast Open (RFC 7413)\n" + " -T type Content-Type header to use for POST/PUT data,\n" + " e.g. application/x-www-form-urlencoded\n" + " (default: text/plain)\n" + " -A string add Basic WWW Authorization (str is username:password)\n" + " -P string add Basic Proxy-Authorization (str is username:password)\n" + " -X proxy proxy:port or unix domain socket path beginning w/ '/'\n" + " -p file make HTTP POST request using file contents for body\n" + " -u file make HTTP PUT request using file contents for body\n" + " -d (ignored; compatibility with Apache Bench (ab))\n" + " -l (ignored; compatibility with Apache Bench (ab))\n" + " -r (ignored; compatibility with Apache Bench (ab))\n" + " -q quiet: do not show version header or progress\n" + " -h show help and exit\n" + " -V show version and exit\n\n" + "example: \n" + " weighttpd -n 500000 -c 100 -t 2 -K 64 http://localhost/index.html\n"); +} + +/* Notes regarding pipelining + * Enabling pipelining (-p x where x > 1) results in extra requests being sent + * beyond the precise number requested on the command line. Subsequently, + * extra bytes might be read and reported in stats at the end of the test run. + * Additionally, the extra requests are dropped once the req_todo amount is + * reached, and so the target web server(s) might report errors that client + * dropped connection (client disconnect) for those final requests. + * + * The benefits of pipelining include reduced latency between request/response, + * as well as potentially fewer socket read()s for data if multiple requests or + * multiple responses are available to be read by server or client, respectively + */ + +#define CLIENT_BUFFER_SIZE 32 * 1024 + + +struct Stats; +typedef struct Stats Stats; +struct Client; +typedef struct Client Client; +struct Worker; +typedef struct Worker Worker; +struct Config; +typedef struct Config Config; +struct Worker_Config; +typedef struct Worker_Config Worker_Config; + + +struct Stats { + uint64_t req_todo; /* total num of requests to do */ + uint64_t req_started; /* total num of requests started */ + uint64_t req_done; /* total num of requests done */ + uint64_t req_success; /* total num of successful requests */ + uint64_t req_failed; /* total num of failed requests */ + uint64_t req_error; /* total num of errored requests */ + uint64_t bytes_total; /* total num of bytes received (headers+body) */ + uint64_t bytes_headers; /* total num of bytes received (headers) */ + uint64_t req_2xx; + uint64_t req_3xx; + uint64_t req_4xx; + uint64_t req_5xx; +}; + +struct Client { + int revents; + enum { + PARSER_CONNECT, + PARSER_START, + PARSER_HEADER, + PARSER_BODY + } parser_state; + + uint32_t buffer_offset; /* pos in buffer (size of data in buffer) */ + uint32_t parser_offset; /* pos in parsing (behind buffer_offset) */ + uint32_t request_offset; /* pos in sending request */ + int chunked; + int64_t content_length; + int64_t chunk_size; + int64_t chunk_received; + int http_status_success; + int config_keepalive; + int keepalive; + int keptalive; + int pipelined; + int pipeline_max; + int tcp_fastopen; + int http_head; + int so_bufsz; + + uint32_t request_size; + const char *request; + struct pollfd *pfd; + Stats *stats; + const struct addrinfo *raddr; + const struct addrinfo *laddr; + char buffer[CLIENT_BUFFER_SIZE]; +}; + +struct Worker { + struct pollfd *pfds; + Client *clients; + Stats stats; + struct addrinfo raddr; + struct addrinfo laddr; + struct sockaddr_storage raddr_storage; + struct sockaddr_storage laddr_storage; +}; + +struct Worker_Config { + const Config *config; + int id; + int num_clients; + uint64_t num_requests; + Stats stats; + /* pad struct Worker_Config for cache line separation between threads. + * Round up to 256 to avoid chance of false sharing between threads. + * Alternatively, could memalign the allocation of struct Worker_Config + * list to cache line size (e.g. 128 bytes) */ + uint64_t padding[(256 - (1*sizeof(void *)) + - (2*sizeof(int)) + - (1*sizeof(uint64_t)) + - sizeof(Stats)) + / sizeof(uint64_t)]; +}; + +struct Config { + Worker_Config *wconfs; + char *proxy; + struct timeval ts_start; + struct timeval ts_end; + + uint64_t req_count; + int thread_count; + int keep_alive; + int concur_count; + int pipeline_max; + int tcp_fastopen; + int http_head; + int so_bufsz; + + int quiet; + uint32_t request_size; + char *request; + char buf[16384]; /*(used for simple 8k memaligned request buffer on stack)*/ + struct addrinfo raddr; + struct addrinfo laddr; + struct sockaddr_storage raddr_storage; + struct sockaddr_storage laddr_storage; + struct laddrs { + struct addrinfo **addrs; + int num; + } laddrs; +}; + + +__attribute_cold__ +static void +client_init (Worker * const restrict worker, + const Config * const restrict config, + const int i) +{ + Client * const restrict client = worker->clients+i; + client->pfd = worker->pfds+i; + client->pfd->fd = -1; + client->parser_state = PARSER_CONNECT; + + client->stats = &worker->stats; + client->raddr = &worker->raddr; + client->laddr = config->laddrs.num > 0 + ? config->laddrs.addrs[(i % config->laddrs.num)] + : (0 != worker->laddr.ai_addrlen) ? &worker->laddr : NULL; + client->config_keepalive = config->keep_alive; + client->pipeline_max = config->pipeline_max; + client->tcp_fastopen = config->tcp_fastopen; + client->http_head = config->http_head; + client->so_bufsz = config->so_bufsz; + client->request_size = config->request_size; + client->request = config->request; + /* future: might copy config->request to new allocation in Worker + * so that all memory accesses during benchmark execution are to + * independent, per-thread allocations */ +} + + +__attribute_cold__ +static void +client_delete (const Client * const restrict client) +{ + if (-1 != client->pfd->fd) + close(client->pfd->fd); +} + + +__attribute_cold__ +__attribute_noinline__ +static void +worker_init (Worker * const restrict worker, + Worker_Config * const restrict wconf) +{ + const Config * const restrict config = wconf->config; + memset(worker, 0, sizeof(Worker)); + memcpy(&worker->laddr, &config->laddr, sizeof(config->laddr)); + memcpy(&worker->raddr, &config->raddr, sizeof(config->raddr)); + if (config->laddr.ai_addrlen) + worker->laddr.ai_addr = (struct sockaddr *) + memcpy(&worker->laddr_storage, + &config->laddr_storage, config->laddr.ai_addrlen); + worker->raddr.ai_addr = (struct sockaddr *) + memcpy(&worker->raddr_storage, + &config->raddr_storage, config->raddr.ai_addrlen); + const int num_clients = wconf->num_clients; + worker->stats.req_todo = wconf->num_requests; + worker->pfds = (struct pollfd *)calloc(num_clients, sizeof(struct pollfd)); + worker->clients = (Client *)calloc(num_clients, sizeof(Client)); + for (int i = 0; i < num_clients; ++i) + client_init(worker, wconf->config, i); +} + + +__attribute_cold__ +__attribute_noinline__ +static void +worker_delete (Worker * const restrict worker, + Worker_Config * const restrict wconf) +{ + int i; + const int num_clients = wconf->num_clients; + + /* adjust bytes_total to discard count of excess responses + * (> worker->stats.req_todo) */ + if (worker->clients[0].pipeline_max > 1) { + for (i = 0; i < num_clients; ++i) { + worker->stats.bytes_total -= ( worker->clients[i].buffer_offset + - worker->clients[i].parser_offset ); + } + } + + memcpy(&wconf->stats, &worker->stats, sizeof(Stats)); + for (i = 0; i < num_clients; ++i) + client_delete(worker->clients+i); + free(worker->clients); + free(worker->pfds); +} + + +__attribute_cold__ +__attribute_noinline__ +static void +wconfs_init (Config * const restrict config) +{ + /* create Worker_Config data structures for each (future) thread */ + Worker_Config * const restrict wconfs = + (Worker_Config *)calloc(config->thread_count, sizeof(Worker_Config)); + + uint32_t rest_concur = config->concur_count % config->thread_count; + uint32_t rest_req = config->req_count % config->thread_count; + + for (int i = 0; i < config->thread_count; ++i) { + uint64_t reqs = config->req_count / config->thread_count; + int concur = config->concur_count / config->thread_count; + + if (rest_concur) { + concur += 1; + rest_concur -= 1; + } + + if (rest_req) { + reqs += 1; + rest_req -= 1; + } + + if (!config->quiet) + printf("spawning thread #%d: %d concurrent requests, " + "%"PRIu64" total requests\n", i+1, concur, reqs); + + wconfs[i].config = config; + wconfs[i].id = i; + wconfs[i].num_clients = concur; + wconfs[i].num_requests = reqs; + } + + config->wconfs = wconfs; +} + + +__attribute_cold__ +__attribute_noinline__ +static void +wconfs_delete (const Config * const restrict config) +{ + free(config->wconfs); + if (config->request < config->buf + || config->buf+sizeof(config->buf) <= config->request) + free(config->request); + + if (config->laddrs.num > 0) { + for (int i = 0; i < config->laddrs.num; ++i) + freeaddrinfo(config->laddrs.addrs[i]); + free(config->laddrs.addrs); + } +} + + +__attribute_hot__ +static void +client_reset (Client * const restrict client, const int success) +{ + /* update worker stats */ + Stats * const restrict stats = client->stats; + + ++stats->req_done; + if (__builtin_expect( (0 != success), 1)) + ++stats->req_success; + else + ++stats->req_failed; + + client->revents = (stats->req_started < stats->req_todo) ? POLLOUT : 0; + if (client->revents && client->keepalive) { + /*(assumes writable; will find out soon if not and register interest)*/ + ++stats->req_started; + client->parser_state = PARSER_START; + client->keptalive = 1; + if (client->parser_offset == client->buffer_offset) { + client->parser_offset = 0; + client->buffer_offset = 0; + } + #if 0 + else if (client->parser_offset > (CLIENT_BUFFER_SIZE/2)) { + memmove(client->buffer, client->buffer+client->parser_offset, + client->buffer_offset - client->parser_offset + 1); + client->buffer_offset -= client->parser_offset; + client->parser_offset = 0; + } + /* future: if we tracked size of headers for first successful response, + * we might use that size to determine whether or not to memmove() + * any remaining contents in client->buffer to the beginning of buffer, + * e.g. if parser_offset + expected_response_len exceeds buffer size + * On the size, if we expect to already have completed response fully + * received in buffer, then skip the memmove(). */ + #endif + if (--client->pipelined && client->buffer_offset) + client->revents |= POLLIN; + } + else { + close(client->pfd->fd); + client->pfd->fd = -1; + client->pfd->events = 0; + /*client->pfd->revents = 0;*/ + client->parser_state = PARSER_CONNECT; + } +} + + +__attribute_cold__ +__attribute_noinline__ +static void +client_error (Client * const restrict client) +{ + ++client->stats->req_error; + if (client->parser_state != PARSER_BODY) { + /*(might include subsequent responses to pipelined requests, but + * some sort of invalid response received if client_error() called)*/ + client->stats->bytes_headers += + (client->buffer_offset - client->parser_offset); + client->buffer_offset = 0; + client->parser_offset = 0; + } + client->keepalive = 0; + client_reset(client, 0); +} + + +__attribute_cold__ +__attribute_noinline__ +static void +client_perror (Client * const restrict client, const char * const restrict tag) +{ + const int errnum = errno; + client->buffer[0] = '\0'; + #if defined(_GNU_SOURCE) && defined(__GLIBC__) + const char * const errstr = + strerror_r(errnum, client->buffer, sizeof(client->buffer)); + #else /* XSI-compliant strerror_r() */ + const char * const errstr = client->buffer; + strerror_r(errnum, client->buffer, sizeof(client->buffer)); + #endif + fprintf(stderr, "error: %s failed: (%d) %s\n", tag, errnum, errstr); + client_error(client); +} + + + +static void +client_connected (Client * const restrict client) +{ + client->request_offset = 0; + client->buffer_offset = 0; + client->parser_offset = 0; + client->parser_state = PARSER_START; + client->pipelined = 0; + client->keepalive = client->config_keepalive; + client->keptalive = 0; + /*client->success = 0;*/ +} + + +__attribute_noinline__ +static int +client_connect (Client * const restrict client) +{ + const struct addrinfo * const restrict raddr = client->raddr; + int fd = client->pfd->fd; + int opt; + + if (-1 == fd) { + ++client->stats->req_started; + + do { + fd = socket(raddr->ai_family,raddr->ai_socktype,raddr->ai_protocol); + } while (__builtin_expect( (-1 == fd), 0) && errno == EINTR); + + if (fd >= 0) { + #if !SOCK_NONBLOCK + fcntl(fd, F_SETFL, O_NONBLOCK | O_RDWR); /* set non-blocking */ + #endif + client->pfd->fd = fd; + } + else { + client_perror(client, "socket()"); + return 0; + } + + if (1 == client->pipeline_max && raddr->ai_family != AF_UNIX) { + /* disable Nagle if not pipelining requests and not AF_UNIX + * (pipelining enables keepalive, but if not pipelining but + * keepalive enabled, still want to disable Nagle to reduce latency + * when sending next keepalive request after receiving response) */ + opt = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); + } + + if (0 != client->so_bufsz) { + opt = client->so_bufsz; + if (0 != setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(opt))) + client_perror(client, "setsockopt() SO_SNDBUF"); + if (0 != setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(opt))) + client_perror(client, "setsockopt() SO_RCVBUF"); + } + + if (raddr->ai_family != AF_UNIX) { + /*(might not be correct for real clients, but ok for load test)*/ + struct linger l = { .l_onoff = 1, .l_linger = 0 }; + if (0 != setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof(l))) + client_perror(client, "setsockopt() SO_LINGER"); + } + + if (NULL != client->laddr) { + if (0 != bind(fd,client->laddr->ai_addr,client->laddr->ai_addrlen)){ + client_perror(client, "bind() (local addr)"); + return 0; + } + } + + int rc; + #ifdef TCP_FASTOPEN + ssize_t wr = 0; + if (client->tcp_fastopen) {/*(disabled if config->proxy is AF_UNIX)*/ + wr = sendto(fd, client->request, client->request_size, + MSG_FASTOPEN | MSG_DONTWAIT | MSG_NOSIGNAL, + raddr->ai_addr, raddr->ai_addrlen); + if (wr > 0) { + client_connected(client); + if (client->request_size == (uint32_t)wr) { + client->pfd->events |= POLLIN; + if (++client->pipelined == client->pipeline_max) { + client->revents &= ~POLLOUT; + client->pfd->events &= ~POLLOUT; + } + } + else + client->request_offset = (uint32_t)wr; + return 1; + } + else if (-1 == wr && errno == EOPNOTSUPP) + wr = 0; + else { + /*(0 == wr with sendto() should not happen + * with MSG_FASTOPEN and non-zero request_size)*/ + wr = -1; + rc = -1; + } + } + if (0 == wr) + #endif + do { + rc = connect(fd, raddr->ai_addr, raddr->ai_addrlen); + } while (__builtin_expect( (-1 == rc), 0) && errno == EINTR); + + if (0 != rc) { + switch (errno) { + case EINPROGRESS: + case EALREADY: + /* async connect now in progress */ + client->revents &= ~POLLOUT; + client->pfd->events |= POLLOUT; + return 0; + default: + client_perror(client, "connect()"); + return 0; + } + } + } + else { + opt = 0; + socklen_t optlen = sizeof(opt); + if (0 != getsockopt(fd,SOL_SOCKET,SO_ERROR,&opt,&optlen) || 0 != opt) { + if (0 != opt) errno = opt; + client_perror(client, "connect() getsockopt()"); + return 0; /* error connecting */ + } + } + + /* successfully connected */ + client_connected(client); + return 1; +} + + + +static int +client_parse_chunks (Client * const restrict client) +{ + do { + char *str = client->buffer+client->parser_offset; + + if (-1 == client->chunk_size) { + /* read chunk size */ + /*char *end = strchr(str, '\n');*/ + char *end = + memchr(str, '\n', client->buffer_offset - client->parser_offset); + if (!end) /* partial line */ + return 1; + ++end; + + /* assume server sends valid chunked header + * (not validating; (invalid) chunked header without any + * hex digits is treated as 0-chunk, ending input) */ + client->chunk_size = 0; + do { + int c = *str; + client->chunk_size <<= 4; + if (c >= '0' && c <= '9') + client->chunk_size |= (c - '0'); + else if ((c |= 0x20) >= 'a' && c <= 'f') + client->chunk_size |= (c - 'a' + 10); + else { + if (c=='\r' || c=='\n' || c==' ' || c=='\t' || c==';') + break; + client_error(client); + return 0; + } + } while (*++str != '\r' && *str != '\n'); + + if (0 == client->chunk_size) { + /* chunk of size 0 marks end of content body + * check for final "\r\n" ending response + * (not handling trailers if user supplied -H "TE: trailers") */ + if (end + 2 > client->buffer + client->buffer_offset) { + client->chunk_size = -1; + return 1; /* final "\r\n" not yet received */ + } + if (end[0] == '\r' && end[1] == '\n') + client->stats->bytes_headers += 2; + else + client->keepalive = 0; /*(just close con if trailers)*/ + client->parser_offset = end - client->buffer + 2; + client_reset(client, client->http_status_success); + return 0; /*(trigger loop continue in caller)*/ + } + + client->parser_offset = end - client->buffer; + client->chunk_received = 0; + client->chunk_size += 2; /*(for chunk "\r\n" end)*/ + } + + /* consume chunk until chunk_size is reached */ + const int rd = client->buffer_offset - client->parser_offset; + int chunk_remain = client->chunk_size - client->chunk_received; + if (rd >= chunk_remain) { + client->chunk_received += chunk_remain; + client->parser_offset += chunk_remain; + + if (client->buffer[client->parser_offset-1] != '\n') { + client_error(client); + return 0; + } + + /* got whole chunk, next! */ + client->chunk_size = -1; + client->chunk_received = 0; + } + else { + client->chunk_received += rd; + client->parser_offset += rd; + } + + } while (client->parser_offset != client->buffer_offset);/* more to parse */ + + client->parser_offset = 0; + client->buffer_offset = 0; + return 1; +} + + +__attribute_hot__ +__attribute_pure__ +static uint64_t +client_parse_uint64 (const char * const restrict str) +{ + /* quick-n-dirty conversion of numerical string to integral number + * Note: not validating field and not checking for valid number + * (weighttp not intended for use with requests > 2 GB, as transfer + * of body would take the majority of the time in that case)*/ + uint64_t x = 0; + for (int i = 0; (unsigned int)(str[i] - '0') < 10u; ++i) { + x *= 10; + x += (unsigned int)(str[i] - '0'); + } + return x; +} + + +__attribute_hot__ +__attribute_noinline__ +static int +client_parse (Client * const restrict client) +{ + char *end; + uint32_t len; + + /* future: might combine PARSER_START and PARSER_HEADER states by + * collecting entire set of headers (reading until "\r\n\r\n") + * prior to parsing */ + + switch (client->parser_state) { + + case PARSER_START: + /* look for HTTP/1.1 200 OK (though also accept HTTP/1.0 200) + * Note: does not support 1xx intermediate messages */ + /* Note: not validating response line; assume valid */ + /*end = strchr(client->buffer+client->parser_offset, '\n');*/ + end = memchr(client->buffer+client->parser_offset, '\n', + client->buffer_offset - client->parser_offset); + if (NULL != end) { + len = (uint32_t)(end - client->buffer - client->parser_offset + 1); + if (len < sizeof("HTTP/1.1 200\r\n")-1) { + client_error(client); + return 0; + } + } + else /*(partial response line; incomplete)*/ + return 1; + + client->content_length = -1; + client->chunked = 0; + client->http_status_success = 1; + switch (client->buffer[client->parser_offset + sizeof("HTTP/1.1 ")-1] + - '0') { + case 2: + ++client->stats->req_2xx; + break; + case 3: + ++client->stats->req_3xx; + break; + case 4: + client->http_status_success = 0; + ++client->stats->req_4xx; + break; + case 5: + client->http_status_success = 0; + ++client->stats->req_5xx; + break; + default: + /* invalid status code */ + client_error(client); + return 0; + } + client->stats->bytes_headers += len; + client->parser_offset += len; + client->parser_state = PARSER_HEADER; + /* fall through */ + + case PARSER_HEADER: + /* minimally peek at Content-Length, Connection, Transfer-Encoding */ + do { + const char *str = client->buffer+client->parser_offset; + /*end = strchr(str, '\n');*/ + end = + memchr(str, '\n', client->buffer_offset - client->parser_offset); + if (NULL == end) + return 1; + len = (uint32_t)(end - str + 1); + client->stats->bytes_headers += len; + client->parser_offset += len; + + /* minimum lengths for us to check for ':' in the following: + * "Content-Length:0\r\n" + * "Connection:close\r\n" + * "Transfer-Encoding:chunked\r\n"*/ + if (end - str < 17) + continue; + + if (str[14] == ':' + && (0 == memcmp(str, "Content-Length", + sizeof("Content-Length")-1) + || 0 == strncasecmp(str, "Content-Length", + sizeof("Content-Length")-1))) { + str += sizeof("Content-Length:")-1; + if (__builtin_expect( (*str == ' '), 1)) + ++str; + while (__builtin_expect( (*str == ' '), 0) + || __builtin_expect( (*str == '\t'), 0)) + ++str; + client->content_length = client_parse_uint64(str); + } + else if (str[10] == ':' + && (0 == memcmp(str, "Connection", + sizeof("Connection")-1) + || 0 == strncasecmp(str, "Connection", + sizeof("Connection")-1))) { + str += sizeof("Connection:")-1; + if (__builtin_expect( (*str == ' '), 1)) + ++str; + while (__builtin_expect( (*str == ' '), 0) + || __builtin_expect( (*str == '\t'), 0)) + ++str; + if ((*str | 0x20) == 'c') /*(assume "close")*/ + client->keepalive = 0; + } + else if (str[17] == ':' + && (0 == memcmp(str, "Transfer-Encoding", + sizeof("Transfer-Encoding")-1) + || 0 == strncasecmp(str, "Transfer-Encoding", + sizeof("Transfer-Encoding")-1))) { + client->chunked = 1; /*(assume "chunked")*/ + client->chunk_size = -1; + client->chunk_received = 0; + } + + } while (end[1] != '\r' || end[2] != '\n'); + + /* body reached */ + client->stats->bytes_headers += 2; + client->parser_offset += 2; + client->parser_state = PARSER_BODY; + if (client->http_head) + client->content_length = 0; + else if (!client->chunked && -1 == client->content_length) + client->keepalive = 0; + /* fall through */ + + case PARSER_BODY: + /* consume and discard response body */ + + if (client->chunked) + return client_parse_chunks(client); + else { + /* consume all data until content-length reached (or EOF) */ + if (-1 != client->content_length) { + uint32_t rd = client->buffer_offset - client->parser_offset; + if (client->content_length > rd) + client->content_length -= rd; + else { /* full response received */ + client->parser_offset += client->content_length; + client_reset(client, client->http_status_success); + return 0; /*(trigger loop continue in caller)*/ + } + } + + client->buffer_offset = 0; + client->parser_offset = 0; + return 1; + } + + case PARSER_CONNECT: /*(should not happen here)*/ + break; + } + + return 1; +} + + + +static void +client_revents (Client * const restrict client) +{ + while (client->revents & POLLIN) { + /* parse pipelined responses */ + if (client->buffer_offset && !client_parse(client)) + continue; + + ssize_t r; + do { + r = recv(client->pfd->fd, client->buffer+client->buffer_offset, + sizeof(client->buffer) - client->buffer_offset - 1, + MSG_DONTWAIT); + } while (__builtin_expect( (-1 == r), 0) && errno == EINTR); + if (__builtin_expect( (r > 0), 1)) { + if (r < (ssize_t)(sizeof(client->buffer)-client->buffer_offset-1)) + client->revents &= ~POLLIN; + client->buffer[(client->buffer_offset += (uint32_t)r)] = '\0'; + client->stats->bytes_total += r; + + if (!client_parse(client)) + continue; + + /* PARSER_BODY handling consumes data, so buffer full might happen + * only when parsing response header line or chunked header line. + * If buffer is full, then line is *way* too long. However, if + * client->parser_offset is non-zero, then move data to beginning + * of buffer and attempt to read() more */ + if (__builtin_expect( + (client->buffer_offset == sizeof(client->buffer)-1), 0)) { + if (0 == client->parser_offset) { + client_error(client); /* response header too big */ + break; + } + else { + memmove(client->buffer,client->buffer+client->parser_offset, + client->buffer_offset - client->parser_offset + 1); + client->buffer_offset -= client->parser_offset; + client->parser_offset = 0; + } + } + } + else { + if (-1 == r) { /* error */ + if (errno == EAGAIN + #if EAGAIN != EWOULDBLOCK + || errno == EWOULDBLOCK + #endif + ) { + client->revents &= ~POLLIN; + client->pfd->events |= POLLIN; + break; + } + else + client_perror(client, "read()"); + } + else { /* disconnect; evaluate if end-of-response or error */ + if (client->http_status_success + && client->parser_state == PARSER_BODY + && !client->chunked && -1 == client->content_length) { + client->keepalive = 0; + client_reset(client, 1); + } + else { + if (client->keptalive + && client->parser_state == PARSER_START + && 0 == client->buffer_offset) { + /* (server might still read and discard request, + * but has initiated connection close) + * (decrement counters to redo request, including + * decrementing counters that will be incremented + * by call to client_error() directly below) */ + --client->stats->req_started; + --client->stats->req_failed; + --client->stats->req_error; + --client->stats->req_done; + } + client_error(client); + } + } + } + } + + if (__builtin_expect( (client->revents & (POLLERR|POLLHUP)), 0)) { + client->keepalive = 0; + client_reset(client, 0); + } + + while (client->revents & POLLOUT) { + ssize_t r; + if (client->parser_state == PARSER_CONNECT && !client_connect(client)) + continue; + + do { + r = send(client->pfd->fd, + client->request+client->request_offset, + client->request_size - client->request_offset, + MSG_DONTWAIT | MSG_NOSIGNAL); + } while (__builtin_expect( (-1 == r), 0) && errno == EINTR); + if (__builtin_expect( (r > 0), 1)) { + if (client->request_size == (uint32_t)r + || client->request_size==(client->request_offset+=(uint32_t)r)){ + /* request sent; register read interest for response */ + client->request_offset = 0; + client->pfd->events |= POLLIN; + if (++client->pipelined < client->pipeline_max) + continue; + else { + client->revents &= ~POLLOUT; /*(trigger write() loop exit)*/ + client->pfd->events &= ~POLLOUT; + } + } + else { + client->revents &= ~POLLOUT; /*(trigger write() loop exit)*/ + client->pfd->events |= POLLOUT; + } + } + else { + if (-1 == r) { /* error */ + if (errno == EAGAIN + #if EAGAIN != EWOULDBLOCK + || errno == EWOULDBLOCK + #endif + ) { + client->revents &= ~POLLOUT; + client->pfd->events |= POLLOUT; + break; + } + else + client_perror(client, "write()"); + } + else { /* (0 == r); not expected; not attempting to write 0 bytes */ + client->keepalive = 0; + client_reset(client, 0); + } + } + } +} + + + +static void * +worker_thread (void * const arg) +{ + Worker worker; + int i, nready; + Worker_Config * const restrict wconf = (Worker_Config *)arg; + worker_init(&worker, wconf); + + const int num_clients = wconf->num_clients; + const int progress = + (0==wconf->id && !wconf->config->quiet); /* report only in first thread */ + const uint64_t progress_interval = /* print every 10% done */ + (worker.stats.req_todo > 10) ? worker.stats.req_todo / 10 : 1; + uint64_t progress_next = progress_interval; + + /* start all clients */ + for (i = 0; i < num_clients; ++i) { + if (worker.stats.req_started < worker.stats.req_todo) { + worker.clients[i].revents = POLLOUT; + client_revents(worker.clients+i); + } + } + + while (worker.stats.req_done < worker.stats.req_todo) { + do { /*(infinite wait)*/ + nready = poll(worker.pfds, (nfds_t)num_clients, -1); + } while (__builtin_expect( (-1 == nready), 0) && errno == EINTR); + if (__builtin_expect( (-1 == nready), 0)) { + /*(repurpose client_perror(); use client buffer for strerror_r())*/ + client_perror(worker.clients+0, "poll()"); /* fatal; ENOMEM */ + return NULL; + } + + i = 0; + do { + while (0 == worker.pfds[i].revents) + ++i; + worker.clients[i].revents |= worker.pfds[i].revents; + worker.pfds[i].revents = 0; + client_revents(worker.clients+i); + } while (--nready); + + if (progress) { + /*(assume progress of one thread approximates that of all threads)*/ + /*(RFE: main thread could poll and report progress of all workers)*/ + while (__builtin_expect( worker.stats.req_done >= progress_next,0)){ + printf("progress: %3d%% done\n", (int) + (worker.stats.req_done * 100 / worker.stats.req_todo)); + if (progress_next == worker.stats.req_todo) + break; + progress_next += progress_interval; + if (__builtin_expect( progress_next > worker.stats.req_todo, 0)) + progress_next = worker.stats.req_todo; + } + } + } + + worker_delete(&worker, wconf); + return NULL; +} + + +__attribute_cold__ +__attribute_noinline__ +static void +config_error_diagnostic (const char * const restrict errfmt, + const int perr, va_list ap) +{ + const int errnum = errno; + show_version(); + show_help(); + fflush(stdout); + + fprintf(stderr, "\nerror: "); + vfprintf(stderr, errfmt, ap); + + if (!perr) + fprintf(stderr, "\n\n"); + else { + char buf[1024]; + buf[0] = '\0'; + #if defined(_GNU_SOURCE) && defined(__GLIBC__) + const char * const errstr = strerror_r(errnum, buf, sizeof(buf)); + #else /* XSI-compliant strerror_r() */ + const char * const errstr = buf; + strerror_r(errnum, buf, sizeof(buf)); + #endif + + fprintf(stderr, ": (%d) %s\n\n", errnum, errstr); + } +} + + +__attribute_cold__ +__attribute_format__((__printf__, 1, 2)) +__attribute_noinline__ +__attribute_noreturn__ +static void +config_error (const char * const restrict errfmt, ...) +{ + va_list ap; + va_start(ap, errfmt); + config_error_diagnostic(errfmt, 0, ap); + va_end(ap); + exit(1); +} + + +__attribute_cold__ +__attribute_format__((__printf__, 1, 2)) +__attribute_noinline__ +__attribute_noreturn__ +static void +config_perror (const char * const restrict errfmt, ...) +{ + va_list ap; + va_start(ap, errfmt); + config_error_diagnostic(errfmt, 1, ap); + va_end(ap); + exit(1); +} + + +typedef struct config_params { + const char *method; + const char *uri; + char *laddrstr; + int use_ipv6; + int headers_num; + int cookies_num; + const char *headers[64]; + const char *cookies[64]; + const char *body_content_type; + const char *body_filename; + const char *authorization; + const char *proxy_authorization; +} config_params; + + +__attribute_cold__ +static int +config_laddr (Config * const restrict config, + const char * const restrict laddrstr) +{ + struct addrinfo hints, *res = NULL; + memset(&hints, 0, sizeof(hints)); + /*hints.ai_flags |= AI_NUMERICHOST;*/ + hints.ai_family = config->raddr.ai_family; + hints.ai_socktype = SOCK_STREAM; + + if (0 != getaddrinfo(laddrstr, NULL, &hints, &res) || NULL == res) + return 0; + + config->laddr.ai_family = res->ai_family; + config->laddr.ai_socktype = res->ai_socktype; + config->laddr.ai_protocol = res->ai_protocol; + config->laddr.ai_addrlen = res->ai_addrlen; + config->laddr.ai_addr = (struct sockaddr *) + memcpy(&config->laddr_storage, res->ai_addr, res->ai_addrlen); + + freeaddrinfo(res); + return 1; +} + + +__attribute_cold__ +static int +config_laddrs (Config * const restrict config, + char * const restrict laddrstr) +{ + char *s; + int num = 1; + for (s = laddrstr; NULL != (s = strchr(s, ',')); s = s+1) ++num; + if (1 == num) return config_laddr(config, laddrstr); + + struct addrinfo hints, **res; + memset(&hints, 0, sizeof(hints)); + /*hints.ai_flags |= AI_NUMERICHOST;*/ + hints.ai_family = config->raddr.ai_family; + hints.ai_socktype = SOCK_STREAM; + + config->laddrs.num = num; + config->laddrs.addrs = res = + (struct addrinfo **)calloc((size_t)num, sizeof(struct addrinfo *)); + + s = laddrstr; + for (int i = 0; i < num; ++i, ++res) { + char *e = strchr(s, ','); + if (NULL != e) *e = '\0'; + + *res = NULL; + if (0 != getaddrinfo(s, NULL, &hints, res) || NULL == *res) + return 0; /*(leave laddrstr modified so last addr is one w/ error)*/ + + if (NULL == e) break; + *e = ','; + s = e+1; + } + + return 1; +} + + +__attribute_cold__ +static void +config_raddr (Config * const restrict config, + const char * restrict hostname, uint16_t port, const int use_ipv6) +{ + if (config->proxy && config->proxy[0] == '/') { + #ifndef UNIX_PATH_MAX + #define UNIX_PATH_MAX 108 + #endif + const size_t len = strlen(config->proxy); + if (len >= UNIX_PATH_MAX) + config_error("socket path too long: %s", config->proxy); + + config->raddr.ai_family = AF_UNIX; + config->raddr.ai_socktype = SOCK_STREAM | SOCK_NONBLOCK; + config->raddr.ai_protocol = 0; + /* calculate effective SUN_LEN(); macro not always available)*/ + config->raddr.ai_addrlen = + (socklen_t)((size_t)(((struct sockaddr_un *) 0)->sun_path) + len); + config->raddr.ai_addr = (struct sockaddr *)&config->raddr_storage; + memset(&config->raddr_storage, 0, sizeof(config->raddr_storage)); + config->raddr_storage.ss_family = AF_UNIX; + memcpy(((struct sockaddr_un *)&config->raddr_storage)->sun_path, + config->proxy, len+1); + return; + } + + char host[1024]; /*(host should be < 256 chars)*/ + if (config->proxy) { /* (&& config->proxy[0] != '/') */ + char * const colon = strrchr(config->proxy, ':'); + if (colon) { + char *endptr; + unsigned long i = strtoul(colon+1, &endptr, 10); + if (*endptr == '\0' && 0 != i && i <= USHRT_MAX) + port = (unsigned short)i; + else /*(might mis-parse IPv6 addr which omitted port)*/ + config_error("could not parse -X proxy: %s", config->proxy); + + const size_t len = (size_t)(colon - config->proxy); + if (len >= sizeof(host)) + config_error("proxy host path too long: %s", config->proxy); + memcpy(host, config->proxy, len); + host[len] = '\0'; + hostname = host; + } + else { + hostname = config->proxy; + port = 80; /* default HTTP port */ + } + } + + struct addrinfo hints, *res, *res_first; + memset(&hints, 0, sizeof(hints)); + hints.ai_flags |= AI_NUMERICSERV; + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + char port_str[6]; + snprintf(port_str, sizeof(port_str), "%hu", port); + + if (0 != getaddrinfo(hostname, port_str, &hints, &res_first)) + config_error("could not resolve hostname: %s", hostname); + + for (res = res_first; res != NULL; res = res->ai_next) { + if (res->ai_family == (use_ipv6 ? AF_INET6 : AF_INET)) { + config->raddr.ai_family = res->ai_family; + config->raddr.ai_socktype = res->ai_socktype | SOCK_NONBLOCK; + config->raddr.ai_protocol = res->ai_protocol; + config->raddr.ai_addrlen = res->ai_addrlen; + config->raddr.ai_addr = (struct sockaddr *) + memcpy(&config->raddr_storage, res->ai_addr, res->ai_addrlen); + break; + } + } + + freeaddrinfo(res_first); + if (NULL == res) + config_error("could not resolve hostname: %s", hostname); +} + + +__attribute_cold__ +static int +config_base64_encode_pad (char * const restrict dst, const size_t dstsz, + const char * const restrict ssrc) +{ + static const char base64_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + + const size_t srclen = strlen(ssrc); + const int rem = (int)(srclen % 3); + const int tuples = (int)(srclen / 3); + const int tuplen = (int)(srclen - (size_t)rem); + if (srclen > INT_MAX/2) /*(ridiculous size; prevent integer overflow)*/ + return -1; + if (dstsz < (size_t)(4*tuples + (rem ? 4 : 0) + 1)) + return -1; + + int s = 0, d = 0; + const unsigned char * const src = (const unsigned char *)ssrc; + for (; s < tuplen; s += 3, d += 4) { + unsigned int v; + + v = (unsigned int)(src[s+0] << 16); + v |= (unsigned int)(src[s+1] << 8); + v |= (unsigned int)src[s+2]; + + dst[d+0] = base64_table[(v >> 18) & 0x3f]; + dst[d+1] = base64_table[(v >> 12) & 0x3f]; + dst[d+2] = base64_table[(v >> 6) & 0x3f]; + dst[d+3] = base64_table[(v ) & 0x3f]; + } + + if (rem) { + unsigned int v; + + if (1 == rem) { + v = (unsigned int)(src[s+0] << 4); + dst[d+2] = base64_table[64]; /* pad */ + } + else { /*(2 == rem)*/ + v = (unsigned int)(src[s+0] << 10); + v |= (unsigned int)(src[s+1] << 2); + dst[d+2] = base64_table[v & 0x3f]; v >>= 6; + } + dst[d+0] = base64_table[(v >> 6) & 0x3f]; + dst[d+1] = base64_table[(v ) & 0x3f]; + dst[d+3] = base64_table[64]; /* pad */ + d += 4; + } + + dst[d] = '\0'; + return d; /*(base64-encoded string length; might be 0)*/ +} + + +__attribute_cold__ +static void +config_request (Config * const restrict config, + const config_params * const restrict params) +{ + const char * restrict uri = params->uri; + uint16_t port = 80; + uint16_t default_port = 80; + char host[1024]; /*(host should be < 256 chars)*/ + + if (0 == strncmp(uri, "http://", sizeof("http://")-1)) + uri += 7; + else if (0 == strncmp(uri, "https://", sizeof("https://")-1)) { + uri += 8; + port = default_port = 443; + config_error("no ssl support yet"); + } + + /* XXX: note that this is not a fully proper URI parse */ + const char *c; + if ((c = strchr(uri, ':'))) { /* found ':' => host:port */ + if (c - uri + 1 > (int)sizeof(host)) + config_error("host name in URI is too long"); + memcpy(host, uri, c - uri); + host[c - uri] = '\0'; + + char *endptr; + unsigned long i = strtoul(c+1, &endptr, 10); + if (0 != i && i <= USHRT_MAX) { + port = (unsigned short)i; + uri = endptr; + } + else + config_error("could not parse URI"); + } + else { + if ((c = strchr(uri, '/'))) { + if (c - uri + 1 > (int)sizeof(host)) + config_error("host name in URI is too long"); + memcpy(host, uri, c - uri); + host[c - uri] = '\0'; + uri = c; + } + else { + size_t len = strlen(uri); + if (len + 1 > (int)sizeof(host)) + config_error("host name in URI is too long"); + memcpy(host, uri, len); + host[len] = '\0'; + uri += len; + } + } + + /* resolve hostname to sockaddr */ + config_raddr(config, host, port, params->use_ipv6); + + int idx_host = -1; + int idx_user_agent = -1; + int idx_content_type = -1; + int idx_content_length = -1; + int idx_transfer_encoding = -1; + const char * const * const restrict headers = params->headers; + for (int i = 0; i < params->headers_num; i++) { + if (0 == strncasecmp(headers[i],"Host:",sizeof("Host:")-1)) { + if (-1 != idx_host) + config_error("duplicate Host header"); + idx_host = i; + } + if (0 == strncasecmp(headers[i],"User-Agent:",sizeof("User-Agent:")-1)) + idx_user_agent = i; + if (0 == strncasecmp(headers[i],"Connection:",sizeof("Connection:")-1)) + config_error("Connection request header not allowed; " + "use -k param to enable keep-alive"); + if (0 == strncasecmp(headers[i],"Content-Type:", + sizeof("Content-Type:")-1)) + idx_content_type = i; + if (0 == strncasecmp(headers[i],"Content-Length:", + sizeof("Content-Length:")-1)) + idx_content_length = i; + if (0 == strncasecmp(headers[i],"Transfer-Encoding:", + sizeof("Transfer-Encoding:")-1)) + idx_transfer_encoding = i; + } + + /*(simple 8k memaligned request buffer (part of struct Config))*/ + config->request = + (char *)((uintptr_t)(config->buf + (8*1024-1)) & ~(uintptr_t)(8*1024-1)); + char * const restrict req = config->request; + const size_t sz = sizeof(config->buf) >> 1; + int offset = snprintf(req, sz, "%s %s HTTP/1.1\r\n", params->method, + config->proxy && config->proxy[0] != '/' + ? params->uri /*(provide full URI to proxy host)*/ + : *uri != '\0' ? uri : "/"); + if (offset >= (int)sz) + config_error("request too large"); + + int len = (-1 != idx_host) + ? snprintf(req+offset, sz-offset, "%s\r\n", headers[idx_host]) + : (port == default_port) + ? snprintf(req+offset, sz-offset, "Host: %s\r\n", host) + : snprintf(req+offset, sz-offset, "Host: %s:%hu\r\n", host, port); + if (len >= (int)sz - offset) + config_error("request too large"); + offset += len; + + if (!config->keep_alive) { + len = sizeof("Connection: close\r\n")-1; + if (len >= (int)sz - offset) + config_error("request too large"); + memcpy(req+offset, "Connection: close\r\n", len); + offset += len; + } + + int fd = -1; + off_t fsize = 0; + if (params->body_filename) { + #ifndef O_BINARY + #define O_BINARY 0 + #endif + #ifndef O_LARGEFILE + #define O_LARGEFILE 0 + #endif + #ifndef O_NOATIME + #define O_NOATIME 0 + #endif + fd = open(params->body_filename, + O_RDONLY|O_BINARY|O_LARGEFILE|O_NOATIME|O_NONBLOCK, 0); + if (-1 == fd) + config_perror("open(%s)", params->body_filename); + struct stat st; + if (0 != fstat(fd, &st)) + config_perror("fstat(%s)", params->body_filename); + fsize = st.st_size; + if (fsize > UINT32_MAX - (8*1024)) + config_error("file size too large (not supported > ~4GB) (%s)", + params->body_filename); + + /* If user specified Transfer-Encoding, trust that it is proper, + * e.g. chunked, and that body_filename contains already-chunked data */ + if (-1 == idx_transfer_encoding) { + if (-1 == idx_content_length) { + len = snprintf(req+offset, sz-offset, + "Content-Length: %"PRId64"\r\n", (int64_t)fsize); + if (len >= (int)sz - offset) + config_error("request too large"); + offset += len; + } /*(else trust user specified length matching body_filename size)*/ + } + else if (-1 != idx_content_length) + config_error("Content-Length must be omitted " + "if Transfer-Encoding provided"); + + if (params->body_content_type) { + if (-1 == idx_content_type) + config_error("Content-Type duplicated in -H and -T params"); + len = snprintf(req+offset, sz-offset, + "Content-Type: %s\r\n", params->body_content_type); + if (len >= (int)sz - offset) + config_error("request too large"); + offset += len; + } + else if (-1 == idx_content_type) { + len = sizeof("Content-Type: text/plain\r\n")-1; + if (len >= (int)sz - offset) + config_error("request too large"); + memcpy(req+offset, "Content-Type: text/plain\r\n", len); + offset += len; + } + } + + for (int i = 0; i < params->headers_num; ++i) { + if (i == idx_host) + continue; + len = snprintf(req+offset, sz-offset, "%s\r\n", headers[i]); + if (len >= (int)sz - offset) + config_error("request too large"); + offset += len; + } + + if (params->authorization) { + len = snprintf(req+offset, sz-offset, "Authorization: Basic "); + if (len >= (int)sz - offset) + config_error("request too large"); + offset += len; + + len = config_base64_encode_pad(req+offset, sz-offset, + params->authorization); + if (len < 0) + config_error("request too large"); + offset += len; + + if (2 >= (int)sz - offset) + config_error("request too large"); + memcpy(req+offset, "\r\n", 3); + offset += 2; + } + + if (params->proxy_authorization) { + len = snprintf(req+offset, sz-offset, "Proxy-Authorization: Basic "); + if (len >= (int)sz - offset) + config_error("request too large"); + offset += len; + + len = config_base64_encode_pad(req+offset, sz-offset, + params->proxy_authorization); + if (len < 0) + config_error("request too large"); + offset += len; + + if (2 >= (int)sz - offset) + config_error("request too large"); + memcpy(req+offset, "\r\n", 3); + offset += 2; + } + + if (-1 == idx_user_agent) { + len = sizeof("User-Agent: weighttp/" PACKAGE_VERSION "\r\n")-1; + if (len >= (int)sz - offset) + config_error("request too large"); + memcpy(req+offset, + "User-Agent: weighttp/" PACKAGE_VERSION "\r\n", len); + offset += len; + } + + const char * const * const restrict cookies = params->cookies; + for (int i = 0; i < params->cookies_num; ++i) { + len = snprintf(req+offset, sz-offset, "Cookie: %s\r\n",cookies[i]); + if (len >= (int)sz - offset) + config_error("request too large"); + offset += len; + } + + if (3 > (int)sz - offset) + config_error("request too large"); + memcpy(req+offset, "\r\n", 3); /*(including terminating '\0')*/ + offset += 2; /*(not including terminating '\0')*/ + + config->request_size = (uint32_t)(offset + fsize); + + if (-1 != fd && 0 != fsize) { + /*(not checking if file changed between fstat() and read())*/ + /*(not using mmap() since we expect benchmark test file to be smallish + * and able to fit in memory, or */ + config->request = malloc(config->request_size); + memcpy(config->request, req, (size_t)offset); + off_t reqsz = offset; + ssize_t rd; + do { + rd = read(fd, config->request+reqsz, config->request_size-reqsz); + } while (rd > 0 ? (reqsz += rd) < config->request_size + : (rd < 0 && errno == EINTR)); + if (reqsz != config->request_size) + config_perror("read(%s)", params->body_filename); + } +} + + +__attribute_cold__ +__attribute_noinline__ +static void +weighttp_setup (Config * const restrict config, const int argc, char *argv[]) +{ + int opt_show_help = 0; + int opt_show_version = 0; + config_params params; + memset(¶ms, 0, sizeof(params)); + + /* default settings */ + config->thread_count = 1; + config->concur_count = 1; + config->req_count = 0; + config->keep_alive = 0; + config->proxy = NULL; + config->pipeline_max = 0; + config->tcp_fastopen = 0; + config->http_head = 0; + config->so_bufsz = 0; + config->quiet = 0; + + setlocale(LC_ALL, "C"); + signal(SIGPIPE, SIG_IGN); + + const char * const optstr = ":hVikqdlr6Fm:n:t:c:b:p:u:A:B:C:H:K:P:T:X:"; + int opt; + while (-1 != (opt = getopt(argc, argv, optstr))) { + switch (opt) { + case '6': + params.use_ipv6 = 1; + break; + case 'A': + params.authorization = optarg; + break; + case 'B': + params.laddrstr = optarg; + break; + case 'C': + if (params.cookies_num == sizeof(params.cookies)/sizeof(char *)) + config_error("too many cookies"); + params.cookies[params.cookies_num++] = optarg; + break; + case 'F': + config->tcp_fastopen = 1; + break; + case 'H': + if (params.headers_num == sizeof(params.headers)/sizeof(char *)) + config_error("too many headers"); + params.headers[params.headers_num++] = optarg; + break; + case 'K': + config->pipeline_max = (int)strtoul(optarg, NULL, 10); + if (config->pipeline_max >= 2) + config->keep_alive = 1; + break; + case 'P': + params.proxy_authorization = optarg; + break; + case 'T': + params.body_content_type = optarg; + break; + case 'X': + config->proxy = optarg; + break; + case 'b': + config->so_bufsz = (int)strtoul(optarg, NULL, 10); + break; + case 'c': + config->concur_count = (int)strtoul(optarg, NULL, 10); + break; + case 'i': + config->http_head = 1; + break; + case 'k': + config->keep_alive = 1; + break; + case 'm': + params.method = optarg; + config->http_head = (0 == strcasecmp(optarg, "HEAD")); + break; + case 'n': + config->req_count = strtoull(optarg, NULL, 10); + break; + case 'p': + params.body_filename = optarg; + params.method = "POST"; + config->http_head = 0; + break; + case 'q': + config->quiet = 1; + break; + case 'd': + case 'l': + case 'r': + /*(ignored; compatibility with Apache Bench (ab))*/ + break; + case 't': + config->thread_count = (int)strtoul(optarg, NULL, 10); + break; + case 'u': + params.body_filename = optarg; + params.method = "PUT"; + config->http_head = 0; + break; + case ':': + config_error("option requires an argument: -%c", optopt); + case '?': + if ('?' != optopt) + config_error("unknown option: -%c", optopt); + /* fall through */ + case 'h': + opt_show_help = 1; + /* fall through */ + case 'V': + opt_show_version = 1; + break; + } + } + + if (opt_show_version || !config->quiet) + show_version(); + + if (opt_show_help) + show_help(); + + if (opt_show_version) + exit(0); + + if ((argc - optind) < 1) + config_error("missing URI argument"); + else if ((argc - optind) > 1) + config_error("too many arguments"); + params.uri = argv[optind]; + + /* check for sane arguments */ + if (!config->req_count) + config_error("num of requests has to be > 0"); + if (config->req_count == UINT64_MAX) + config_error("invalid req_count"); + if (!config->thread_count) + config_error("thread count has to be > 0"); + if ((uint64_t)config->thread_count > config->req_count) + config_error("thread_count > req_count"); + if (!config->concur_count) + config_error("num of concurrent clients has to be > 0"); + if ((uint64_t)config->concur_count > config->req_count) + config_error("concur_count > req_count"); + if (config->thread_count > config->concur_count) + config_error("thread_count > concur_count"); + if (config->pipeline_max < 1) + config->pipeline_max = 1; + if (NULL == params.method) + params.method = config->http_head ? "HEAD" : "GET"; + + config_request(config, ¶ms); + + config->laddr.ai_addrlen = 0; + config->laddrs.addrs = NULL; + config->laddrs.num = 0; + if (params.laddrstr && !config_laddrs(config, params.laddrstr)) + config_error("could not resolve local bind address: %s", + params.laddrstr); + + if (config->concur_count > 32768 && config->raddr.ai_family != AF_UNIX) { + int need = config->concur_count; + int avail = 32768; + int fd = open("/proc/sys/net/ipv4/ip_local_port_range", + O_RDONLY|O_BINARY|O_LARGEFILE|O_NONBLOCK, 0); + if (fd >= 0) { + char buf[32]; + ssize_t rd = read(fd, buf, sizeof(buf)); + if (rd >= 3 && rd < (ssize_t)sizeof(buf)) { + long lb, ub; + char *e; + buf[rd] = '\0'; + lb = strtoul(buf, &e, 10); + if (lb > 0 && lb < USHRT_MAX && *e) { + ub = strtoul(e, &e, 10); + if (ub > 0 && ub <= USHRT_MAX && (*e=='\0' || *e=='\n')) { + if (lb <= ub) + avail = ub - lb + 1; + } + } + } + close(fd); + } + if (config->laddrs.num) + need = (need + config->laddrs.num - 1) / config->laddrs.num; + if (need > avail) + config_error("not enough local ports for concurrency\n" + "Reduce concur or provide -B addr,addr,addr " + "to specify multiple local bind addrs"); + } + + /* (see [RFC7413] 4.1.3. Client Cookie Handling) */ + if ((config->proxy && config->proxy[0] == '/') + || config->request_size > (params.use_ipv6 ? 1440 : 1460)) + config->tcp_fastopen = 0; +} + + +__attribute_cold__ +__attribute_noinline__ +static void +weighttp_report (const Config * const restrict config) +{ + /* collect worker stats and releaes resources */ + Stats stats; + memset(&stats, 0, sizeof(stats)); + for (int i = 0; i < config->thread_count; ++i) { + const Stats * const restrict wstats = &config->wconfs[i].stats; + + stats.req_started += wstats->req_started; + stats.req_done += wstats->req_done; + stats.req_success += wstats->req_success; + stats.req_failed += wstats->req_failed; + stats.bytes_total += wstats->bytes_total; + stats.bytes_headers += wstats->bytes_headers; + stats.req_2xx += wstats->req_2xx; + stats.req_3xx += wstats->req_3xx; + stats.req_4xx += wstats->req_4xx; + stats.req_5xx += wstats->req_5xx; + } + + /* report cumulative stats */ + struct timeval tdiff; + tdiff.tv_sec = config->ts_end.tv_sec - config->ts_start.tv_sec; + tdiff.tv_usec = config->ts_end.tv_usec - config->ts_start.tv_usec; + if (tdiff.tv_usec < 0) { + --tdiff.tv_sec; + tdiff.tv_usec += 1000000; + } + const uint64_t total_usecs = tdiff.tv_sec * 1000000 + tdiff.tv_usec; + const uint64_t rps = stats.req_done * 1000000 / total_usecs; + const uint64_t kbps= stats.bytes_total / 1024 * 1000000 / total_usecs; + #if 1 /* JSON-style formatted output */ + printf("{\n" + " \"reqs_per_sec\": %"PRIu64",\n" + " \"kBps_per_sec\": %"PRIu64",\n" + " \"secs_elapsed\": %01d.%06ld,\n", + rps, kbps, (int)tdiff.tv_sec, (long)tdiff.tv_usec); + printf(" \"request_counts\": {\n" + " \"started\": %"PRIu64",\n" + " \"retired\": %"PRIu64",\n" + " \"total\": %"PRIu64"\n" + " },\n", + stats.req_started, stats.req_done, config->req_count); + printf(" \"response_counts\": {\n" + " \"pass\": %"PRIu64",\n" + " \"fail\": %"PRIu64",\n" + " \"errs\": %"PRIu64"\n" + " },\n", + stats.req_success, stats.req_failed, stats.req_error); + printf(" \"status_codes\": {\n" + " \"2xx\": %"PRIu64",\n" + " \"3xx\": %"PRIu64",\n" + " \"4xx\": %"PRIu64",\n" + " \"5xx\": %"PRIu64"\n" + " },\n", + stats.req_2xx, stats.req_3xx, stats.req_4xx, stats.req_5xx); + printf(" \"traffic\": {\n" + " \"bytes_total\": %12."PRIu64",\n" + " \"bytes_headers\": %12."PRIu64",\n" + " \"bytes_body\": %12."PRIu64"\n" + " }\n" + "}\n", + stats.bytes_total, stats.bytes_headers, + stats.bytes_total - stats.bytes_headers); + #else + printf("\nfinished in %01d.%06ld sec, %"PRIu64" req/s, %"PRIu64" kbyte/s\n", + (int)tdiff.tv_sec, (long)tdiff.tv_usec, rps, kbps); + + printf("requests: %"PRIu64" started, %"PRIu64" done, %"PRIu64" total\n" + "responses: %"PRIu64" success, %"PRIu64" fail, %"PRIu64" error\n", + stats.req_started, stats.req_done, config->req_count, + stats.req_success, stats.req_failed, stats.req_error); + + printf("status codes: " + "%"PRIu64" 2xx, %"PRIu64" 3xx, %"PRIu64" 4xx, %"PRIu64" 5xx\n", + stats.req_2xx, stats.req_3xx, stats.req_4xx, stats.req_5xx); + + printf("traffic: %"PRIu64" bytes total, %"PRIu64" bytes headers, " + "%"PRIu64" bytes body\n", stats.bytes_total, + stats.bytes_headers, stats.bytes_total - stats.bytes_headers); + #endif +} + + +int main (int argc, char *argv[]) +{ + Config config; + weighttp_setup(&config, argc, argv); /* exits if error (or done) */ + wconfs_init(&config); + #if defined(__STDC_VERSION__) && __STDC_VERSION__-0 >= 199901L /* C99 */ + pthread_t threads[config.thread_count]; /*(C99 dynamic array)*/ + #else + pthread_t * const restrict threads = + (pthread_t *)calloc(config.thread_count, sizeof(pthread_t)); + #endif + + if (!config.quiet) + puts("starting benchmark..."); + gettimeofday(&config.ts_start, NULL); + + for (int i = 0; i < config.thread_count; ++i) { + int err = pthread_create(threads+i, NULL, + worker_thread, (void*)(config.wconfs+i)); + if (__builtin_expect( (0 != err), 0)) { + fprintf(stderr, "error: failed spawning thread (%d)\n", err); + /*(XXX: leaks resources and does not attempt pthread_cancel())*/ + return 2; /* (unexpected) fatal error */ + } + } + + for (int i = 0; i < config.thread_count; ++i) { + int err = pthread_join(threads[i], NULL); + if (__builtin_expect( (0 != err), 0)) { + fprintf(stderr, "error: failed joining thread (%d)\n", err); + /*(XXX: leaks resources and does not attempt pthread_cancel())*/ + return 3; /* (unexpected) fatal error */ + } + } + + gettimeofday(&config.ts_end, NULL); + + #if !(defined(__STDC_VERSION__) && __STDC_VERSION__-0 >= 199901L) /* !C99 */ + free(threads); + #endif + weighttp_report(&config); + wconfs_delete(&config); + + return 0; +} diff --git a/src/cmake/CodeCoverage.cmake b/src/cmake/CodeCoverage.cmake new file mode 100644 index 000000000..1b397a06c --- /dev/null +++ b/src/cmake/CodeCoverage.cmake @@ -0,0 +1,751 @@ +# Copyright (c) 2012 - 2017, Lars Bilke +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# CHANGES: +# +# 2012-01-31, Lars Bilke +# - Enable Code Coverage +# +# 2013-09-17, Joakim Söderberg +# - Added support for Clang. +# - Some additional usage instructions. +# +# 2016-02-03, Lars Bilke +# - Refactored functions to use named parameters +# +# 2017-06-02, Lars Bilke +# - Merged with modified version from github.com/ufz/ogs +# +# 2019-05-06, Anatolii Kurotych +# - Remove unnecessary --coverage flag +# +# 2019-12-13, FeRD (Frank Dana) +# - Deprecate COVERAGE_LCOVR_EXCLUDES and COVERAGE_GCOVR_EXCLUDES lists in favor +# of tool-agnostic COVERAGE_EXCLUDES variable, or EXCLUDE setup arguments. +# - CMake 3.4+: All excludes can be specified relative to BASE_DIRECTORY +# - All setup functions: accept BASE_DIRECTORY, EXCLUDE list +# - Set lcov basedir with -b argument +# - Add automatic --demangle-cpp in lcovr, if 'c++filt' is available (can be +# overridden with NO_DEMANGLE option in setup_target_for_coverage_lcovr().) +# - Delete output dir, .info file on 'make clean' +# - Remove Python detection, since version mismatches will break gcovr +# - Minor cleanup (lowercase function names, update examples...) +# +# 2019-12-19, FeRD (Frank Dana) +# - Rename Lcov outputs, make filtered file canonical, fix cleanup for targets +# +# 2020-01-19, Bob Apthorpe +# - Added gfortran support +# +# 2020-02-17, FeRD (Frank Dana) +# - Make all add_custom_target()s VERBATIM to auto-escape wildcard characters +# in EXCLUDEs, and remove manual escaping from gcovr targets +# +# 2021-01-19, Robin Mueller +# - Add CODE_COVERAGE_VERBOSE option which will allow to print out commands which are run +# - Added the option for users to set the GCOVR_ADDITIONAL_ARGS variable to supply additional +# flags to the gcovr command +# +# 2020-05-04, Mihchael Davis +# - Add -fprofile-abs-path to make gcno files contain absolute paths +# - Fix BASE_DIRECTORY not working when defined +# - Change BYPRODUCT from folder to index.html to stop ninja from complaining about double defines +# +# 2021-05-10, Martin Stump +# - Check if the generator is multi-config before warning about non-Debug builds +# +# 2022-02-22, Marko Wehle +# - Change gcovr output from -o for --xml and --html output respectively. +# This will allow for Multiple Output Formats at the same time by making use of GCOVR_ADDITIONAL_ARGS, e.g. GCOVR_ADDITIONAL_ARGS "--txt". +# +# 2022-09-28, Sebastian Mueller +# - fix append_coverage_compiler_flags_to_target to correctly add flags +# - replace "-fprofile-arcs -ftest-coverage" with "--coverage" (equivalent) +# +# USAGE: +# +# 1. Copy this file into your cmake modules path. +# +# 2. Add the following line to your CMakeLists.txt (best inside an if-condition +# using a CMake option() to enable it just optionally): +# include(CodeCoverage) +# +# 3. Append necessary compiler flags for all supported source files: +# append_coverage_compiler_flags() +# Or for specific target: +# append_coverage_compiler_flags_to_target(YOUR_TARGET_NAME) +# +# 3.a (OPTIONAL) Set appropriate optimization flags, e.g. -O0, -O1 or -Og +# +# 4. If you need to exclude additional directories from the report, specify them +# using full paths in the COVERAGE_EXCLUDES variable before calling +# setup_target_for_coverage_*(). +# Example: +# set(COVERAGE_EXCLUDES +# '${PROJECT_SOURCE_DIR}/src/dir1/*' +# '/path/to/my/src/dir2/*') +# Or, use the EXCLUDE argument to setup_target_for_coverage_*(). +# Example: +# setup_target_for_coverage_lcov( +# NAME coverage +# EXECUTABLE testrunner +# EXCLUDE "${PROJECT_SOURCE_DIR}/src/dir1/*" "/path/to/my/src/dir2/*") +# +# 4.a NOTE: With CMake 3.4+, COVERAGE_EXCLUDES or EXCLUDE can also be set +# relative to the BASE_DIRECTORY (default: PROJECT_SOURCE_DIR) +# Example: +# set(COVERAGE_EXCLUDES "dir1/*") +# setup_target_for_coverage_gcovr_html( +# NAME coverage +# EXECUTABLE testrunner +# BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src" +# EXCLUDE "dir2/*") +# +# 5. Use the functions described below to create a custom make target which +# runs your test executable and produces a code coverage report. +# +# 6. Build a Debug build: +# cmake -DCMAKE_BUILD_TYPE=Debug .. +# make +# make my_coverage_target +# + +include(CMakeParseArguments) + +option(CODE_COVERAGE_VERBOSE "Verbose information" FALSE) + +get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG OR CMAKE_BUILD_TYPE STREQUAL "Coverage") + +# Check prereqs +find_program( GCOV_PATH gcov ) +find_program( LCOV_PATH NAMES lcov lcov.bat lcov.exe lcov.perl) +find_program( FASTCOV_PATH NAMES fastcov fastcov.py ) +find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat ) +find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test) +find_program( CPPFILT_PATH NAMES c++filt ) + +if(GCOV_PATH) + +# Check supported compiler (Clang, GNU and Flang) +get_property(LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) +foreach(LANG ${LANGUAGES}) + if("${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") + if("${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS 3) + message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...") + endif() + elseif(NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "GNU" + AND NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(LLVM)?[Ff]lang") + message(FATAL_ERROR "Compiler is not GNU or Flang! Aborting...") + endif() +endforeach() + +set(COVERAGE_COMPILER_FLAGS "-g --coverage" + CACHE INTERNAL "") + +if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag(-fprofile-abs-path HAVE_cxx_fprofile_abs_path) + if(HAVE_cxx_fprofile_abs_path) + set(COVERAGE_CXX_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") + endif() +endif() +if(CMAKE_C_COMPILER_ID MATCHES "(GNU|Clang)") + include(CheckCCompilerFlag) + check_c_compiler_flag(-fprofile-abs-path HAVE_c_fprofile_abs_path) + if(HAVE_c_fprofile_abs_path) + set(COVERAGE_C_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") + endif() +endif() + +set(CMAKE_Fortran_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the Fortran compiler during coverage builds." + FORCE ) +set(CMAKE_CXX_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C++ compiler during coverage builds." + FORCE ) +set(CMAKE_C_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C compiler during coverage builds." + FORCE ) +set(CMAKE_EXE_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used for linking binaries during coverage builds." + FORCE ) +set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used by the shared libraries linker during coverage builds." + FORCE ) +mark_as_advanced( + CMAKE_Fortran_FLAGS_COVERAGE + CMAKE_CXX_FLAGS_COVERAGE + CMAKE_C_FLAGS_COVERAGE + CMAKE_EXE_LINKER_FLAGS_COVERAGE + CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) + + +if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + link_libraries(gcov) +endif() + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_lcov( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# NO_DEMANGLE # Don't demangle C++ symbols +# # even if c++filt is found +# ) +function(setup_target_for_coverage_lcov) + + set(options NO_DEMANGLE SONARQUBE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT LCOV_PATH) + message(FATAL_ERROR "lcov not found! Aborting...") + endif() # NOT LCOV_PATH + + if(NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() # NOT GENHTML_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(LCOV_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_LCOV_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND LCOV_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES LCOV_EXCLUDES) + + # Conditional arguments + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) + set(GENHTML_EXTRA_ARGS "--demangle-cpp") + endif() + + # Setting up commands which will be run to generate coverage data. + # Cleanup lcov + set(LCOV_CLEAN_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory . + -b ${BASEDIR} --zerocounters + ) + # Create baseline to make sure untouched files show up in the report + set(LCOV_BASELINE_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -b + ${BASEDIR} -o ${Coverage_NAME}.base + ) + # Run tests + set(LCOV_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Capturing lcov counters and generating report + set(LCOV_CAPTURE_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . -b + ${BASEDIR} --capture --output-file ${Coverage_NAME}.capture + ) + # add baseline counters + set(LCOV_BASELINE_COUNT_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base + -a ${Coverage_NAME}.capture --output-file ${Coverage_NAME}.total + ) + # filter collected data to final coverage report + set(LCOV_FILTER_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove + ${Coverage_NAME}.total ${LCOV_EXCLUDES} --output-file ${Coverage_NAME}.info + ) + # Generate HTML output + set(LCOV_GEN_HTML_CMD + ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} -o + ${Coverage_NAME} ${Coverage_NAME}.info + ) + if(${Coverage_SONARQUBE}) + # Generate SonarQube output + set(GCOVR_XML_CMD + ${GCOVR_PATH} --sonarqube ${Coverage_NAME}_sonarqube.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + set(GCOVR_XML_CMD_COMMAND + COMMAND ${GCOVR_XML_CMD} + ) + set(GCOVR_XML_CMD_BYPRODUCTS ${Coverage_NAME}_sonarqube.xml) + set(GCOVR_XML_CMD_COMMENT COMMENT "SonarQube code coverage info report saved in ${Coverage_NAME}_sonarqube.xml.") + endif() + + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + message(STATUS "Command to clean up lcov: ") + string(REPLACE ";" " " LCOV_CLEAN_CMD_SPACED "${LCOV_CLEAN_CMD}") + message(STATUS "${LCOV_CLEAN_CMD_SPACED}") + + message(STATUS "Command to create baseline: ") + string(REPLACE ";" " " LCOV_BASELINE_CMD_SPACED "${LCOV_BASELINE_CMD}") + message(STATUS "${LCOV_BASELINE_CMD_SPACED}") + + message(STATUS "Command to run the tests: ") + string(REPLACE ";" " " LCOV_EXEC_TESTS_CMD_SPACED "${LCOV_EXEC_TESTS_CMD}") + message(STATUS "${LCOV_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to capture counters and generate report: ") + string(REPLACE ";" " " LCOV_CAPTURE_CMD_SPACED "${LCOV_CAPTURE_CMD}") + message(STATUS "${LCOV_CAPTURE_CMD_SPACED}") + + message(STATUS "Command to add baseline counters: ") + string(REPLACE ";" " " LCOV_BASELINE_COUNT_CMD_SPACED "${LCOV_BASELINE_COUNT_CMD}") + message(STATUS "${LCOV_BASELINE_COUNT_CMD_SPACED}") + + message(STATUS "Command to filter collected data: ") + string(REPLACE ";" " " LCOV_FILTER_CMD_SPACED "${LCOV_FILTER_CMD}") + message(STATUS "${LCOV_FILTER_CMD_SPACED}") + + message(STATUS "Command to generate lcov HTML output: ") + string(REPLACE ";" " " LCOV_GEN_HTML_CMD_SPACED "${LCOV_GEN_HTML_CMD}") + message(STATUS "${LCOV_GEN_HTML_CMD_SPACED}") + + if(${Coverage_SONARQUBE}) + message(STATUS "Command to generate SonarQube XML output: ") + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") + message(STATUS "${GCOVR_XML_CMD_SPACED}") + endif() + endif() + + # Setup target + add_custom_target(${Coverage_NAME} + COMMAND ${LCOV_CLEAN_CMD} + COMMAND ${LCOV_BASELINE_CMD} + COMMAND ${LCOV_EXEC_TESTS_CMD} + COMMAND ${LCOV_CAPTURE_CMD} + COMMAND ${LCOV_BASELINE_COUNT_CMD} + COMMAND ${LCOV_FILTER_CMD} + COMMAND ${LCOV_GEN_HTML_CMD} + ${GCOVR_XML_CMD_COMMAND} + + # Set output files as GENERATED (will be removed on 'make clean') + BYPRODUCTS + ${Coverage_NAME}.base + ${Coverage_NAME}.capture + ${Coverage_NAME}.total + ${Coverage_NAME}.info + ${GCOVR_XML_CMD_BYPRODUCTS} + ${Coverage_NAME}/index.html + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." + ) + + # Show where to find the lcov info report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info." + ${GCOVR_XML_CMD_COMMENT} + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # setup_target_for_coverage_lcov + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_xml( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_xml) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_XML_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Running gcovr + set(GCOVR_XML_CMD + ${GCOVR_PATH} --xml ${Coverage_NAME}.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_XML_EXEC_TESTS_CMD_SPACED "${GCOVR_XML_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_XML_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to generate gcovr XML coverage data: ") + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") + message(STATUS "${GCOVR_XML_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_XML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_XML_CMD} + + BYPRODUCTS ${Coverage_NAME}.xml + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce Cobertura code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml." + ) +endfunction() # setup_target_for_coverage_gcovr_xml + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_html( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_html) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_HTML_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Create folder + set(GCOVR_HTML_FOLDER_CMD + ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME} + ) + # Running gcovr + set(GCOVR_HTML_CMD + ${GCOVR_PATH} --html ${Coverage_NAME}/index.html --html-details -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_HTML_EXEC_TESTS_CMD_SPACED "${GCOVR_HTML_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_HTML_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to create a folder: ") + string(REPLACE ";" " " GCOVR_HTML_FOLDER_CMD_SPACED "${GCOVR_HTML_FOLDER_CMD}") + message(STATUS "${GCOVR_HTML_FOLDER_CMD_SPACED}") + + message(STATUS "Command to generate gcovr HTML coverage data: ") + string(REPLACE ";" " " GCOVR_HTML_CMD_SPACED "${GCOVR_HTML_CMD}") + message(STATUS "${GCOVR_HTML_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_HTML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_HTML_FOLDER_CMD} + COMMAND ${GCOVR_HTML_CMD} + + BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html # report directory + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce HTML code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # setup_target_for_coverage_gcovr_html + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_fastcov( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/" "src/dir2/" # Patterns to exclude. +# NO_DEMANGLE # Don't demangle C++ symbols +# # even if c++filt is found +# SKIP_HTML # Don't create html report +# POST_CMD perl -i -pe s!${PROJECT_SOURCE_DIR}/!!g ctest_coverage.json # E.g. for stripping source dir from file paths +# ) +function(setup_target_for_coverage_fastcov) + + set(options NO_DEMANGLE SKIP_HTML) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES FASTCOV_ARGS GENHTML_ARGS POST_CMD) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT FASTCOV_PATH) + message(FATAL_ERROR "fastcov not found! Aborting...") + endif() + + if(NOT Coverage_SKIP_HTML AND NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (Patterns, not paths, for fastcov) + set(FASTCOV_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_FASTCOV_EXCLUDES}) + list(APPEND FASTCOV_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES FASTCOV_EXCLUDES) + + # Conditional arguments + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) + set(GENHTML_EXTRA_ARGS "--demangle-cpp") + endif() + + # Set up commands which will be run to generate coverage data + set(FASTCOV_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}) + + set(FASTCOV_CAPTURE_CMD ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} + --search-directory ${BASEDIR} + --process-gcno + --output ${Coverage_NAME}.json + --exclude ${FASTCOV_EXCLUDES} + ) + + set(FASTCOV_CONVERT_CMD ${FASTCOV_PATH} + -C ${Coverage_NAME}.json --lcov --output ${Coverage_NAME}.info + ) + + if(Coverage_SKIP_HTML) + set(FASTCOV_HTML_CMD ";") + else() + set(FASTCOV_HTML_CMD ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} + -o ${Coverage_NAME} ${Coverage_NAME}.info + ) + endif() + + set(FASTCOV_POST_CMD ";") + if(Coverage_POST_CMD) + set(FASTCOV_POST_CMD ${Coverage_POST_CMD}) + endif() + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Code coverage commands for target ${Coverage_NAME} (fastcov):") + + message(" Running tests:") + string(REPLACE ";" " " FASTCOV_EXEC_TESTS_CMD_SPACED "${FASTCOV_EXEC_TESTS_CMD}") + message(" ${FASTCOV_EXEC_TESTS_CMD_SPACED}") + + message(" Capturing fastcov counters and generating report:") + string(REPLACE ";" " " FASTCOV_CAPTURE_CMD_SPACED "${FASTCOV_CAPTURE_CMD}") + message(" ${FASTCOV_CAPTURE_CMD_SPACED}") + + message(" Converting fastcov .json to lcov .info:") + string(REPLACE ";" " " FASTCOV_CONVERT_CMD_SPACED "${FASTCOV_CONVERT_CMD}") + message(" ${FASTCOV_CONVERT_CMD_SPACED}") + + if(NOT Coverage_SKIP_HTML) + message(" Generating HTML report: ") + string(REPLACE ";" " " FASTCOV_HTML_CMD_SPACED "${FASTCOV_HTML_CMD}") + message(" ${FASTCOV_HTML_CMD_SPACED}") + endif() + if(Coverage_POST_CMD) + message(" Running post command: ") + string(REPLACE ";" " " FASTCOV_POST_CMD_SPACED "${FASTCOV_POST_CMD}") + message(" ${FASTCOV_POST_CMD_SPACED}") + endif() + endif() + + # Setup target + add_custom_target(${Coverage_NAME} + + # Cleanup fastcov + COMMAND ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} + --search-directory ${BASEDIR} + --zerocounters + + COMMAND ${FASTCOV_EXEC_TESTS_CMD} + COMMAND ${FASTCOV_CAPTURE_CMD} + COMMAND ${FASTCOV_CONVERT_CMD} + COMMAND ${FASTCOV_HTML_CMD} + COMMAND ${FASTCOV_POST_CMD} + + # Set output files as GENERATED (will be removed on 'make clean') + BYPRODUCTS + ${Coverage_NAME}.info + ${Coverage_NAME}.json + ${Coverage_NAME}/index.html # report directory + + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Resetting code coverage counters to zero. Processing code coverage counters and generating report." + ) + + set(INFO_MSG "fastcov code coverage info report saved in ${Coverage_NAME}.info and ${Coverage_NAME}.json.") + if(NOT Coverage_SKIP_HTML) + string(APPEND INFO_MSG " Open ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html in your browser to view the coverage report.") + endif() + # Show where to find the fastcov info report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo ${INFO_MSG} + ) + +endfunction() # setup_target_for_coverage_fastcov + +function(append_coverage_compiler_flags) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}") +endfunction() # append_coverage_compiler_flags + +# Setup coverage for specific library +function(append_coverage_compiler_flags_to_target name) + separate_arguments(_flag_list NATIVE_COMMAND "${COVERAGE_COMPILER_FLAGS}") + target_compile_options(${name} PRIVATE ${_flag_list}) + if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + target_link_libraries(${name} PRIVATE gcov) + endif() +endfunction() + +endif() # CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG + +endif() # NOT GCOV_PATH diff --git a/src/cmake/EnableCFlag.cmake b/src/cmake/EnableCFlag.cmake new file mode 100644 index 000000000..fc721f043 --- /dev/null +++ b/src/cmake/EnableCFlag.cmake @@ -0,0 +1,11 @@ +macro (enable_c_flag_if_avail _flag _append_to_var _set_var) + check_c_compiler_flag(${_flag} ${_set_var}) + + if (${_set_var}) + set(${_append_to_var} "${${_append_to_var}} ${_flag}") + endif () +endmacro () + +macro (enable_warning_if_supported _flag) + enable_c_flag_if_avail(${_flag} CMAKE_C_FLAGS "supports ${_flag}") +endmacro () diff --git a/src/cmake/GitVersionDetect.cmake b/src/cmake/GitVersionDetect.cmake new file mode 100644 index 000000000..21c3d216c --- /dev/null +++ b/src/cmake/GitVersionDetect.cmake @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright 2023 Antonio Vázquez Blanco + +#[=======================================================================[.rst: +GitVersionDetect +------- + +Derives a program version from Git tags. Only tags starting with a "v" will be +used for version inference. + + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``GITVERSIONDETECT_VERSION`` + Full git tag as git describe --dirty would produce. +``GITVERSIONDETECT_VERSION_MAJOR`` + Major version matched from GITVERSIONDETECT_VERSION. +``GITVERSIONDETECT_VERSION_MINOR`` + Minor version matched from GITVERSIONDETECT_VERSION. +``GITVERSIONDETECT_VERSION_PATCH`` + Patch version matched from GITVERSIONDETECT_VERSION. +``GITVERSIONDETECT_VERSION_COMMIT_NUM`` + Number of commits that separate current source from the detected version + tag. +``GITVERSIONDETECT_VERSION_COMMIT_SHA`` + Latest commit sha. + +#]=======================================================================] + +# Required packages +find_package(Git) + +# Check if a git executable was found +if(GIT_EXECUTABLE) + # Generate a git-describe version string from Git repository tags + execute_process( + COMMAND ${GIT_EXECUTABLE} describe --tags --dirty --match "v*" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE GIT_DESCRIBE_VERSION + RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + # If no error took place, save the version + if(NOT GIT_DESCRIBE_ERROR_CODE) + string(REGEX REPLACE "^v" "" GITVERSIONDETECT_VERSION "${GIT_DESCRIBE_VERSION}") + endif() +endif() + +# Final fallback: Just use a bogus version string that is semantically older +# than anything else and spit out a warning to the developer. +if(NOT DEFINED GITVERSIONDETECT_VERSION) + set(GITVERSIONDETECT_VERSION 0.0.0-0-unknown) + message(WARNING "Failed to determine GITVERSIONDETECT_VERSION from Git tags. Using default version \"${GITVERSIONDETECT_VERSION}\".") +endif() + +# Split the version into major, minor, patch and prerelease +string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+)-([a-z0-9]+))?" GITVERSIONDETECT_VERSION_MATCH ${GITVERSIONDETECT_VERSION}) +set(GITVERSIONDETECT_VERSION_MAJOR ${CMAKE_MATCH_1}) +set(GITVERSIONDETECT_VERSION_MINOR ${CMAKE_MATCH_2}) +set(GITVERSIONDETECT_VERSION_PATCH ${CMAKE_MATCH_3}) +set(GITVERSIONDETECT_VERSION_COMMIT_NUM ${CMAKE_MATCH_5}) +set(GITVERSIONDETECT_VERSION_COMMIT_SHA ${CMAKE_MATCH_6}) diff --git a/src/cmake/TrySanitizer.cmake b/src/cmake/TrySanitizer.cmake new file mode 100644 index 000000000..6b0a3e905 --- /dev/null +++ b/src/cmake/TrySanitizer.cmake @@ -0,0 +1,22 @@ +macro(try_sanitizer _type) + set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS}) + + set(SANITIZER_FLAG "-fsanitize=${_type}") + set(CMAKE_REQUIRED_FLAGS "-Werror ${SANITIZER_FLAG}") + + check_c_compiler_flag(${SANITIZER_FLAG} HAVE_SANITIZER) + + set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS}) + unset(OLD_CMAKE_REQUIRED_FLAGS) + + if (HAVE_SANITIZER) + message(STATUS "Building with ${_type} sanitizer") + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${SANITIZER_FLAG}") + + string(TOUPPER ${_type} SANITIZER_NAME) + set(LWAN_HAVE_${SANITIZER_NAME}_SANITIZER 1) + endif () + + unset(HAVE_SANITIZER) + unset(SANITIZER_FLAG) +endmacro () diff --git a/src/cmake/lwan-build-config.h.cmake b/src/cmake/lwan-build-config.h.cmake new file mode 100644 index 000000000..9efc1f090 --- /dev/null +++ b/src/cmake/lwan-build-config.h.cmake @@ -0,0 +1,86 @@ +/* + * lwan - web server + * Copyright (c) 2016 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#cmakedefine LWAN_VERSION "@LWAN_VERSION@" + +/* API available in Glibc/Linux, but possibly not elsewhere */ +#cmakedefine LWAN_HAVE_ACCEPT4 +#cmakedefine LWAN_HAVE_ALLOCA_H +#cmakedefine LWAN_HAVE_CLOCK_GETTIME +#cmakedefine LWAN_HAVE_GET_CURRENT_DIR_NAME +#cmakedefine LWAN_HAVE_GETAUXVAL +#cmakedefine LWAN_HAVE_MEMPCPY +#cmakedefine LWAN_HAVE_MEMRCHR +#cmakedefine LWAN_HAVE_MKOSTEMP +#cmakedefine LWAN_HAVE_PIPE2 +#cmakedefine LWAN_HAVE_PTHREADBARRIER +#cmakedefine LWAN_HAVE_READAHEAD +#cmakedefine LWAN_HAVE_REALLOCARRAY +#cmakedefine LWAN_HAVE_EPOLL +#cmakedefine LWAN_HAVE_KQUEUE +#cmakedefine LWAN_HAVE_KQUEUE1 +#cmakedefine LWAN_HAVE_DLADDR +#cmakedefine LWAN_HAVE_POSIX_FADVISE +#cmakedefine LWAN_HAVE_LINUX_CAPABILITY +#cmakedefine LWAN_HAVE_PTHREAD_SET_NAME_NP +#cmakedefine LWAN_HAVE_GETENTROPY +#cmakedefine LWAN_HAVE_FWRITE_UNLOCKED +#cmakedefine LWAN_HAVE_GETTID +#cmakedefine LWAN_HAVE_SECURE_GETENV +#cmakedefine LWAN_HAVE_STATFS +#cmakedefine LWAN_HAVE_SO_ATTACH_REUSEPORT_CBPF +#cmakedefine LWAN_HAVE_SO_INCOMING_CPU +#cmakedefine LWAN_HAVE_SYSLOG +#cmakedefine LWAN_HAVE_STPCPY +#cmakedefine LWAN_HAVE_EVENTFD +#cmakedefine LWAN_HAVE_MINCORE +#cmakedefine LWAN_HAVE_STATFS_F_TYPE + +/* Compiler builtins for specific CPU instruction support */ +#cmakedefine LWAN_HAVE_BUILTIN_CLZLL +#cmakedefine LWAN_HAVE_BUILTIN_CPU_INIT +#cmakedefine LWAN_HAVE_BUILTIN_IA32_CRC32 +#cmakedefine LWAN_HAVE_BUILTIN_MUL_OVERFLOW +#cmakedefine LWAN_HAVE_BUILTIN_ADD_OVERFLOW +#cmakedefine LWAN_HAVE_BUILTIN_FPCLASSIFY +#cmakedefine LWAN_HAVE_BUILTIN_EXPECT_PROBABILITY + +/* GCC extensions */ +#cmakedefine LWAN_HAVE_ACCESS_ATTRIBUTE + +/* C11 _Static_assert() */ +#cmakedefine LWAN_HAVE_STATIC_ASSERT + +/* Libraries */ +#cmakedefine LWAN_HAVE_LUA +#cmakedefine LWAN_HAVE_LUA_JIT +#cmakedefine LWAN_HAVE_BROTLI +#cmakedefine LWAN_HAVE_ZSTD +#cmakedefine LWAN_HAVE_LIBUCONTEXT +#cmakedefine LWAN_HAVE_MBEDTLS + +/* Valgrind support for coroutines */ +#cmakedefine LWAN_HAVE_VALGRIND + +/* Sanitizer */ +#cmakedefine LWAN_HAVE_UNDEFINED_SANITIZER +#cmakedefine LWAN_HAVE_ADDRESS_SANITIZER +#cmakedefine LWAN_HAVE_THREAD_SANITIZER diff --git a/src/cmake/lwan.pc.cmake b/src/cmake/lwan.pc.cmake new file mode 100644 index 000000000..fe3ee4c9b --- /dev/null +++ b/src/cmake/lwan.pc.cmake @@ -0,0 +1,9 @@ +Name: ${PROJECT_NAME} +Description: ${PROJECT_DESCRIPTION} +Version: ${PROJECT_VERSION} +Requires: ${PKG_CONFIG_REQUIRES} +prefix=${CMAKE_INSTALL_PREFIX} +includedir=${PKG_CONFIG_INCLUDEDIR} +libdir=${PKG_CONFIG_LIBDIR} +Libs: ${PKG_CONFIG_LIBS} +Cflags: ${PKG_CONFIG_CFLAGS} -include ${PKG_CONFIG_INCLUDEDIR}/lwan-build-config.h diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt new file mode 100644 index 000000000..8cdf4e281 --- /dev/null +++ b/src/lib/CMakeLists.txt @@ -0,0 +1,199 @@ +if (CMAKE_CROSSCOMPILING) + set(IMPORT_EXECUTABLES "IMPORTFILE-NOTFILE" CACHE FILEPATH "Point it to the export file from a native build") + include(${IMPORT_EXECUTABLES}) +endif () + +set(SOURCES + lwan-arena.c + lwan-array.c + lwan.c + lwan-cache.c + lwan-config.c + lwan-coro.c + lwan-h2-huffman.c + lwan-http-authorize.c + lwan-io-wrappers.c + lwan-job.c + lwan-pubsub.c + lwan-readahead.c + lwan-request.c + lwan-response.c + lwan-socket.c + lwan-status.c + lwan-straitjacket.c + lwan-strbuf.c + lwan-tables.c + lwan-template.c + lwan-thread.c + lwan-time.c + lwan-tq.c + lwan-trie.c + lwan-websocket.c + + lwan-mod-fastcgi.c + lwan-mod-redirect.c + lwan-mod-response.c + lwan-mod-rewrite.c + lwan-mod-serve-files.c + + missing.c + missing-epoll.c + missing-pthread.c + + base64.c + hash.c + int-to-str.c + list.c + patterns.c + realpathat.c + sd-daemon.c + sha1.c + timeout.c +) + +if (LWAN_HAVE_LUA) + list(APPEND SOURCES lwan-lua.c lwan-mod-lua.c) +endif () + +add_library(lwan-static STATIC ${SOURCES}) +set_target_properties(lwan-static PROPERTIES + OUTPUT_NAME lwan CLEAN_DIRECT_OUTPUT 1) +set_target_properties(lwan-static PROPERTIES COMPILE_FLAGS "-fPIC") + +# Can't call add_library() without source files. Create an empty .c file, +# then link with the static library just recently built. +file(WRITE "${CMAKE_BINARY_DIR}/empty.c" "") +add_library(lwan-shared SHARED "${CMAKE_BINARY_DIR}/empty.c") +target_link_libraries(lwan-shared + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) +if (NOT APPLE) + target_link_libraries(lwan-shared + -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/liblwan.sym + ) +endif () + +set_target_properties(lwan-shared PROPERTIES + OUTPUT_NAME lwan CLEAN_DIRECT_OUTPUT 1) + + +# Build mimegen +add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/mime-types.h + COMMAND mimegen + ${CMAKE_SOURCE_DIR}/src/bin/tools/mime.types > + ${CMAKE_BINARY_DIR}/mime-types.h + DEPENDS ${CMAKE_SOURCE_DIR}/src/bin/tools/mime.types mimegen + COMMENT "Building MIME type table" +) +add_custom_target(generate_mime_types_table + DEPENDS ${CMAKE_BINARY_DIR}/mime-types.h +) +add_dependencies(lwan-static generate_mime_types_table) + + +add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/lookup-http-status.h + COMMAND statuslookupgen > ${CMAKE_BINARY_DIR}/lookup-http-status.h + DEPENDS statuslookupgen + COMMENT "Building HTTP status lookup perfect hash table" +) +add_custom_target(generate_lookup_http_status + DEPENDS ${CMAKE_BINARY_DIR}/lookup-http-status.h +) +add_dependencies(lwan-static generate_lookup_http_status) + + +add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/servefile-data.h + COMMAND bin2hex + ${CMAKE_SOURCE_DIR}/wwwroot/icons/back.gif back_gif + ${CMAKE_SOURCE_DIR}/wwwroot/icons/file.gif file_gif + ${CMAKE_SOURCE_DIR}/wwwroot/icons/folder.gif folder_gif + ${CMAKE_SOURCE_DIR}/src/lib/servefile-template.html servefile_template + > + ${CMAKE_BINARY_DIR}/servefile-data.h + DEPENDS ${CMAKE_SOURCE_DIR}/wwwroot/icons/back.gif + ${CMAKE_SOURCE_DIR}/wwwroot/icons/file.gif + ${CMAKE_SOURCE_DIR}/wwwroot/icons/folder.gif + ${CMAKE_SOURCE_DIR}/src/lib/servefile-template.html + bin2hex + COMMENT "Bundling data for serve-files module" +) +add_custom_target(bundle_servefile_data + DEPENDS ${CMAKE_BINARY_DIR}/servefile-data.h +) +add_dependencies(lwan-static bundle_servefile_data) + +add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/response-data.h + COMMAND bin2hex + ${CMAKE_SOURCE_DIR}/src/lib/response-template.html response_template + > + ${CMAKE_BINARY_DIR}/response-data.h + DEPENDS ${CMAKE_SOURCE_DIR}/src/lib/response-template.html + bin2hex + COMMENT "Bundling data for response" +) +add_custom_target(bundle_response_data + DEPENDS ${CMAKE_BINARY_DIR}/response-data.h +) +add_dependencies(lwan-static bundle_response_data) + +if (NOT HAVE_BUILTIN_FPCLASSIFY) + set(ADDITIONAL_LIBRARIES ${ADDITIONAL_LIBRARIES} -lm PARENT_SCOPE) +endif () + +if (LWAN_HAVE_LIBUCONTEXT) + message(STATUS "Using libucontext/${CMAKE_SYSTEM_PROCESSOR} for coroutine context switching") + + include(ExternalProject) + + ExternalProject_Add(libucontext + SOURCE_DIR ${CMAKE_SOURCE_DIR}/src/3rdparty/libucontext + + BUILD_IN_SOURCE ON + + CONFIGURE_COMMAND "" + BUILD_COMMAND make libucontext.a FREESTANDING=yes + INSTALL_COMMAND make install-static install-headers DESTDIR=${CMAKE_BINARY_DIR} FREESTANDING=yes + + BUILD_BYPRODUCTS ${CMAKE_BINARY_DIR}/usr/lib/libucontext.a + + BUILD_ALWAYS OFF + UPDATE_DISCONNECTED ON + ) + add_dependencies(lwan-static libucontext) + + set(ADDITIONAL_LIBRARIES ${CMAKE_BINARY_DIR}/usr/lib/libucontext.a ${ADDITIONAL_LIBRARIES} PARENT_SCOPE) + include_directories(${CMAKE_BINARY_DIR}/usr/include) +else () + message(STATUS "Using built-in context switching routines for ${CMAKE_SYSTEM_PROCESSOR} processors") +endif () + + +install( + TARGETS lwan-static lwan-shared + DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR} +) + +install(FILES + hash.h + lwan-array.h + lwan-config.h + lwan-coro.h + lwan.h + lwan-http-status.h + lwan-mod-serve-files.h + lwan-mod-rewrite.h + lwan-mod-response.h + lwan-mod-redirect.h + lwan-mod-lua.h + lwan-status.h + lwan-template.h + lwan-trie.h + lwan-strbuf.h + timeout.h + list.h + DESTINATION "${CMAKE_INSTALL_FULL_INCLUDEDIR}/lwan") diff --git a/src/lib/base64.c b/src/lib/base64.c new file mode 100644 index 000000000..1535d9569 --- /dev/null +++ b/src/lib/base64.c @@ -0,0 +1,179 @@ +/* + * Base64 encoding/decoding (RFC1341) + * Copyright (c) 2005-2011, Jouni Malinen + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ + +#include +#include + +#include "base64.h" + +static const unsigned char base64_table[65] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/* + * Calculated using: + * memset(dtable, 0x80, 256); + * for (i = 0; i < sizeof(base64_table) - 1; i++) + * dtable[base64_table[i]] = (unsigned char) i; + * dtable['='] = 0; + */ +static const unsigned char base64_decode_table[256] = { + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x3e, 0x80, 0x80, 0x80, 0x3f, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x80, 0x80, + 0x80, 0x00, 0x80, 0x80, 0x80, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, + 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, + 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80}; + +bool +base64_validate(const unsigned char *src, size_t len) +{ + for (size_t i = 0; i < len; i++) { + if (base64_decode_table[src[i]] == 0x80) + return false; + } + + return true; +} + +/** + * base64_encode - Base64 encode + * @src: Data to be encoded + * @len: Length of the data to be encoded + * @out_len: Pointer to output length variable, or %NULL if not used + * Returns: Allocated buffer of out_len bytes of encoded data, + * or %NULL on failure + * + * Caller is responsible for freeing the returned buffer. Returned buffer is + * nul terminated to make it easier to use as a C string. The nul terminator is + * not included in out_len. + */ +unsigned char * +base64_encode(const unsigned char *src, size_t len, size_t *out_len) +{ + unsigned char *out, *pos; + const unsigned char *end, *in; + size_t olen; + + olen = base64_encoded_len(len) + 1 /* for NUL termination */; + if (olen < len) + return NULL; /* integer overflow */ + + out = malloc(olen); + if (out == NULL) + return NULL; + + end = src + len; + in = src; + pos = out; + while (end - in >= 3) { + *pos++ = base64_table[in[0] >> 2]; + *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; + *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)]; + *pos++ = base64_table[in[2] & 0x3f]; + in += 3; + } + + if (end - in) { + *pos++ = base64_table[in[0] >> 2]; + if (end - in == 1) { + *pos++ = base64_table[(in[0] & 0x03) << 4]; + *pos++ = '='; + } else { + *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; + *pos++ = base64_table[(in[1] & 0x0f) << 2]; + } + *pos++ = '='; + } + + *pos = '\0'; + if (out_len) + *out_len = (size_t)(pos - out); + return out; +} + +/** + * base64_decode - Base64 decode + * @src: Data to be decoded + * @len: Length of the data to be decoded + * @out_len: Pointer to output length variable + * Returns: Allocated buffer of out_len bytes of decoded data, + * or %NULL on failure + * + * Caller is responsible for freeing the returned buffer. + */ +unsigned char * +base64_decode(const unsigned char *src, size_t len, size_t *out_len) +{ + unsigned char *out, *pos, block[4]; + size_t i, count, olen; + int pad = 0; + + count = 0; + for (i = 0; i < len; i++) { + if (base64_decode_table[src[i]] != 0x80) + count++; + } + + if (count == 0 || count % 4) + return NULL; + + olen = (count / 4 * 3) + 1; + pos = out = malloc(olen); + if (out == NULL) + return NULL; + + count = 0; + for (i = 0; i < len; i++) { + unsigned char tmp = base64_decode_table[src[i]]; + if (tmp == 0x80) + continue; + + if (src[i] == '=') + pad++; + block[count] = tmp; + count++; + if (count == 4) { + *pos++ = (unsigned char)((block[0] << 2) | (block[1] >> 4)); + *pos++ = (unsigned char)((block[1] << 4) | (block[2] >> 2)); + *pos++ = (unsigned char)((block[2] << 6) | block[3]); + count = 0; + if (pad) { + if (pad == 1) + pos--; + else if (pad == 2) + pos -= 2; + else { + /* Invalid padding */ + free(out); + return NULL; + } + break; + } + } + } + *pos = '\0'; + + *out_len = (size_t)(pos - out); + return out; +} diff --git a/src/lib/base64.h b/src/lib/base64.h new file mode 100644 index 000000000..191b54f21 --- /dev/null +++ b/src/lib/base64.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +unsigned char *base64_encode(const unsigned char *src, size_t len, + size_t *out_len); +unsigned char *base64_decode(const unsigned char *src, size_t len, + size_t *out_len); + +bool base64_validate(const unsigned char *src, size_t len); + +static inline size_t base64_encoded_len(size_t decoded_len) +{ + /* This counts the padding bytes (by rounding to the next multiple of 4). */ + return ((4u * decoded_len / 3u) + 3u) & ~3u; +} diff --git a/src/lib/hash.c b/src/lib/hash.c new file mode 100644 index 000000000..0bf022a7d --- /dev/null +++ b/src/lib/hash.c @@ -0,0 +1,637 @@ +/* + * Based on libkmod-hash.c from libkmod - interface to kernel module operations + * Copyright (C) 2011-2012 ProFUSION embedded systems + * Copyright (C) 2013 L.Pereira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define _DEFAULT_SOURCE +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" +#include "hash.h" + +struct hash_bucket { + void **keys; + void **values; + unsigned int *hashvals; + + unsigned int used; + unsigned int total; +}; + +struct hash { + unsigned int count; + unsigned int n_buckets_mask; + + unsigned (*hash_value)(const void *key); + int (*key_equal)(const void *k1, const void *k2); + void (*free_value)(void *value); + void (*free_key)(void *value); + + struct hash_bucket *buckets; + + unsigned int refs; +}; + +struct hash_entry { + void **key; + void **value; + unsigned int *hashval; + + /* Only set when adding a new entry if it was already in the + * hash table -- always 0/false otherwise. */ + bool existing; +}; + +#define MIN_BUCKETS 64 + +/* Due to rehashing heuristics, most hash tables won't have more than 4 + * entries in each bucket. Use a conservative allocation threshold to + * curb wasteful allocations */ +#define STEPS 4 + +static_assert((MIN_BUCKETS & (MIN_BUCKETS - 1)) == 0, + "Bucket size is power of 2"); + +#define DEFAULT_FNV1A_64_SEED 0xcbf29ce484222325ull +#define DEFAULT_FNV1A_32_SEED 0x811c9dc5u + +uint64_t fnv1a_64_seed = DEFAULT_FNV1A_64_SEED; +uint32_t fnv1a_32_seed = DEFAULT_FNV1A_32_SEED; + +#define ASSERT_SEED_INITIALIZED() \ + do { \ + assert(fnv1a_64_seed != DEFAULT_FNV1A_64_SEED); \ + assert(fnv1a_32_seed != DEFAULT_FNV1A_32_SEED); \ + } while (0) + +static inline unsigned int hash_fnv1a_32(const void *keyptr); +static inline unsigned int hash_int_32(const void *keyptr); +static inline unsigned int hash_int_64(const void *keyptr); + +static unsigned (*hash_str)(const void *key) = hash_fnv1a_32; +static unsigned (*hash_int)(const void *key) = hash_int_32; +static unsigned (*hash_int64)(const void *key) = hash_int_64; + +static bool resize_bucket(struct hash_bucket *bucket, unsigned int new_size) +{ + void **new_keys; + void **new_values; + unsigned int *new_hashvals; + + new_keys = reallocarray(bucket->keys, new_size, STEPS * sizeof(void *)); + new_values = reallocarray(bucket->values, new_size, STEPS * sizeof(void *)); + new_hashvals = + reallocarray(bucket->hashvals, new_size, STEPS * sizeof(unsigned int)); + + if (new_keys) + bucket->keys = new_keys; + if (new_values) + bucket->values = new_values; + if (new_hashvals) + bucket->hashvals = new_hashvals; + + if (new_keys && new_values && new_hashvals) { + bucket->total = new_size * STEPS; + return true; + } + + return false; +} + +static inline unsigned int hash_fnv1a_32(const void *keyptr) +{ + return fnv1a_32(keyptr, strlen(keyptr)); +} + +static inline unsigned int hash_int_32(const void *keyptr) +{ + unsigned int key = (unsigned int)(long)keyptr; + return fnv1a_32(&key, sizeof(key)); +} + +static inline unsigned int hash_int_64(const void *keyptr) +{ + const uint64_t key = (uint64_t)(uintptr_t)keyptr; + return fnv1a_32(&key, sizeof(key)); +} + +#if defined(LWAN_HAVE_BUILTIN_CPU_INIT) && defined(LWAN_HAVE_BUILTIN_IA32_CRC32) +static inline unsigned int hash_str_crc32(const void *keyptr) +{ + unsigned int hash = fnv1a_32_seed; + const char *key = keyptr; + size_t len = strlen(key); + + ASSERT_SEED_INITIALIZED(); + +#if __x86_64__ + while (len >= sizeof(uint64_t)) { + uint64_t data; + memcpy(&data, key, sizeof(data)); + hash = (unsigned int)__builtin_ia32_crc32di(hash, data); + key += sizeof(uint64_t); + len -= sizeof(uint64_t); + } +#endif /* __x86_64__ */ + while (len >= sizeof(uint32_t)) { + uint32_t data; + memcpy(&data, key, sizeof(data)); + hash = __builtin_ia32_crc32si(hash, data); + key += sizeof(uint32_t); + len -= sizeof(uint32_t); + } + if (*key && *(key + 1)) { + uint16_t data; + memcpy(&data, key, sizeof(data)); + hash = __builtin_ia32_crc32hi(hash, data); + key += sizeof(uint16_t); + } + /* Last byte might be the terminating NUL or the last character. + * For a hash, this doesn't matter, and shaves off a branch. + */ + hash = __builtin_ia32_crc32qi(hash, (unsigned char)*key); + + return hash; +} + +static inline unsigned int hash_int_crc32(const void *keyptr) +{ + ASSERT_SEED_INITIALIZED(); + + return __builtin_ia32_crc32si(fnv1a_32_seed, + (unsigned int)(uintptr_t)keyptr); +} + +static inline unsigned int hash_int64_crc32(const void *keyptr) +{ + ASSERT_SEED_INITIALIZED(); + +#ifdef __x86_64__ + return (unsigned int)__builtin_ia32_crc32di(fnv1a_32_seed, + (uint64_t)(uintptr_t)keyptr); +#else + const uint64_t key = (uint64_t)(uintptr_t)keyptr; + uint32_t crc; + + crc = __builtin_ia32_crc32si(fnv1a_32_seed, (uint32_t)(key & 0xffffffff)); + crc = __builtin_ia32_crc32si(crc, (uint32_t)(key >> 32)); + + return crc; +#endif +} + +#endif + +LWAN_CONSTRUCTOR(fnv1a_seed, 65535) +{ + uint8_t entropy[128]; + + /* The seeds are randomized in order to mitigate the DDoS attack + * described by Crosby and Wallach in UsenixSec2003. */ + if (UNLIKELY(lwan_getentropy(entropy, sizeof(entropy), 0) < 0)) { + lwan_status_critical_perror("Could not initialize FNV1a seed"); + __builtin_unreachable(); + } + + fnv1a_64_seed = fnv1a_64(entropy, sizeof(entropy)); + fnv1a_32_seed = fnv1a_32(entropy, sizeof(entropy)); + lwan_always_bzero(entropy, sizeof(entropy)); + +#if defined(LWAN_HAVE_BUILTIN_CPU_INIT) && defined(LWAN_HAVE_BUILTIN_IA32_CRC32) + __builtin_cpu_init(); + if (__builtin_cpu_supports("sse4.2")) { + lwan_status_debug("Using CRC32 instructions to calculate hashes"); + hash_str = hash_str_crc32; + hash_int = hash_int_crc32; + hash_int64 = hash_int64_crc32; + } +#endif +} + +static inline int hash_int_key_equal(const void *k1, const void *k2) +{ + return k1 == k2; +} + +static void no_op(void *arg __attribute__((unused))) {} + +static struct hash * +hash_internal_new(unsigned int (*hash_value)(const void *key), + int (*key_equal)(const void *k1, const void *k2), + void (*free_key)(void *value), + void (*free_value)(void *value)) +{ + struct hash *hash = malloc(sizeof(*hash)); + + if (hash == NULL) + return NULL; + + hash->buckets = calloc(MIN_BUCKETS, sizeof(struct hash_bucket)); + if (hash->buckets == NULL) { + free(hash); + return NULL; + } + + hash->hash_value = hash_value; + hash->key_equal = key_equal; + + hash->free_value = free_value; + hash->free_key = free_key; + + hash->n_buckets_mask = MIN_BUCKETS - 1; + hash->count = 0; + + hash->refs = 1; + + return hash; +} + +struct hash *hash_int_new(void (*free_key)(void *value), + void (*free_value)(void *value)) +{ + return hash_internal_new(hash_int, hash_int_key_equal, + free_key ? free_key : no_op, + free_value ? free_value : no_op); +} + +struct hash *hash_int64_new(void (*free_key)(void *value), + void (*free_value)(void *value)) +{ + return hash_internal_new(hash_int64, hash_int_key_equal, + free_key ? free_key : no_op, + free_value ? free_value : no_op); +} + +struct hash *hash_str_new(void (*free_key)(void *value), + void (*free_value)(void *value)) +{ + return hash_internal_new( + hash_str, (int (*)(const void *, const void *))streq, + free_key ? free_key : no_op, free_value ? free_value : no_op); +} + +static __attribute__((pure)) inline unsigned int +hash_n_buckets(const struct hash *hash) +{ + return hash->n_buckets_mask + 1; +} + +struct hash *hash_ref(struct hash *hash) +{ + hash->refs++; + return hash; +} + +void hash_unref(struct hash *hash) +{ + struct hash_bucket *bucket, *bucket_end; + + if (hash == NULL) + return; + + hash->refs--; + if (hash->refs) + return; + + bucket = hash->buckets; + bucket_end = hash->buckets + hash_n_buckets(hash); + for (; bucket < bucket_end; bucket++) { + for (unsigned int entry = 0; entry < bucket->used; entry++) { + hash->free_value(bucket->values[entry]); + hash->free_key(bucket->keys[entry]); + } + free(bucket->keys); + free(bucket->values); + free(bucket->hashvals); + } + free(hash->buckets); + free(hash); +} + +static struct hash_entry hash_add_entry_hashed(struct hash *hash, + const void *key, + unsigned int hashval) +{ + unsigned int pos = hashval & hash->n_buckets_mask; + struct hash_bucket *bucket = hash->buckets + pos; + unsigned int entry; + bool existing = false; + + if (bucket->used + 1 >= bucket->total) { + unsigned int new_bucket_total; + + if (__builtin_add_overflow(bucket->total, 1, &new_bucket_total)) + return (struct hash_entry) {}; + + if (!resize_bucket(bucket, new_bucket_total)) + return (struct hash_entry) {}; + } + + for (entry = 0; entry < bucket->used; entry++) { + if (hashval != bucket->hashvals[entry]) + continue; + if (hash->key_equal(key, bucket->keys[entry])) { + existing = true; + goto done; + } + } + + entry = bucket->used; + + bucket->keys[entry] = NULL; + bucket->values[entry] = NULL; + bucket->hashvals[entry] = hashval; + + bucket->used++; + hash->count++; + +done: + return (struct hash_entry){ + .key = &bucket->keys[entry], + .value = &bucket->values[entry], + .hashval = &bucket->hashvals[entry], + .existing = existing, + }; +} + +static void rehash(struct hash *hash, unsigned int new_bucket_size) +{ + struct hash_bucket *buckets = calloc(new_bucket_size, sizeof(*buckets)); + const unsigned int n_buckets = hash_n_buckets(hash); + struct hash hash_copy = *hash; + + assert((new_bucket_size & (new_bucket_size - 1)) == 0); + assert(hash_n_buckets(hash) != new_bucket_size); + + if (buckets == NULL) + return; + + hash_copy.count = 0; + hash_copy.n_buckets_mask = new_bucket_size - 1; + hash_copy.buckets = buckets; + + struct hash_bucket *bucket; + struct hash_bucket *bucket_end = hash->buckets + n_buckets; + for (bucket = hash->buckets; bucket < bucket_end; bucket++) { + for (unsigned int old = 0; old < bucket->used; old++) { + struct hash_entry new = + hash_add_entry_hashed(&hash_copy, + bucket->keys[old], + bucket->hashvals[old]); + if (UNLIKELY(!new.key)) + goto fail; + + *new.key = bucket->keys[old]; + *new.value = bucket->values[old]; + } + } + + /* Original table must remain untouched in the event resizing fails: + * previous loop may return early on allocation failure, so can't free + * bucket entry arrays there. */ + for (bucket = hash->buckets; bucket < bucket_end; bucket++) { + free(bucket->keys); + free(bucket->values); + free(bucket->hashvals); + } + free(hash->buckets); + + hash->buckets = buckets; + hash->n_buckets_mask = new_bucket_size - 1; + + assert(hash_copy.count == hash->count); + + return; + +fail: + for (bucket_end = bucket, bucket = hash->buckets; bucket < bucket_end; + bucket++) { + free(bucket->keys); + free(bucket->values); + free(bucket->hashvals); + } + + free(buckets); +} + +static struct hash_entry hash_add_entry(struct hash *hash, const void *key) +{ + unsigned int hashval = hash->hash_value(key); + + return hash_add_entry_hashed(hash, key, hashval); +} + +static inline bool need_rehash_grow(const struct hash *hash) +{ + /* The heuristic to rehash and grow the number of buckets is if there's + * more than 16 entries per bucket on average. This is the number of + * elements in the hashvals array that would fit in a single cache line. */ + return hash->count > hash_n_buckets(hash) * 16; +} + +static inline bool need_rehash_shrink(const struct hash *hash) +{ + /* A hash table will be shrunk if, on average, more than 50% of its + * buckets are empty, but will never have less than MIN_BUCKETS buckets. */ + const unsigned int n_buckets = hash_n_buckets(hash); + + if (n_buckets <= MIN_BUCKETS) + return false; + + if (hash->count > n_buckets / 2) + return false; + + return true; +} + +/* + * add or replace key in hash map. + * + * none of key or value are copied, just references are remembered as is, + * make sure they are live while pair exists in hash! + */ +int hash_add(struct hash *hash, const void *key, const void *value) +{ + struct hash_entry entry = hash_add_entry(hash, key); + + if (!entry.key) + return -errno; + if (entry.existing) { + hash->free_key(*entry.key); + hash->free_value(*entry.value); + } + + *entry.key = (void *)key; + *entry.value = (void *)value; + + if (need_rehash_grow(hash)) + rehash(hash, hash_n_buckets(hash) * 2); + + return 0; +} + +/* similar to hash_add(), but fails if key already exists */ +int hash_add_unique(struct hash *hash, const void *key, const void *value) +{ + struct hash_entry entry = hash_add_entry(hash, key); + + if (!entry.key) + return -errno; + if (entry.existing) + return -EEXIST; + + *entry.key = (void *)key; + *entry.value = (void *)value; + + if (need_rehash_grow(hash)) + rehash(hash, hash_n_buckets(hash) * 2); + + return 0; +} + +static inline struct hash_entry +hash_find_entry(const struct hash *hash, const char *key, unsigned int hashval) +{ + unsigned int pos = hashval & hash->n_buckets_mask; + const struct hash_bucket *bucket = hash->buckets + pos; + + for (unsigned int entry = 0; entry < bucket->used; entry++) { + if (hashval != bucket->hashvals[entry]) + continue; + if (hash->key_equal(key, bucket->keys[entry])) { + return (struct hash_entry){ + .key = &bucket->keys[entry], + .value = &bucket->values[entry], + .hashval = &bucket->hashvals[entry], + }; + } + } + + return (struct hash_entry){}; +} + +void *hash_find(const struct hash *hash, const void *key) +{ + struct hash_entry entry = + hash_find_entry(hash, key, hash->hash_value(key)); + + return entry.key ? *entry.value : NULL; +} + +int hash_del(struct hash *hash, const void *key) +{ + unsigned int hashval = hash->hash_value(key); + unsigned int pos = hashval & hash->n_buckets_mask; + struct hash_bucket *bucket = hash->buckets + pos; + + struct hash_entry entry = hash_find_entry(hash, key, hashval); + if (entry.key == NULL) + return -ENOENT; + + hash->free_value(*entry.value); + hash->free_key(*entry.key); + + if (bucket->used > 1) { + /* Instead of compacting the bucket array by moving elements, just copy + * over the last element on top of the element being removed. This + * changes the ordering inside the bucket array, but it's much more + * efficient, as it always has to copy exactly at most 1 element instead + * of potentially bucket->used elements. */ + void *last_key = &bucket->keys[bucket->used - 1]; + + /* FIXME: Is comparing these pointers UB after calling free_key()? */ + if (entry.key != last_key) { + *entry.key = last_key; + *entry.value = bucket->values[bucket->used - 1]; + *entry.hashval = bucket->hashvals[bucket->used - 1]; + } + } + + bucket->used--; + hash->count--; + + if (need_rehash_shrink(hash)) { + rehash(hash, hash_n_buckets(hash) / 2); + } else { + unsigned int steps_used = bucket->used / STEPS; + unsigned int steps_total = bucket->total / STEPS; + + if (steps_used + 1 < steps_total) { + unsigned int new_total; + + if (__builtin_add_overflow(steps_used, 1, &new_total)) + return -EOVERFLOW; + + resize_bucket(bucket, new_total); + } + } + + return 0; +} + +unsigned int hash_get_count(const struct hash *hash) { return hash->count; } + +void hash_iter_init(const struct hash *hash, struct hash_iter *iter) +{ + iter->hash = hash; + iter->bucket = 0; + iter->entry = -1; +} + +bool hash_iter_next(struct hash_iter *iter, + const void **key, + const void **value) +{ + const struct hash_bucket *b = iter->hash->buckets + iter->bucket; + + iter->entry++; + + if ((unsigned int)iter->entry >= b->used) { + unsigned int n_buckets = hash_n_buckets(iter->hash); + + iter->entry = 0; + + for (iter->bucket++; iter->bucket < n_buckets; iter->bucket++) { + b = iter->hash->buckets + iter->bucket; + + if (b->used > 0) + break; + } + + if (iter->bucket >= n_buckets) + return false; + } + + if (value != NULL) + *value = b->values[iter->entry]; + if (key != NULL) + *key = b->keys[iter->entry]; + + return true; +} diff --git a/src/lib/hash.h b/src/lib/hash.h new file mode 100644 index 000000000..edec94134 --- /dev/null +++ b/src/lib/hash.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +struct hash; + +struct hash_iter { + const struct hash *hash; + unsigned int bucket; + int entry; +}; + +struct hash *hash_int_new(void (*free_key)(void *value), + void (*free_value)(void *value)); +struct hash *hash_int64_new(void (*free_key)(void *value), + void (*free_value)(void *value)); +struct hash *hash_str_new(void (*free_key)(void *value), + void (*free_value)(void *value)); + +struct hash *hash_ref(struct hash *hash); +void hash_unref(struct hash *hash); + +int hash_add(struct hash *hash, const void *key, const void *value); +int hash_add_unique(struct hash *hash, const void *key, const void *value); +int hash_del(struct hash *hash, const void *key); +void *hash_find(const struct hash *hash, const void *key); +unsigned int hash_get_count(const struct hash *hash); +void hash_iter_init(const struct hash *hash, struct hash_iter *iter); +bool hash_iter_next(struct hash_iter *iter, + const void **key, + const void **value); + +static inline uint64_t fnv1a_64(const void *buffer, size_t len) +{ + const unsigned char *data = (unsigned char *)buffer; + extern uint64_t fnv1a_64_seed; + uint64_t hash; + + for (hash = fnv1a_64_seed; len--; data++) { + hash = (hash ^ *data) * 0x100000001b3ul; + } + + return hash; +} + +static inline uint32_t fnv1a_32(const void *buffer, size_t len) +{ + const unsigned char *data = (unsigned char *)buffer; + extern uint32_t fnv1a_32_seed; + uint32_t hash; + + for (hash = fnv1a_32_seed; len--; data++) { + hash = (hash ^ *data) * 0x1000193u; + } + + return hash; +} diff --git a/src/lib/int-to-str.c b/src/lib/int-to-str.c new file mode 100644 index 000000000..d5a553f18 --- /dev/null +++ b/src/lib/int-to-str.c @@ -0,0 +1,84 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include + +#include "lwan-private.h" + +#include "int-to-str.h" + +static const char digits[201] = "0001020304050607080910111213141516171819" + "2021222324252627282930313233343536373839" + "4041424344454647484950515253545556575859" + "6061626364656667686970717273747576777879" + "8081828384858687888990919293949596979899"; + +ALWAYS_INLINE __attribute__((pure)) const char * +uint_to_string_2_digits(size_t value) +{ + assert(value <= 99); + return &digits[2 * value]; +} + +ALWAYS_INLINE char *uint_to_string(size_t value, + char dst[static INT_TO_STR_BUFFER_SIZE], + size_t *length_out) +{ + /* + * Based on routine by A. Alexandrescu, licensed under CC0 + * https://creativecommons.org/publicdomain/zero/1.0/legalcode + */ + static const size_t length = INT_TO_STR_BUFFER_SIZE; + size_t next = length - 1; + dst[next--] = '\0'; + while (value >= 100) { + const uint32_t i = (uint32_t)((value % 100) * 2); + value /= 100; + dst[next] = digits[i + 1]; + dst[next - 1] = digits[i]; + next -= 2; + } + // Handle last 1-2 digits + if (value < 10) { + dst[next] = (char)('0' + (uint32_t)value); + *length_out = length - next - 1; + return dst + next; + } + uint32_t i = (uint32_t)value * 2; + dst[next] = digits[i + 1]; + dst[next - 1] = digits[i]; + *length_out = length - next; + return dst + next - 1; +} + +ALWAYS_INLINE char *int_to_string(ssize_t value, + char dst[static INT_TO_STR_BUFFER_SIZE], + size_t *length_out) +{ + if (value < 0) { + char *p = uint_to_string((size_t)-value, dst, length_out); + *--p = '-'; + ++*length_out; + + return p; + } + + return uint_to_string((size_t)value, dst, length_out); +} diff --git a/common/int-to-str.h b/src/lib/int-to-str.h similarity index 80% rename from common/int-to-str.h rename to src/lib/int-to-str.h index d3b48d80a..7aa7ecedf 100644 --- a/common/int-to-str.h +++ b/src/lib/int-to-str.h @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -18,9 +18,10 @@ */ #pragma once -#include "lwan.h" +#include +#include -#define INT_TO_STR_BUFFER_SIZE (3 * sizeof(size_t) + 1) +#define INT_TO_STR_BUFFER_SIZE (3 * sizeof(size_t)) char *int_to_string(ssize_t value, char buffer[static INT_TO_STR_BUFFER_SIZE], @@ -29,3 +30,4 @@ char *uint_to_string(size_t value, char buffer[static INT_TO_STR_BUFFER_SIZE], size_t *len); +const char *uint_to_string_2_digits(size_t value) __attribute__((pure)); diff --git a/src/lib/liblwan.sym b/src/lib/liblwan.sym new file mode 100644 index 000000000..53241b454 --- /dev/null +++ b/src/lib/liblwan.sym @@ -0,0 +1,83 @@ +LIBLWAN_1 { +global: + lwan_array_append_heap; + lwan_array_append_inline; + lwan_array_reset; + lwan_array_sort; + + lwan_determine_mime_type_for_file_name; + lwan_get_config_path; + lwan_get_default_config; + + lwan_init; + lwan_init_with_config; + lwan_shutdown; + + lwan_job_add; + lwan_job_del; + + lwan_main_loop; + + lwan_request_get_*; + lwan_request_sleep; + + lwan_response_send_chunk; + lwan_response_send_event; + lwan_response_set_chunked; + lwan_response_set_event_stream; + lwan_prepare_response_header; + lwan_prepare_response_header_full; + + lwan_set_url_map; + + lwan_status_critical*; + lwan_status_critical_perror*; + lwan_status_error*; + lwan_status_info*; + lwan_status_perror*; + lwan_status_warning*; + + lwan_tpl_apply; + lwan_tpl_apply_with_buffer; + lwan_tpl_compile_file; + lwan_tpl_compile_string; + lwan_tpl_free; + lwan_append_*_to_strbuf; + lwan_tpl_*_is_empty; + + lwan_*_fd; + + lwan_strbuf_append_char; + lwan_strbuf_append_printf; + lwan_strbuf_append_str; + lwan_strbuf_free; + lwan_strbuf_init; + lwan_strbuf_init_with_size; + lwan_strbuf_new; + lwan_strbuf_new_static; + lwan_strbuf_new_with_size; + lwan_strbuf_printf; + lwan_strbuf_reset; + lwan_strbuf_set; + lwan_strbuf_set_static; + + lwan_module_info_*; + lwan_handler_info_*; + + lwan_request_await_*; + lwan_request_awaitv_*; + lwan_request_async_*; + + lwan_straitjacket_enforce*; + +local: + *; +}; + +LIBLWAN_LUA_1 { +global: + lwan_lua_create_state; + lwan_lua_state_last_error; + lwan_lua_state_push_request; + lwan_lua_get_request_from_userdata; +} LIBLWAN_1; diff --git a/src/lib/list.c b/src/lib/list.c new file mode 100644 index 000000000..c27fcc2f3 --- /dev/null +++ b/src/lib/list.c @@ -0,0 +1,102 @@ +/* + * list - double linked list routines + * Author: Rusty Russell + * + * The list header contains routines for manipulating double linked lists. + * It defines two types: struct list_head used for anchoring lists, and + * struct list_node which is usually embedded in the structure which is placed + * in the list. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT + * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include "list.h" + +#ifndef NDEBUG +static struct list_node poison_node_1 = {.next = (void *)0xdeadbeef, + .prev = (void *)0xbebacafe}; +static struct list_node poison_node_2 = {.next = (void *)0xbebacafe, + .prev = (void *)0xdeadbeef}; + +void list_poison_node(struct list_node *node) +{ + node->prev = &poison_node_1; + node->next = &poison_node_2; +} + +static bool is_poisoned(const struct list_node *node) +{ + assert(poison_node_1.next == poison_node_2.prev); + assert(poison_node_1.prev == poison_node_2.next); + assert(poison_node_1.next == (void *)0xdeadbeef); + assert(poison_node_1.prev == (void *)0xbebacafe); + + return (node->prev == &poison_node_1 || node->prev == &poison_node_2) || + (node->next == &poison_node_1 || node->next == &poison_node_2); +} + +static void *corrupt(const char *abortstr, + const struct list_node *head, + const struct list_node *node, + unsigned int count) +{ + if (abortstr) { + fprintf(stderr, "%s: prev corrupt in node %p (%u) of %p\n", abortstr, + node, count, head); + abort(); + } + return NULL; +} +#endif + +struct list_node *list_check_node(const struct list_node *node, + const char *abortstr) +{ +#ifndef NDEBUG + const struct list_node *p, *n; + unsigned int count = 0; + + if (is_poisoned(node)) + return corrupt(abortstr, node, node, 0); + + for (p = node, n = node->next; n != node; p = n, n = n->next) { + count++; + if (n->prev != p) + return corrupt(abortstr, node, n, count); + } + /* Check prev on head node. */ + if (node->prev != p) + return corrupt(abortstr, node, node, 0); +#else + (void)abortstr; +#endif + + return (struct list_node *)node; +} + +struct list_head *list_check(const struct list_head *h, const char *abortstr) +{ + if (!list_check_node(&h->n, abortstr)) + return NULL; + return (struct list_head *)h; +} diff --git a/common/list.h b/src/lib/list.h similarity index 95% rename from common/list.h rename to src/lib/list.h index a46a67fc6..59739cd0b 100644 --- a/common/list.h +++ b/src/lib/list.h @@ -257,44 +257,18 @@ struct list_head *list_check(const struct list_head *h, const char *abortstr); struct list_node *list_check_node(const struct list_node *n, const char *abortstr); -#ifdef _LIST_DEBUG +#ifndef NDEBUG +void list_poison_node(struct list_node *node); + +#define list_poison(n) list_poison_node(n) #define list_debug(h) list_check((h), __func__) #define list_debug_node(n) list_check_node((n), __func__) #else +#define list_poison(n) #define list_debug(h) (h) #define list_debug_node(n) (n) #endif -/** - * LIST_HEAD_INIT - initializer for an empty list_head - * @name: the name of the list. - * - * Explicit initializer for an empty list. - * - * See also: - * LIST_HEAD, list_head_init() - * - * Example: - * static struct list_head my_list = LIST_HEAD_INIT(my_list); - */ -#define LIST_HEAD_INIT(name) { { &name.n, &name.n } } - -/** - * LIST_HEAD - define and initialize an empty list_head - * @name: the name of the list. - * - * The LIST_HEAD macro defines a list_head and initializes it to an empty - * list. It can be prepended by "static" to define a static list_head. - * - * See also: - * LIST_HEAD_INIT, list_head_init() - * - * Example: - * static LIST_HEAD(my_global_list); - */ -#define LIST_HEAD(name) \ - struct list_head name = LIST_HEAD_INIT(name) - /** * list_head_init - initialize a list_head * @h: the list_head to set to the empty list @@ -386,10 +360,9 @@ static inline void list_del(struct list_node *n) (void)list_debug_node(n); n->next->prev = n->prev; n->prev->next = n->next; -#ifdef _LIST_DEBUG + /* Catch use-after-del. */ - n->next = n->prev = NULL; -#endif + list_poison(n); } /** @@ -408,7 +381,7 @@ static inline void list_del(struct list_node *n) */ static inline void list_del_from(struct list_head *h, struct list_node *n) { -#ifdef _LIST_DEBUG +#ifndef NDEBUG { /* Thorough check: make sure it was in list! */ struct list_node *i; @@ -417,7 +390,7 @@ static inline void list_del_from(struct list_head *h, struct list_node *n) } #else (void)h; -#endif /* _LIST_DEBUG */ +#endif /* NDEBUG */ /* Quick test that catches a surprising number of bugs. */ assert(!list_empty(h)); @@ -635,7 +608,7 @@ static inline void list_prepend_list(struct list_head *to, /** * list_for_each_off - iterate through a list of memory regions. * @h: the list_head - * @i: the pointer to a memory region wich contains list node data. + * @i: the pointer to a memory region which contains list node data. * @off: offset(relative to @i) at which list node data resides. * * This is a low-level wrapper to iterate @i over the entire list, used to @@ -671,7 +644,7 @@ static inline void list_prepend_list(struct list_head *to, * list_for_each_safe_off - iterate through a list of memory regions, maybe * during deletion * @h: the list_head - * @i: the pointer to a memory region wich contains list node data. + * @i: the pointer to a memory region which contains list node data. * @nxt: the structure containing the list_node * @off: offset(relative to @i) at which list node data resides. * diff --git a/src/lib/lwan-arena.c b/src/lib/lwan-arena.c new file mode 100644 index 000000000..30695d4d3 --- /dev/null +++ b/src/lib/lwan-arena.c @@ -0,0 +1,120 @@ +/* + * lwan - web server + * Copyright (c) 2024 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include "lwan-private.h" +#include "lwan-arena.h" + +#if !defined(NDEBUG) && defined(LWAN_HAVE_VALGRIND) +#define INSTRUMENT_FOR_VALGRIND +#include +#include +#endif + +#if defined(__clang__) +# if defined(__has_feature) && __has_feature(address_sanitizer) +# define __SANITIZE_ADDRESS__ +# endif +#endif +#if defined(__SANITIZE_ADDRESS__) +#define INSTRUMENT_FOR_ASAN +void __asan_poison_memory_region(void const volatile *addr, size_t size); +void __asan_unpoison_memory_region(void const volatile *addr, size_t size); +#endif + +void arena_init(struct arena *a) +{ + ptr_array_init(&a->ptrs); + a->bump_ptr_alloc.ptr = NULL; + a->bump_ptr_alloc.remaining = 0; +} + +void arena_reset(struct arena *a) +{ + void **iter; + + LWAN_ARRAY_FOREACH(&a->ptrs, iter) { + free(*iter); + } + + arena_init(a); +} + +void *arena_alloc(struct arena *a, const size_t sz) +{ + const size_t aligned_sz = (sz + sizeof(void *) - 1ul) & ~(sizeof(void *) - 1ul); + + if (a->bump_ptr_alloc.remaining < aligned_sz) { + const size_t alloc_sz = LWAN_MAX((size_t)PAGE_SIZE, aligned_sz); + void *ptr = malloc(alloc_sz); + + if (UNLIKELY(!ptr)) + return NULL; + + void **saved_ptr = ptr_array_append(&a->ptrs); + if (UNLIKELY(!saved_ptr)) { + free(ptr); + return NULL; + } + + *saved_ptr = ptr; + + a->bump_ptr_alloc.ptr = ptr; + a->bump_ptr_alloc.remaining = alloc_sz; + +#if defined(INSTRUMENT_FOR_ASAN) + __asan_poison_memory_region(ptr, alloc_sz); +#endif +#if defined(INSTRUMENT_FOR_VALGRIND) + VALGRIND_MAKE_MEM_NOACCESS(ptr, alloc_sz); +#endif + } + + void *ptr = a->bump_ptr_alloc.ptr; + +#if defined(INSTRUMENT_FOR_VALGRIND) + VALGRIND_MAKE_MEM_UNDEFINED(ptr, sz); +#endif +#if defined(INSTRUMENT_FOR_ASAN) + __asan_unpoison_memory_region(ptr, sz); +#endif + + a->bump_ptr_alloc.remaining -= aligned_sz; + a->bump_ptr_alloc.ptr = (char *)ptr + aligned_sz; + + return ptr; +} + +static void reset_arena(void *data) +{ + struct arena *arena = data; + arena_reset(arena); +} + +struct arena *coro_arena_new(struct coro *coro) +{ + struct arena *arena = coro_malloc(coro, sizeof(*arena)); + + if (LIKELY(arena)) { + arena_init(arena); + coro_defer(coro, reset_arena, arena); + } + + return arena; +} diff --git a/src/lib/lwan-arena.h b/src/lib/lwan-arena.h new file mode 100644 index 000000000..fb9973d10 --- /dev/null +++ b/src/lib/lwan-arena.h @@ -0,0 +1,41 @@ +/* + * lwan - web server + * Copyright (c) 2024 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include "lwan-array.h" +#include "lwan-coro.h" + +DEFINE_ARRAY_TYPE_INLINEFIRST(ptr_array, void *) + +struct arena { + struct ptr_array ptrs; + + struct { + void *ptr; + size_t remaining; + } bump_ptr_alloc; +}; + +void arena_init(struct arena *a); +struct arena *coro_arena_new(struct coro *coro); +void arena_reset(struct arena *a); + +void *arena_alloc(struct arena *a, const size_t sz); diff --git a/src/lib/lwan-array.c b/src/lib/lwan-array.c new file mode 100644 index 000000000..d39ef6158 --- /dev/null +++ b/src/lib/lwan-array.c @@ -0,0 +1,143 @@ +/* + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _DEFAULT_SOURCE +#define _GNU_SOURCE +#include +#include +#include + +#include "lwan.h" +#include "lwan-array.h" + +int lwan_array_reset(struct lwan_array *a, void *inline_storage) +{ + if (UNLIKELY(!a)) + return -EINVAL; + + if (a->base != inline_storage) + free(a->base); + + a->base = NULL; + a->elements = 0; + + return 0; +} + +#if !defined(LWAN_HAVE_BUILTIN_ADD_OVERFLOW) +static inline bool add_overflow(size_t a, size_t b, size_t *out) +{ + if (UNLIKELY(a > 0 && b > SIZE_MAX - a)) + return true; + + *out = a + b; + return false; +} +#else +#define add_overflow __builtin_add_overflow +#endif + +void *lwan_array_append_heap(struct lwan_array *a, size_t element_size) +{ + if (!(a->elements % LWAN_ARRAY_INCREMENT)) { + void *new_base; + size_t new_cap; + + if (UNLIKELY( + add_overflow(a->elements, LWAN_ARRAY_INCREMENT, &new_cap))) { + errno = EOVERFLOW; + return NULL; + } + + new_base = reallocarray(a->base, new_cap, element_size); + if (UNLIKELY(!new_base)) + return NULL; + + a->base = new_base; + } + + return ((char *)a->base) + a->elements++ * element_size; +} + +void *lwan_array_append_inline(struct lwan_array *a, + size_t element_size, + void *inline_storage) +{ + if (!a->base) + a->base = inline_storage; + else if (UNLIKELY(a->base != inline_storage)) + return lwan_array_append_heap(a, element_size); + + assert(a->elements <= LWAN_ARRAY_INCREMENT); + + if (a->elements == LWAN_ARRAY_INCREMENT) { + void *new_base = + reallocarray(NULL, 2 * LWAN_ARRAY_INCREMENT, element_size); + + if (UNLIKELY(!new_base)) + return NULL; + + a->base = memcpy(new_base, inline_storage, + LWAN_ARRAY_INCREMENT * element_size); + } + + return ((char *)a->base) + a->elements++ * element_size; +} + +void lwan_array_sort(struct lwan_array *a, + size_t element_size, + int (*cmp)(const void *a, const void *b)) +{ + if (LIKELY(a->elements)) + qsort(a->base, a->elements, element_size, cmp); +} + +static void coro_lwan_array_free_heap(void *data) +{ + struct lwan_array *array = data; + + lwan_array_reset(array, NULL); + free(array); +} + +static void coro_lwan_array_free_inline(void *data) +{ + struct lwan_array *array = data; + + lwan_array_reset(array, array + 1); + free(array); +} + +struct lwan_array *coro_lwan_array_new(struct coro *coro, + size_t struct_size, + bool inline_first) +{ + struct lwan_array *array; + + assert(struct_size >= sizeof(struct lwan_array)); + + array = coro_malloc_full(coro, struct_size, + inline_first ? coro_lwan_array_free_inline + : coro_lwan_array_free_heap); + if (LIKELY(array)) + *array = (struct lwan_array){.base = NULL, .elements = 0}; + + return array; +} diff --git a/src/lib/lwan-array.h b/src/lib/lwan-array.h new file mode 100644 index 000000000..73344b52a --- /dev/null +++ b/src/lib/lwan-array.h @@ -0,0 +1,148 @@ +/* + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include +#include +#include + +#include "lwan-coro.h" + +#define LWAN_ARRAY_INCREMENT 16 + +struct lwan_array { + void *base; + size_t elements; +}; + +int lwan_array_reset(struct lwan_array *a, void *inline_storage); +void *lwan_array_append_heap(struct lwan_array *a, size_t element_size); +void *lwan_array_append_inline(struct lwan_array *a, + size_t element_size, + void *inline_storage); +void lwan_array_sort(struct lwan_array *a, + size_t element_size, + int (*cmp)(const void *a, const void *b)); +struct lwan_array *coro_lwan_array_new(struct coro *coro, + size_t struct_size, + bool inline_first); + +#define LWAN_ARRAY_FOREACH(array_, iter_) \ + for (iter_ = (array_)->base.base; \ + iter_ < \ + ((typeof(iter_))(array_)->base.base + (array_)->base.elements); \ + iter_++) + +#define LWAN_ARRAY_FOREACH_REVERSE(array_, iter_) \ + if ((array_)->base.elements) \ + for (iter_ = ((typeof(iter_))(array_)->base.base + \ + (array_)->base.elements - 1); \ + iter_ >= (typeof(iter_))(array_)->base.base; iter_--) + +#define DEFINE_ARRAY_TYPE(array_type_, element_type_) \ + struct array_type_ { \ + struct lwan_array base; \ + }; \ + __attribute__((unused)) static inline element_type_ *array_type_##_append( \ + struct array_type_ *array) \ + { \ + return (element_type_ *)lwan_array_append_heap(&array->base, \ + sizeof(element_type_)); \ + } \ + __attribute__((unused)) static inline struct array_type_ \ + *coro_##array_type_##_new(struct coro *coro) \ + { \ + return (struct array_type_ *)coro_lwan_array_new( \ + coro, sizeof(struct array_type_), false); \ + } \ + DEFINE_ARRAY_TYPE_FUNCS(array_type_, element_type_, NULL) + +#define DEFINE_ARRAY_TYPE_INLINEFIRST(array_type_, element_type_) \ + struct array_type_ { \ + struct lwan_array base; \ + element_type_ storage[LWAN_ARRAY_INCREMENT]; \ + }; \ + __attribute__((unused)) static inline element_type_ *array_type_##_append( \ + struct array_type_ *array) \ + { \ + return (element_type_ *)lwan_array_append_inline( \ + &array->base, sizeof(element_type_), &array->storage); \ + } \ + __attribute__((unused)) static inline struct array_type_ \ + *coro_##array_type_##_new(struct coro *coro) \ + { \ + return (struct array_type_ *)coro_lwan_array_new( \ + coro, sizeof(struct array_type_), true); \ + } \ + DEFINE_ARRAY_TYPE_FUNCS(array_type_, element_type_, &array->storage) + +#define DEFINE_ARRAY_TYPE_FUNCS(array_type_, element_type_, inline_storage_) \ + __attribute__((unused)) static inline element_type_ \ + *array_type_##_get_array(struct array_type_ *array) \ + { \ + return (element_type_ *)array->base.base; \ + } \ + __attribute__((unused)) \ + __attribute__((nonnull(1))) static inline void array_type_##_init( \ + struct array_type_ *array) \ + { \ + array->base = (struct lwan_array){.base = NULL, .elements = 0}; \ + } \ + __attribute__((unused)) static inline int array_type_##_reset( \ + struct array_type_ *array) \ + { \ + return lwan_array_reset(&array->base, inline_storage_); \ + } \ + __attribute__((unused)) static inline element_type_ \ + *array_type_##_append0(struct array_type_ *array) \ + { \ + element_type_ *element = array_type_##_append(array); \ + \ + if (element) \ + memset(element, 0, sizeof(*element)); \ + \ + return element; \ + } \ + __attribute__((unused)) static inline void array_type_##_sort( \ + struct array_type_ *array, int (*cmp)(const void *a, const void *b)) \ + { \ + lwan_array_sort(&array->base, sizeof(element_type_), cmp); \ + } \ + __attribute__((unused)) static inline size_t array_type_##_get_elem_index( \ + const struct array_type_ *array, element_type_ *elem) \ + { \ + assert(elem >= (element_type_ *)array->base.base); \ + assert(elem < \ + (element_type_ *)array->base.base + array->base.elements); \ + return (size_t)(elem - (element_type_ *)array->base.base); \ + } \ + __attribute__((unused)) static inline element_type_ \ + *array_type_##_get_elem(const struct array_type_ *array, size_t index) \ + { \ + assert(index <= /* Legal to have a ptr to 1 past end */ \ + array->base.elements); \ + return &((element_type_ *)array->base.base)[index]; \ + } \ + __attribute__((unused)) static inline size_t array_type_##_len( \ + const struct array_type_ *array) \ + { \ + return array->base.elements; \ + } diff --git a/src/lib/lwan-cache.c b/src/lib/lwan-cache.c new file mode 100644 index 000000000..5e5808809 --- /dev/null +++ b/src/lib/lwan-cache.c @@ -0,0 +1,503 @@ +/* + * lwan - web server + * Copyright (c) 2013 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "lwan-cache.h" +#include "hash.h" + +#define GET_AND_REF_TRIES 5 + +enum { + /* Entry flags */ + FLOATING = 1 << 0, + TEMPORARY = 1 << 1, + FREE_KEY_ON_DESTROY = 1 << 2, + EXISTING_ITEM = 1 << 3, + + /* Cache flags */ + SHUTTING_DOWN = 1 << 0, + READ_ONLY = 1 << 1, +}; + +struct cache { + struct { + struct hash *table; + pthread_rwlock_t lock; + } hash; + + struct { + struct list_head list; + pthread_rwlock_t lock; + } queue; + + struct { + cache_create_entry_cb create_entry; + cache_destroy_entry_cb destroy_entry; + void *context; + } cb; + + /* FIXME: I'm not really a fan of how these are set in cache_create(), + * but this has to do for now. */ + struct { + void *(*copy)(const void *); + void (*free)(void *); + } key; + + struct { + time_t time_to_live; + } settings; + + unsigned flags; + +#ifndef NDEBUG + struct { + unsigned hits; + unsigned misses; + unsigned evicted; + } stats; +#endif +}; + +static bool cache_pruner_job(void *data); + +static ALWAYS_INLINE void *identity_key_copy(const void *key) +{ + return (void *)key; +} + +static void identity_key_free(void *key __attribute__((unused))) +{ +} + +static ALWAYS_INLINE void *str_key_copy(const void *key) +{ + return strdup(key); +} + +static ALWAYS_INLINE void str_key_free(void *key) +{ + free(key); +} + +struct cache *cache_create_full(cache_create_entry_cb create_entry_cb, + cache_destroy_entry_cb destroy_entry_cb, + hash_create_func_cb hash_create_func, + void *cb_context, + time_t time_to_live) +{ + struct cache *cache; + + assert(create_entry_cb); + assert(destroy_entry_cb); + assert(time_to_live > 0); + + cache = calloc(1, sizeof(*cache)); + if (!cache) + return NULL; + + if (hash_create_func == hash_str_new) { + cache->hash.table = hash_create_func(free, NULL); + } else { + cache->hash.table = hash_create_func(NULL, NULL); + } + if (!cache->hash.table) + goto error_no_hash; + + if (pthread_rwlock_init(&cache->hash.lock, NULL)) + goto error_no_hash_lock; + if (pthread_rwlock_init(&cache->queue.lock, NULL)) + goto error_no_queue_lock; + + cache->cb.create_entry = create_entry_cb; + cache->cb.destroy_entry = destroy_entry_cb; + cache->cb.context = cb_context; + + if (hash_create_func == hash_str_new) { + cache->key.copy = str_key_copy; + cache->key.free = str_key_free; + } else { + cache->key.copy = identity_key_copy; + cache->key.free = identity_key_free; + } + + cache->settings.time_to_live = time_to_live; + + list_head_init(&cache->queue.list); + + lwan_job_add(cache_pruner_job, cache); + + return cache; + +error_no_queue_lock: + pthread_rwlock_destroy(&cache->hash.lock); +error_no_hash_lock: + hash_unref(cache->hash.table); +error_no_hash: + free(cache); + + return NULL; +} + +struct cache *cache_create(cache_create_entry_cb create_entry_cb, + cache_destroy_entry_cb destroy_entry_cb, + void *cb_context, + time_t time_to_live) +{ + return cache_create_full(create_entry_cb, destroy_entry_cb, hash_str_new, + cb_context, time_to_live); +} + +void cache_destroy(struct cache *cache) +{ + assert(cache); + +#ifndef NDEBUG + lwan_status_debug("Cache stats: %d hits, %d misses, %d evictions", + cache->stats.hits, cache->stats.misses, + cache->stats.evicted); +#endif + + lwan_job_del(cache_pruner_job, cache); + cache->flags |= SHUTTING_DOWN; + cache_pruner_job(cache); + pthread_rwlock_destroy(&cache->hash.lock); + pthread_rwlock_destroy(&cache->queue.lock); + hash_unref(cache->hash.table); + free(cache); +} + +bool cache_entry_is_new(const struct cache_entry *entry) +{ + return !(entry->flags & EXISTING_ITEM); +} + +struct cache_entry *cache_get_and_ref_entry_with_ctx(struct cache *cache, + const void *key, void *create_ctx, + int *error) +{ + struct cache_entry *entry; + char *key_copy; + + assert(cache); + assert(error); +#ifndef NDEBUG + if (cache->key.copy != identity_key_copy) { + assert(key); + } +#endif + + *error = 0; + + if (cache->flags & READ_ONLY) { + entry = hash_find(cache->hash.table, key); + if (LIKELY(entry)) { + entry->flags |= EXISTING_ITEM; +#ifndef NDEBUG + ATOMIC_INC(cache->stats.hits); + } else { + ATOMIC_INC(cache->stats.misses); +#endif + } + return entry; + } + + /* If the lock can't be obtained, return an error to allow, for instance, + * yielding from the coroutine and trying to obtain the lock at a later + * time. */ + if (UNLIKELY(pthread_rwlock_tryrdlock(&cache->hash.lock) == EBUSY)) { + *error = EWOULDBLOCK; + return NULL; + } + entry = hash_find(cache->hash.table, key); + if (LIKELY(entry)) { + ATOMIC_INC(entry->refs); + pthread_rwlock_unlock(&cache->hash.lock); +#ifndef NDEBUG + ATOMIC_INC(cache->stats.hits); +#endif + return entry; + } + + /* No need to keep the hash table lock locked while the item is being created. */ + pthread_rwlock_unlock(&cache->hash.lock); + +#ifndef NDEBUG + ATOMIC_INC(cache->stats.misses); +#endif + + key_copy = cache->key.copy(key); + if (UNLIKELY(!key_copy)) { + if (cache->key.copy != identity_key_copy) { + *error = ENOMEM; + return NULL; + } + } + + entry = cache->cb.create_entry(key, cache->cb.context, create_ctx); + if (UNLIKELY(!entry)) { + *error = ECANCELED; + cache->key.free(key_copy); + return NULL; + } + + *entry = (struct cache_entry) { .key = key_copy, .refs = 1 }; + + if (pthread_rwlock_trywrlock(&cache->hash.lock) == EBUSY) { + /* Couldn't obtain hash write lock: instead of waiting, just return + * the recently-created item as a temporary item. Might result in + * items not being added to the cache, though, so this might be + * changed back to pthread_rwlock_wrlock() again someday if this + * proves to be a problem. */ + entry->flags = TEMPORARY | FREE_KEY_ON_DESTROY; + return entry; + } + + if (!hash_add_unique(cache->hash.table, entry->key, entry)) { + struct timespec now; + + if (UNLIKELY(clock_gettime(monotonic_clock_id, &now) < 0)) + lwan_status_critical("clock_gettime"); + + entry->time_to_expire = now.tv_sec + cache->settings.time_to_live; + + if (LIKELY(!pthread_rwlock_wrlock(&cache->queue.lock))) { + list_add_tail(&cache->queue.list, &entry->entries); + pthread_rwlock_unlock(&cache->queue.lock); + } else { + /* Key is freed when this entry is removed from the hash + * table below. */ + entry->flags = TEMPORARY; + + /* Ensure item is removed from the hash table; otherwise, + * another thread could potentially get another reference + * to this entry and cause an invalid memory access. */ + hash_del(cache->hash.table, entry->key); + } + } else { + /* Either there's another item with the same key (-EEXIST), or + * there was an error inside the hash table. In either case, + * just return a TEMPORARY entry so that it is destroyed the first + * time someone unrefs this entry. TEMPORARY entries are pretty much + * like FLOATING entries, but unreffing them do not use atomic + * operations. */ + entry->flags = TEMPORARY | FREE_KEY_ON_DESTROY; + } + + pthread_rwlock_unlock(&cache->hash.lock); + return entry; +} + +ALWAYS_INLINE struct cache_entry * +cache_get_and_ref_entry(struct cache *cache, const void *key, int *error) +{ + return cache_get_and_ref_entry_with_ctx(cache, key, NULL, error); +} + +void cache_entry_unref(struct cache *cache, struct cache_entry *entry) +{ + assert(entry); + + if (cache->flags & READ_ONLY) + return; + + /* FIXME: There's a race condition in this function: if the cache is + * destroyed while there are either temporary or floating entries, + * calling the destroy_entry callback function will dereference + * deallocated memory. */ + + if (entry->flags & TEMPORARY) { + /* FREE_KEY_ON_DESTROY is set on elements that never got into the + * hash table, so their keys are never destroyed automatically. */ + if (entry->flags & FREE_KEY_ON_DESTROY) + cache->key.free(entry->key); + + return cache->cb.destroy_entry(entry, cache->cb.context); + } + + if (ATOMIC_DEC(entry->refs)) + return; + + /* FLOATING entries without references won't be picked up by the pruner + * job, so destroy them right here. */ + if (entry->flags & FLOATING) { + assert(!(entry->flags & FREE_KEY_ON_DESTROY)); + return cache->cb.destroy_entry(entry, cache->cb.context); + } +} + +static bool cache_pruner_job(void *data) +{ + struct cache *cache = data; + struct cache_entry *node, *next; + struct timespec now; + bool shutting_down = cache->flags & SHUTTING_DOWN; + struct list_head queue; + unsigned int evicted = 0; + + /* This job might start execution as we mark ourselves as read-only, + * and before this job is removed from the job thread. */ + if (cache->flags & READ_ONLY) + return true; + + if (UNLIKELY(pthread_rwlock_trywrlock(&cache->queue.lock) == EBUSY)) + return false; + + /* If the queue is empty, there's nothing to do; unlock/return*/ + if (list_empty(&cache->queue.list)) { + if (UNLIKELY(pthread_rwlock_unlock(&cache->queue.lock))) + lwan_status_perror("pthread_rwlock_unlock"); + return false; + } + + /* There are things to do; work on a local queue so the lock doesn't + * need to be held while items are being pruned. */ + list_head_init(&queue); + list_append_list(&queue, &cache->queue.list); + list_head_init(&cache->queue.list); + + if (UNLIKELY(pthread_rwlock_unlock(&cache->queue.lock))) { + lwan_status_perror("pthread_rwlock_unlock"); + goto end; + } + + if (UNLIKELY(clock_gettime(monotonic_clock_id, &now) < 0)) { + lwan_status_perror("clock_gettime"); + goto end; + } + + list_for_each_safe(&queue, node, next, entries) { + char *key = node->key; + + if (now.tv_sec < node->time_to_expire && LIKELY(!shutting_down)) + break; + + list_del(&node->entries); + + if (UNLIKELY(pthread_rwlock_wrlock(&cache->hash.lock))) { + lwan_status_perror("pthread_rwlock_wrlock"); + continue; + } + + hash_del(cache->hash.table, key); + + if (UNLIKELY(pthread_rwlock_unlock(&cache->hash.lock))) + lwan_status_perror("pthread_rwlock_unlock"); + + if (ATOMIC_INC(node->refs) == 1) { + /* If the refcount was 0, and turned 1 after the increment, it means the item can + * be destroyed here. */ + cache->cb.destroy_entry(node, cache->cb.context); + } else { + /* If not, some other thread had references to this object. */ + ATOMIC_OP(&node->flags, or, FLOATING); + /* If in the time between the ref check above and setting the floating flag the + * thread holding the reference drops it, if our reference is 0 after dropping it, + * the pruner thread was the last thread holding the reference to this entry, so + * it's safe to destroy it at this point. */ + if (!ATOMIC_DEC(node->refs)) + cache->cb.destroy_entry(node, cache->cb.context); + } + + evicted++; + } + + /* If local queue has been entirely processed, there's no need to + * append items in the cache queue to it; just update statistics and + * return */ + if (list_empty(&queue)) + goto end; + + /* Prepend local, unprocessed queue, to the cache queue. Since the cache + * item TTL is constant, items created later will be destroyed later. */ + if (LIKELY(!pthread_rwlock_wrlock(&cache->queue.lock))) { + list_prepend_list(&cache->queue.list, &queue); + pthread_rwlock_unlock(&cache->queue.lock); + } else { + lwan_status_perror("pthread_rwlock_wrlock"); + } + +end: +#ifndef NDEBUG + ATOMIC_AAF(&cache->stats.evicted, evicted); +#endif + return evicted; +} + +static void cache_entry_unref_defer(void *data1, void *data2) +{ + cache_entry_unref((struct cache *)data1, (struct cache_entry *)data2); +} + +struct cache_entry *cache_coro_get_and_ref_entry_with_ctx(struct cache *cache, + struct coro *coro, + const void *key, + void *create_ctx) +{ + /* If a cache is read-only, cache_get_and_ref_entry() should be + * used directly. */ + assert(!(cache->flags & READ_ONLY)); + + for (int tries = GET_AND_REF_TRIES; tries; tries--) { + int error; + struct cache_entry *ce = + cache_get_and_ref_entry_with_ctx(cache, key, create_ctx, &error); + + if (LIKELY(ce)) { + /* + * This is deferred here so that, if the coroutine is killed + * after it has been yielded, this cache entry is properly + * freed. + */ + coro_defer2(coro, cache_entry_unref_defer, cache, ce); + return ce; + } + + if (error != EWOULDBLOCK) + break; + + /* If the cache would block while reading its hash table, yield and + * try again. (This yields "want-write" because otherwise this + * worker thread might never be resumed again; it's not always that + * a socket can be read from, but you can always write to it.) */ + coro_yield(coro, CONN_CORO_WANT_WRITE); + } + + return NULL; +} + +ALWAYS_INLINE struct cache_entry *cache_coro_get_and_ref_entry( + struct cache *cache, struct coro *coro, const void *key) +{ + return cache_coro_get_and_ref_entry_with_ctx(cache, coro, key, NULL); +} + +void cache_make_read_only(struct cache *cache) +{ + cache->flags |= READ_ONLY; + lwan_job_del(cache_pruner_job, cache); +} diff --git a/src/lib/lwan-cache.h b/src/lib/lwan-cache.h new file mode 100644 index 000000000..169a469e1 --- /dev/null +++ b/src/lib/lwan-cache.h @@ -0,0 +1,67 @@ +/* + * lwan - web server + * Copyright (c) 2013 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +#include "list.h" +#include "lwan-coro.h" + +struct cache_entry { + struct list_node entries; + char *key; + int refs; + unsigned flags; + time_t time_to_expire; +}; + +typedef struct cache_entry *(*cache_create_entry_cb)(const void *key, + void *cache_ctx, + void *create_ctx); +typedef void (*cache_destroy_entry_cb)(struct cache_entry *entry, + void *cache_ctx); +typedef struct hash *(*hash_create_func_cb)(void (*)(void *), void (*)(void *)); + +struct cache; + +struct cache *cache_create(cache_create_entry_cb create_entry_cb, + cache_destroy_entry_cb destroy_entry_cb, + void *cb_context, + time_t time_to_live); +struct cache *cache_create_full(cache_create_entry_cb create_entry_cb, + cache_destroy_entry_cb destroy_entry_cb, + struct hash *(*hash_create_func)(void (*)(void *), void (*)(void *)), + void *cb_context, + time_t time_to_live); +void cache_destroy(struct cache *cache); + +struct cache_entry *cache_get_and_ref_entry(struct cache *cache, + const void *key, int *error); +struct cache_entry *cache_get_and_ref_entry_with_ctx(struct cache *cache, + const void *key, void *create_ctx, int *error); +struct cache_entry *cache_coro_get_and_ref_entry(struct cache *cache, + struct coro *coro, const void *key); +struct cache_entry *cache_coro_get_and_ref_entry_with_ctx(struct cache *cache, + struct coro *coro, const void *key, void *create_ctx); +void cache_entry_unref(struct cache *cache, struct cache_entry *entry); + +void cache_make_read_only(struct cache *cache); + +bool cache_entry_is_new(const struct cache_entry *entry); diff --git a/src/lib/lwan-config.c b/src/lib/lwan-config.c new file mode 100644 index 000000000..14c8ffa63 --- /dev/null +++ b/src/lib/lwan-config.c @@ -0,0 +1,1061 @@ +/* + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" +#include "lwan-status.h" +#include "lwan-config.h" +#include "lwan-strbuf.h" + +#include "ringbuffer.h" + +#define LEX_ERROR(lexer, fmt, ...) \ + ({ \ + config_error(config_from_lexer(lexer), "%s" fmt, \ + "Syntax error: ", ##__VA_ARGS__); \ + NULL; \ + }) + +#define PARSER_ERROR(parser, fmt, ...) \ + ({ \ + config_error(config_from_parser(parser), "%s" fmt, \ + "Parsing error: ", ##__VA_ARGS__); \ + NULL; \ + }) + +#define INTERNAL_ERROR(parser, fmt, ...) \ + ({ \ + config_error(config_from_parser(parser), "%s" fmt, \ + "Internal error: ", ##__VA_ARGS__); \ + NULL; \ + }) + +#define FOR_EACH_LEXEME(X) \ + X(STRING) X(EQUAL) X(OPEN_BRACKET) X(CLOSE_BRACKET) X(LINEFEED) X(VARIABLE) \ + X(VARIABLE_DEFAULT) X(EOF) + +#define GENERATE_ENUM(id) LEXEME_ ## id, + +enum lexeme_type { + FOR_EACH_LEXEME(GENERATE_ENUM) + TOTAL_LEXEMES +}; + +#undef GENERATE_ENUM + +struct lexeme { + enum lexeme_type type; + struct lwan_value value; +}; + +DEFINE_RING_BUFFER_TYPE(lexeme_ring_buffer, struct lexeme, 4) +DEFINE_RING_BUFFER_TYPE(config_ring_buffer, struct config_line, 4) + +struct lexer { + void *(*state)(struct lexer *); + const char *start, *pos, *end; + struct lexeme_ring_buffer buffer; + int cur_line; +}; + +struct parser { + void *(*state)(struct parser *); + struct lexer lexer; + struct lexeme_ring_buffer buffer; + struct config_ring_buffer items; + struct lwan_strbuf strbuf; +}; + +struct config { + struct parser parser; + char *error_message; + struct hash *constants; + struct { + void *addr; + size_t sz; + } mapped; + int opened_brackets; +}; + +unsigned int parse_time_period(const char *str, unsigned int default_value) +{ + unsigned int total = 0; + unsigned int period; + int ignored_spaces = 0; + char multiplier; + + if (!str) + return default_value; + + while (*str) { + /* This check is necessary to avoid making sscanf() take an incredible + * amount of time while trying to scan the input for a number. Fix for + * https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=44910 */ + if (isspace(*str)) { + ignored_spaces++; + str++; + + if (ignored_spaces > 1024) + return default_value; + + continue; + } + + if (sscanf(str, "%u%c", &period, &multiplier) != 2) + break; + + switch (multiplier) { + case 's': total += period; break; + case 'm': total += period * ONE_MINUTE; break; + case 'h': total += period * ONE_HOUR; break; + case 'd': total += period * ONE_DAY; break; + case 'w': total += period * ONE_WEEK; break; + case 'M': total += period * ONE_MONTH; break; + case 'y': total += period * ONE_YEAR; break; + default: + lwan_status_warning("Ignoring unknown multiplier: %c", + multiplier); + } + + str = strchr(str, multiplier) + 1; + } + + return total ? total : default_value; +} + +long long parse_long_long(const char *value, long long default_value) +{ + char *endptr; + long long parsed; + + if (!value) + return default_value; + + errno = 0; + parsed = strtoll(value, &endptr, 0); + + if (errno != 0) + return default_value; + + if (*endptr != '\0' || value == endptr) + return default_value; + + return parsed; +} + +long parse_long(const char *value, long default_value) +{ + long long long_long_value = parse_long_long(value, default_value); + + if ((long long)(long)long_long_value != long_long_value) + return default_value; + + return (long)long_long_value; +} + +int parse_int(const char *value, int default_value) +{ + long long_value = parse_long(value, default_value); + + if ((long)(int)long_value != long_value) + return default_value; + + return (int)long_value; +} + +bool parse_bool(const char *value, bool default_value) +{ + if (!value) + return default_value; + + if (strcaseequal_neutral(value, "true") || + strcaseequal_neutral(value, "on") || strcaseequal_neutral(value, "yes")) + return true; + + if (strcaseequal_neutral(value, "false") || + strcaseequal_neutral(value, "off") || strcaseequal_neutral(value, "no")) + return false; + + return parse_int(value, default_value); +} + +LWAN_SELF_TEST(parse_bool) +{ + assert(parse_bool("true", false) == true); + assert(parse_bool("on", false) == true); + assert(parse_bool("yes", false) == true); + + assert(parse_bool("false", true) == false); + assert(parse_bool("off", true) == false); + assert(parse_bool("no", true) == false); + + assert(parse_bool("0", 1) == false); + assert(parse_bool("1", 0) == true); + + assert(parse_bool("abacate", true) == true); + assert(parse_bool("abacate", false) == false); +} + +bool config_error(struct config *conf, const char *fmt, ...) +{ + va_list values; + int len; + char *output; + + if (conf->error_message) + return false; + + va_start(values, fmt); + len = vasprintf(&output, fmt, values); + va_end(values); + + if (len >= 0) { + conf->error_message = output; + return true; + } + + conf->error_message = NULL; + return false; +} + +static void emit_lexeme(struct lexer *lexer, struct lexeme *lexeme) +{ + if (lexeme_ring_buffer_try_put(&lexer->buffer, lexeme)) + lexer->start = lexer->pos; +} + +static size_t current_len(struct lexer *lexer) +{ + return (size_t)(lexer->pos - lexer->start); +} + +static void emit(struct lexer *lexer, enum lexeme_type type) +{ + struct lexeme lexeme = { + .type = type, + .value = {.value = (char *)lexer->start, .len = current_len(lexer)}, + }; + emit_lexeme(lexer, &lexeme); +} + +static int next(struct lexer *lexer) +{ + if (lexer->pos >= lexer->end) { + lexer->pos = lexer->end + 1; + return '\0'; + } + + int r = *lexer->pos; + lexer->pos++; + + if (r == '\n') + lexer->cur_line++; + + return r; +} + +static void ignore(struct lexer *lexer) +{ + lexer->start = lexer->pos; +} + +static void advance_n(struct lexer *lexer, size_t n) +{ + lexer->pos += n; + ignore(lexer); +} + +static void backup(struct lexer *lexer) +{ + lexer->pos--; + + if (*lexer->pos == '\n') + lexer->cur_line--; +} + +static int peek(struct lexer *lexer) +{ + int chr = next(lexer); + + backup(lexer); + + return chr; +} + +static size_t remaining(struct lexer *lexer) +{ + return (size_t)(lexer->end - lexer->pos); +} + +static void *lex_config(struct lexer *lexer); +static void *lex_variable(struct lexer *lexer); + +static bool is_string(int chr) +{ + return chr && !isspace(chr) && chr != '=' && chr != '#' && chr != '{' && chr != '}'; +} + +static void *lex_string(struct lexer *lexer) +{ + int chr; + + do { + chr = next(lexer); + + if (chr == '$' && peek(lexer) == '{') { + backup(lexer); + emit(lexer, LEXEME_STRING); + + advance_n(lexer, strlen("{")); + + return lex_variable; + } + } while (is_string(chr)); + + backup(lexer); + emit(lexer, LEXEME_STRING); + + return lex_config; +} + +static struct config *config_from_parser(struct parser *parser) +{ + return container_of(parser, struct config, parser); +} + +static struct config *config_from_lexer(struct lexer *lexer) +{ + struct parser *parser = container_of(lexer, struct parser, lexer); + + return config_from_parser(parser); +} + +static bool lex_streq(struct lexer *lexer, const char *str, size_t s) +{ + if (remaining(lexer) < s) + return false; + + return !strncmp(lexer->pos, str, s); +} + +static void *lex_multiline_string(struct lexer *lexer) +{ + const char *end = (peek(lexer) == '"') ? "\"\"\"" : "'''"; + + advance_n(lexer, strlen("'''") - 1); + + do { + if (lex_streq(lexer, end, 3)) { + emit(lexer, LEXEME_STRING); + lexer->pos += 3; + + return lex_config; + } + } while (next(lexer) != '\0'); + + return LEX_ERROR(lexer, "EOF while scanning multiline string"); +} + +static bool is_variable(int chr) +{ + return isalpha(chr) || chr == '_'; +} + +static void *lex_variable_default(struct lexer *lexer) +{ + int chr; + + do { + chr = next(lexer); + + if (chr == '}') { + backup(lexer); + emit(lexer, LEXEME_STRING); + + advance_n(lexer, strlen("}")); + + return lex_config; + } + } while (chr != '\0'); + + return LEX_ERROR(lexer, "EOF while scanning for default value for variable"); +} + +static void *lex_variable(struct lexer *lexer) +{ + int chr; + + advance_n(lexer, strlen("${") - 1); + + do { + chr = next(lexer); + + if (chr == ':') { + backup(lexer); + + if (!current_len(lexer)) + return LEX_ERROR(lexer, "Expecting environment variable name"); + + emit(lexer, LEXEME_VARIABLE_DEFAULT); + advance_n(lexer, strlen(":")); + return lex_variable_default; + } + + if (chr == '}') { + backup(lexer); + + if (!current_len(lexer)) + return LEX_ERROR(lexer, "Expecting environment variable name"); + + emit(lexer, LEXEME_VARIABLE); + advance_n(lexer, strlen("}")); + + return lex_config; + } + } while (is_variable(chr)); + + return LEX_ERROR(lexer, "EOF while scanning for end of variable"); +} + +static bool is_comment(int chr) +{ + return chr != '\0' && chr != '\n'; +} + +static void *lex_comment(struct lexer *lexer) +{ + while (is_comment(next(lexer))) + ; + backup(lexer); + return lex_config; +} + +static void *lex_config(struct lexer *lexer) +{ + while (true) { + int chr = next(lexer); + + if (chr == '\0') + break; + + if (chr == '\n') { + emit(lexer, LEXEME_LINEFEED); + return lex_config; + } + + if (isspace(chr)) { + ignore(lexer); + continue; + } + + if (chr == '{') { + emit(lexer, LEXEME_OPEN_BRACKET); + return lex_config; + } + + if (chr == '}') { + /* Emitting a linefeed lexeme before a close bracket lexeme + * simplifies the parser and allows for situations where a + * section is closed when declaring a key/value pair + * (e.g. "section{key=value}" all in a single line). + */ + emit(lexer, LEXEME_LINEFEED); + emit(lexer, LEXEME_CLOSE_BRACKET); + return lex_config; + } + + if (chr == '=') { + emit(lexer, LEXEME_EQUAL); + return lex_config; + } + + if (chr == '#') + return lex_comment; + + if (chr == '\'' && lex_streq(lexer, "''", 2)) + return lex_multiline_string; + if (chr == '"' && lex_streq(lexer, "\"\"", 2)) + return lex_multiline_string; + + if (chr == '$' && peek(lexer) == '{') + return lex_variable; + + if (is_string(chr)) + return lex_string; + + return LEX_ERROR(lexer, "Invalid character: '%c'", chr); + } + + emit(lexer, LEXEME_LINEFEED); + emit(lexer, LEXEME_EOF); + + return NULL; +} + +static const struct lexeme *lex_next(struct lexer *lexer) +{ + while (lexer->state) { + const struct lexeme *lexeme; + + if ((lexeme = lexeme_ring_buffer_get_ptr_or_null(&lexer->buffer))) + return lexeme; + + lexer->state = lexer->state(lexer); + } + + return lexeme_ring_buffer_get_ptr_or_null(&lexer->buffer); +} + +static void *parse_config(struct parser *parser); +static void *parse_section_end(struct parser *parser); + +#define ENV_VAR_NAME_LEN_MAX 64 + +static __attribute__((noinline)) const char * +get_constant(struct parser *parser, const char *key, size_t len) +{ + if (UNLIKELY(len > ENV_VAR_NAME_LEN_MAX)) { + return PARSER_ERROR(parser, "Variable name \"%.*s\" exceeds %d bytes", + (int)len, key, ENV_VAR_NAME_LEN_MAX); + } + + const char *key_copy = strndupa(key, len); + const char *value = + hash_find(config_from_parser(parser)->constants, key_copy); + return value ? value : secure_getenv(key_copy); +} + +static void *parse_key_value(struct parser *parser) +{ + struct config_line line = {.type = CONFIG_LINE_TYPE_LINE}; + const struct lexeme *lexeme; + enum lexeme_type last_lexeme = TOTAL_LEXEMES; + size_t key_size; + + while ((lexeme = lexeme_ring_buffer_get_ptr_or_null(&parser->buffer))) { + if (lexeme->type != LEXEME_STRING) + return PARSER_ERROR(parser, "Expecting string"); + + lwan_strbuf_append_value(&parser->strbuf, &lexeme->value); + + if (!lexeme_ring_buffer_empty(&parser->buffer)) + lwan_strbuf_append_char(&parser->strbuf, '_'); + } + key_size = lwan_strbuf_get_length(&parser->strbuf); + lwan_strbuf_append_char(&parser->strbuf, '\0'); + + while ((lexeme = lex_next(&parser->lexer))) { + if (UNLIKELY(lwan_strbuf_get_length(&parser->strbuf) > 1ull<<20)) { + /* 1MiB is more than sufficient for a value. (Without this validation + * here, fuzzers often generates values that are gigabite sized.) */ + return PARSER_ERROR(parser, "Value too long"); + } + + switch (lexeme->type) { + case LEXEME_VARIABLE: { + const char *value = + get_constant(parser, lexeme->value.value, lexeme->value.len); + if (!value) { + return PARSER_ERROR( + parser, + "Variable '$%.*s' not defined in a constants section " + "or as an environment variable", + (int)lexeme->value.len, lexeme->value.value); + } + + lwan_strbuf_append_strz(&parser->strbuf, value); + + break; + } + + case LEXEME_VARIABLE_DEFAULT: { + const char *value = + get_constant(parser, lexeme->value.value, lexeme->value.len); + const struct lexeme *var_name = lexeme; + + if (!(lexeme = lex_next(&parser->lexer))) { + return PARSER_ERROR( + parser, "Default value for constant '$%.*s' not given", + (int)var_name->value.len, var_name->value.value); + } + + if (lexeme->type != LEXEME_STRING) + return PARSER_ERROR(parser, "Wrong format for default value"); + + if (!value) { + lwan_status_debug( + "Using default value of '%.*s' for variable '${%.*s}'", + (int)lexeme->value.len, lexeme->value.value, + (int)var_name->value.len, var_name->value.value); + lwan_strbuf_append_value(&parser->strbuf, &lexeme->value); + } else { + lwan_strbuf_append_strz(&parser->strbuf, value); + } + + break; + } + + case LEXEME_EQUAL: + lwan_strbuf_append_char(&parser->strbuf, '='); + break; + + case LEXEME_STRING: + if (last_lexeme == LEXEME_STRING) + lwan_strbuf_append_char(&parser->strbuf, ' '); + + lwan_strbuf_append_value(&parser->strbuf, &lexeme->value); + + break; + + case LEXEME_LINEFEED: + line.key = lwan_strbuf_get_buffer(&parser->strbuf); + line.value = line.key + key_size + 1; + + if (config_ring_buffer_try_put(&parser->items, &line)) + return parse_config; + + return PARSER_ERROR(parser, + "Could not add key/value to ring buffer"); + + case LEXEME_OPEN_BRACKET: + return PARSER_ERROR(parser, "Open bracket not expected here"); + + case LEXEME_CLOSE_BRACKET: + return INTERNAL_ERROR( + parser, "Close bracket found while parsing key/value"); + + case LEXEME_EOF: + return INTERNAL_ERROR( + parser, "EOF found while parsing key/value"); + + case TOTAL_LEXEMES: + __builtin_unreachable(); + } + + last_lexeme = lexeme->type; + } + + return PARSER_ERROR(parser, "EOF while parsing key-value"); +} + +static void *parse_section(struct parser *parser) +{ + const struct lexeme *lexeme; + size_t name_len; + + lexeme = lexeme_ring_buffer_get_ptr_or_null(&parser->buffer); + if (!lexeme || lexeme->type != LEXEME_STRING) + return PARSER_ERROR(parser, "Expecting a string"); + + lwan_strbuf_append_value(&parser->strbuf, &lexeme->value); + name_len = lexeme->value.len; + lwan_strbuf_append_char(&parser->strbuf, '\0'); + + while ((lexeme = lexeme_ring_buffer_get_ptr_or_null(&parser->buffer))) { + if (lexeme->type != LEXEME_STRING) + return PARSER_ERROR(parser, "Expecting a string"); + + lwan_strbuf_append_value(&parser->strbuf, &lexeme->value); + + if (!lexeme_ring_buffer_empty(&parser->buffer)) + lwan_strbuf_append_char(&parser->strbuf, ' '); + } + + struct config_line line = { + .type = CONFIG_LINE_TYPE_SECTION, + .key = lwan_strbuf_get_buffer(&parser->strbuf), + .value = lwan_strbuf_get_buffer(&parser->strbuf) + name_len + 1, + }; + return config_ring_buffer_try_put(&parser->items, &line) ? parse_config + : NULL; +} + +static void *parse_section_shorthand(struct parser *parser) +{ + void *next_state = parse_section(parser); + + if (next_state) { + struct config_line line = {.type = CONFIG_LINE_TYPE_SECTION_END}; + + if (config_ring_buffer_try_put(&parser->items, &line)) + return next_state; + + return INTERNAL_ERROR(parser, "couldn't append line to internal ring buffer"); + } + + return NULL; +} + +static void *parse_section_end(struct parser *parser) +{ + struct config_line line = {.type = CONFIG_LINE_TYPE_SECTION_END}; + struct config *config = config_from_parser(parser); + + if (!config->opened_brackets) + return PARSER_ERROR(parser, "Section closed before it opened"); + + if (!lexeme_ring_buffer_empty(&parser->buffer)) + return PARSER_ERROR(parser, "Not expecting a close bracket here"); + + if (!config_ring_buffer_try_put(&parser->items, &line)) { + return INTERNAL_ERROR(parser, + "could not store section end in ring buffer"); + } + + config->opened_brackets--; + + return parse_config; +} + +static void *parse_config(struct parser *parser) +{ + const struct lexeme *lexeme = lex_next(&parser->lexer); + + if (!lexeme) { + /* EOF is signaled by a LEXEME_EOF from the parser, so + * this should never happen. */ + return INTERNAL_ERROR(parser, "could not obtain lexeme"); + } + + switch (lexeme->type) { + case LEXEME_EQUAL: + if (lexeme_ring_buffer_empty(&parser->buffer)) + return PARSER_ERROR(parser, "Keys can´t be empty"); + + return parse_key_value; + + case LEXEME_OPEN_BRACKET: + if (lexeme_ring_buffer_empty(&parser->buffer)) + return PARSER_ERROR(parser, "Section names can´t be empty"); + + config_from_parser(parser)->opened_brackets++; + + return parse_section; + + case LEXEME_LINEFEED: + if (!lexeme_ring_buffer_empty(&parser->buffer)) + return parse_section_shorthand; + + return parse_config; + + case LEXEME_STRING: + if (!lexeme_ring_buffer_try_put(&parser->buffer, lexeme)) + return INTERNAL_ERROR(parser, "could not store string in ring buffer"); + + return parse_config; + + case LEXEME_CLOSE_BRACKET: + return parse_section_end; + + case LEXEME_EOF: + if (config_from_parser(parser)->opened_brackets) + return PARSER_ERROR(parser, "EOF while looking for a close bracket"); + + if (!lexeme_ring_buffer_empty(&parser->buffer)) + return INTERNAL_ERROR(parser, "premature EOF"); + + break; + + case LEXEME_VARIABLE: + case LEXEME_VARIABLE_DEFAULT: + return PARSER_ERROR(parser, "Variable '%.*s' can't be used here", + (int)lexeme->value.len, lexeme->value.value); + + case TOTAL_LEXEMES: + __builtin_unreachable(); + } + + return NULL; +} + +static const struct config_line *parser_next_internal(struct parser *parser) +{ + while (parser->state) { + const struct config_line *line; + + if ((line = config_ring_buffer_get_ptr_or_null(&parser->items))) + return line; + + lwan_strbuf_reset(&parser->strbuf); + + parser->state = parser->state(parser); + } + + return config_ring_buffer_get_ptr_or_null(&parser->items); +} + +static bool parse_constants(struct config *config, const struct config_line *l) +{ + while ((l = config_read_line(config))) { + switch (l->type) { + case CONFIG_LINE_TYPE_LINE: { + char *k = strdup(l->key); + char *v = strdup(l->value); + + if (!k || !v) + lwan_status_critical("Can't allocate memory for constant"); + + hash_add(config->constants, k, v); + break; + } + + case CONFIG_LINE_TYPE_SECTION_END: + return true; + + case CONFIG_LINE_TYPE_SECTION: + config_error(config, "Constants section can't be nested"); + return false; + } + } + + return true; +} + +static const struct config_line *parser_next(struct parser *parser) +{ + while (true) { + const struct config_line *l = parser_next_internal(parser); + + if (!l) + return NULL; + + if (l->type == CONFIG_LINE_TYPE_SECTION && streq(l->key, "constants") && + config_from_parser(parser)->opened_brackets == 1) { + struct config *config = config_from_parser(parser); + + if (parse_constants(config, l)) + continue; + + return PARSER_ERROR(parser, "Could not parse constants section: %s", + config_last_error(config)); + } + + return l; + } +} + +static struct config * +config_open_path(const char *path, void **data, size_t *size) +{ + struct config *config; + struct stat st; + void *mapped; + int fd; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + lwan_status_perror("Could not open configuration file: %s", path); + return NULL; + } + + if (fstat(fd, &st) < 0) { + close(fd); + return NULL; + } + + mapped = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (mapped == MAP_FAILED) + return NULL; + + config = malloc(sizeof(*config)); + if (!config) { + munmap(mapped, (size_t)st.st_size); + return NULL; + } + + *data = config->mapped.addr = mapped; + *size = config->mapped.sz = (size_t)st.st_size; + + return config; +} + +static struct config * +config_init_data(struct config *config, const void *data, size_t len) +{ + config->parser = (struct parser){ + .state = parse_config, + .lexer = + { + .state = lex_config, + .pos = data, + .start = data, + .end = (char *)data + len, + .cur_line = 1, + }, + }; + + config->error_message = NULL; + config->opened_brackets = 0; + + config->constants = hash_str_new(free, free); + + lwan_strbuf_init(&config->parser.strbuf); + config_ring_buffer_init(&config->parser.items); + lexeme_ring_buffer_init(&config->parser.buffer); + + return config; +} + +struct config *config_open(const char *path) +{ + struct config *config; + void *data; + size_t len; + + config = config_open_path(path, &data, &len); + return config ? config_init_data(config, data, len) : NULL; +} + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) +struct config *config_open_for_fuzzing(const uint8_t *data, size_t len) +{ + struct config *config = malloc(sizeof(*config)); + + if (config) { + config->mapped.addr = NULL; + config->mapped.sz = 0; + + return config_init_data(config, data, len - 1); + } + + return NULL; +} +#endif + +void config_close(struct config *config) +{ + if (!config) + return; + +#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + if (config->mapped.addr) + munmap(config->mapped.addr, config->mapped.sz); +#endif + + hash_unref(config->constants); + + free(config->error_message); + lwan_strbuf_free(&config->parser.strbuf); + free(config); +} + +const struct config_line *config_read_line(struct config *conf) +{ + return conf->error_message ? NULL : parser_next(&conf->parser); +} + +static bool find_section_end(struct config *config) +{ + const struct config_line *line; + int cur_level = 1; + + if (config->error_message) + return false; + + while ((line = parser_next(&config->parser))) { + if (line->type == CONFIG_LINE_TYPE_SECTION) { + cur_level++; + } else if (line->type == CONFIG_LINE_TYPE_SECTION_END) { + cur_level--; + + if (!cur_level) + return true; + } + } + + return false; +} + +struct config *config_isolate_section(struct config *current_conf, + const struct config_line *current_line) +{ + struct lexer *lexer; + struct config *isolated; + const char *pos; + + if (current_line->type != CONFIG_LINE_TYPE_SECTION) + return NULL; + + isolated = malloc(sizeof(*isolated)); + if (!isolated) + return NULL; + + memcpy(isolated, current_conf, sizeof(*isolated)); + lwan_strbuf_init(&isolated->parser.strbuf); + + isolated->constants = hash_ref(current_conf->constants); + + isolated->mapped.addr = NULL; + isolated->mapped.sz = 0; + /* Keep opened_brackets from the original */ + + lexer = &isolated->parser.lexer; + lexer->start = lexer->pos; + + pos = isolated->parser.lexer.pos; + if (!find_section_end(isolated)) { + config_error(current_conf, + "Could not find section end while trying to isolate: %s", + config_last_error(isolated)); + + hash_unref(isolated->constants); + lwan_strbuf_free(&isolated->parser.strbuf); + free(isolated); + + return NULL; + } + + lexer->end = lexer->pos; + lexer->start = lexer->pos = pos; + + return isolated; +} + +bool config_skip_section(struct config *conf, const struct config_line *line) +{ + if (line->type != CONFIG_LINE_TYPE_SECTION) + return false; + + return find_section_end(conf); +} + +const char *config_last_error(struct config *conf) +{ + return conf->error_message; +} + +int config_cur_line(struct config *conf) +{ + return conf->parser.lexer.cur_line; +} diff --git a/common/lwan-config.h b/src/lib/lwan-config.h similarity index 54% rename from common/lwan-config.h rename to src/lib/lwan-config.h index 0942dd8fd..8a6584beb 100644 --- a/common/lwan-config.h +++ b/src/lib/lwan-config.h @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2013 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2013 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -19,6 +19,10 @@ #pragma once +#if defined(__cplusplus) +extern "C" { +#endif + #define ONE_MINUTE 60 #define ONE_HOUR (ONE_MINUTE * 60) #define ONE_DAY (ONE_HOUR * 24) @@ -29,47 +33,47 @@ #include #include -typedef struct config_t_ config_t; -typedef struct config_line_t_ config_line_t; +#include "lwan-strbuf.h" -typedef enum { +enum config_line_type { CONFIG_LINE_TYPE_LINE, CONFIG_LINE_TYPE_SECTION, CONFIG_LINE_TYPE_SECTION_END -} config_line_type_t; - -struct config_t_ { - FILE *file; - char *path, *error_message; - struct { - long end; - } isolated; - int line; }; -struct config_line_t_ { - union { - struct { - char *name, *param; - } section; - struct { - char *key, *value; - } line; - }; - config_line_type_t type; - char buffer[1024]; +struct config_line { + char *key; + char *value; + + enum config_line_type type; }; -bool config_open(config_t *conf, const char *path); -void config_close(config_t *conf); -bool config_error(config_t *conf, const char *fmt, ...); -bool config_read_line(config_t *conf, config_line_t *l); +struct config; + +struct config *config_open(const char *path); +void config_close(struct config *conf); +bool config_error(struct config *conf, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +const struct config_line *config_read_line(struct config *conf); -bool config_isolate_section(config_t *current_conf, - config_line_t *current_line, config_t *isolated); -bool config_skip_section(config_t *conf, config_line_t *line); +const char *config_last_error(struct config *conf); +int config_cur_line(struct config *conf); + +struct config *config_isolate_section(struct config *current_conf, + const struct config_line *current_line); +bool config_skip_section(struct config *conf, const struct config_line *line); bool parse_bool(const char *value, bool default_value); long parse_long(const char *value, long default_value); +long long parse_long_long(const char *value, long long default_value); int parse_int(const char *value, int default_value); unsigned int parse_time_period(const char *str, unsigned int default_value); + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) +#include +struct config *config_open_for_fuzzing(const uint8_t *data, size_t len); +#endif + +#if defined(__cplusplus) +} +#endif diff --git a/src/lib/lwan-coro.c b/src/lib/lwan-coro.c new file mode 100644 index 000000000..2a88c680c --- /dev/null +++ b/src/lib/lwan-coro.c @@ -0,0 +1,529 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "lwan-arena.h" +#include "lwan-array.h" +#include "lwan-coro.h" + +#if !defined(NDEBUG) && defined(LWAN_HAVE_VALGRIND) +#define INSTRUMENT_FOR_VALGRIND +#include +#include +#endif + +#if !defined(SIGSTKSZ) +#define SIGSTKSZ 16384 +#endif + +#ifdef LWAN_HAVE_BROTLI +#define MIN_CORO_STACK_SIZE ((size_t)(8 * SIGSTKSZ)) +#else +#define MIN_CORO_STACK_SIZE ((size_t)(4 * SIGSTKSZ)) +#endif + +#define CORO_STACK_SIZE ((MIN_CORO_STACK_SIZE + (size_t)PAGE_SIZE) & ~((size_t)PAGE_SIZE)) + +#if (!defined(NDEBUG) && defined(MAP_STACK)) || defined(__OpenBSD__) +/* As an exploit mitigation, OpenBSD requires any stacks to be allocated via + * mmap(... MAP_STACK ...). + * + * Also enable this on debug builds to catch stack overflows while testing + * (MAP_STACK exists in Linux, but it's a no-op). */ +#define ALLOCATE_STACK_WITH_MMAP +#endif + +LWAN_SELF_TEST(sizes_are_same) +{ + /* This is done in runtime rather than during compilation time because + * in Glibc >= 2.34, SIGSTKSZ is defined as sysconf(_SC_MINSIGSTKSZ). */ + + /* Request buffer fits inside coroutine stack */ + assert(DEFAULT_BUFFER_SIZE < CORO_STACK_SIZE); +} + +typedef void (*defer1_func)(void *data); +typedef void (*defer2_func)(void *data1, void *data2); + +struct coro_defer { + union { + struct { + defer1_func func; + void *data; + } one; + struct { + defer2_func func; + void *data1; + void *data2; + } two; + }; + bool has_two_args; +}; + +DEFINE_ARRAY_TYPE_INLINEFIRST(coro_defer_array, struct coro_defer) + +struct coro { + coro_context *switcher; + coro_context context; + struct coro_defer_array defer; + + int64_t yield_value; + + struct arena arena; + +#if defined(INSTRUMENT_FOR_VALGRIND) + unsigned int vg_stack_id; +#endif + +#if defined(ALLOCATE_STACK_WITH_MMAP) + unsigned char *stack; +#else + unsigned char stack[]; +#endif +}; + +#if defined(__APPLE__) +#define ASM_SYMBOL(name_) "_" #name_ +#else +#define ASM_SYMBOL(name_) #name_ +#endif + +#define ASM_ROUTINE(name_) \ + ".globl " ASM_SYMBOL(name_) "\n\t" ASM_SYMBOL(name_) ":\n\t" + +/* + * This swapcontext() implementation was obtained from glibc and modified + * slightly to not save/restore the floating point registers, unneeded + * registers, and signal mask. It is Copyright (C) 2001, 2002, 2003 Free + * Software Foundation, Inc and is distributed under GNU LGPL version 2.1 + * (or later). I'm not sure if I can distribute them inside a GPL program; + * they're straightforward so I'm assuming there won't be any problem; if + * there is, I'll just roll my own. + * -- L. + */ +#if defined(__x86_64__) +void __attribute__((noinline, visibility("internal"))) +coro_swapcontext(coro_context *current, coro_context *other); +asm(".text\n\t" + ".p2align 5\n\t" + ASM_ROUTINE(coro_swapcontext) + "movq %rbx,0(%rdi)\n\t" + "movq %rbp,8(%rdi)\n\t" + "movq %r12,16(%rdi)\n\t" + "movq %r13,24(%rdi)\n\t" + "movq %r14,32(%rdi)\n\t" + "movq %r15,40(%rdi)\n\t" + "movq %rdi,48(%rdi)\n\t" + "movq %rsi,56(%rdi)\n\t" + "movq (%rsp),%rcx\n\t" + "movq %rcx,64(%rdi)\n\t" + "leaq 0x8(%rsp),%rcx\n\t" + "movq %rcx,72(%rdi)\n\t" + "movq 72(%rsi),%rsp\n\t" + "movq 0(%rsi),%rbx\n\t" + "movq 8(%rsi),%rbp\n\t" + "movq 16(%rsi),%r12\n\t" + "movq 24(%rsi),%r13\n\t" + "movq 32(%rsi),%r14\n\t" + "movq 40(%rsi),%r15\n\t" + "movq 48(%rsi),%rdi\n\t" + "movq 64(%rsi),%rcx\n\t" + "movq 56(%rsi),%rsi\n\t" + "jmpq *%rcx\n\t"); +#elif defined(__aarch64__) +void __attribute__((noinline, visibility("internal"))) +coro_swapcontext(coro_context *current, coro_context *other); +asm(".text\n\t" + ".p2align 5\n\t" + ASM_ROUTINE(coro_swapcontext) + "mov x10, sp\n\t" + "mov x11, x30\n\t" + "stp x8, x9, [x0, #(1*16)]\n\t" + "stp x10, x11, [x0, #(2*16)]\n\t" + "stp x12, x13, [x0, #(3*16)]\n\t" + "stp x14, x15, [x0, #(4*16)]\n\t" + "stp x19, x20, [x0, #(5*16)]\n\t" + "stp x21, x22, [x0, #(6*16)]\n\t" + "stp x23, x24, [x0, #(7*16)]\n\t" + "stp x25, x26, [x0, #(8*16)]\n\t" + "stp x27, x28, [x0, #(9*16)]\n\t" + "stp x29, x30, [x0, #(10*16)]\n\t" + "stp x0, x1, [x0, #(0*16)]\n\t" + "ldp x8, x9, [x1, #(1*16)]\n\t" + "ldp x10, x11, [x1, #(2*16)]\n\t" + "ldp x12, x13, [x1, #(3*16)]\n\t" + "ldp x14, x15, [x1, #(4*16)]\n\t" + "ldp x19, x20, [x1, #(5*16)]\n\t" + "ldp x21, x22, [x1, #(6*16)]\n\t" + "ldp x23, x24, [x1, #(7*16)]\n\t" + "ldp x25, x26, [x1, #(8*16)]\n\t" + "ldp x27, x28, [x1, #(9*16)]\n\t" + "ldp x29, x30, [x1, #(10*16)]\n\t" + "ldp x0, x1, [x1, #(0*16)]\n\t" + "mov sp, x10\n\t" + "br x11\n\t"); +#elif defined(LWAN_HAVE_LIBUCONTEXT) +#define coro_swapcontext(cur, oth) libucontext_swapcontext(cur, oth) +#else +#error Unsupported platform. +#endif + +__attribute__((used, visibility("internal"))) +void +coro_entry_point(struct coro *coro, coro_function_t func, void *data) +{ + return (void)coro_yield(coro, func(coro, data)); +} + +#ifdef __x86_64__ +/* See comment in coro_reset() for an explanation of why this routine is + * necessary. */ +void __attribute__((visibility("internal"))) coro_entry_point_x86_64(); + +asm(".text\n\t" + ".p2align 5\n\t" + ASM_ROUTINE(coro_entry_point_x86_64) + "mov %r15, %rdx\n\t" + "jmp " ASM_SYMBOL(coro_entry_point) "\n\t" +); +#elif defined(__aarch64__) +void __attribute__((visibility("internal"))) coro_entry_point_arm64(); + +asm(".text\n\t" + ".p2align 5\n\t" + ASM_ROUTINE(coro_entry_point_arm64) + "mov x2, x28\n\t" + "bl " ASM_SYMBOL(coro_entry_point) "\n\t" +); +#endif + +void coro_deferred_run(struct coro *coro, size_t generation) +{ + struct lwan_array *array = (struct lwan_array *)&coro->defer; + struct coro_defer *defers = array->base; + + for (size_t i = array->elements; i != generation; i--) { + struct coro_defer *defer = &defers[i - 1]; + + if (defer->has_two_args) + defer->two.func(defer->two.data1, defer->two.data2); + else + defer->one.func(defer->one.data); + } + + array->elements = generation; +} + +ALWAYS_INLINE size_t coro_deferred_get_generation(const struct coro *coro) +{ + const struct lwan_array *array = (struct lwan_array *)&coro->defer; + + return array->elements; +} + +void coro_reset(struct coro *coro, coro_function_t func, void *data) +{ + unsigned char *stack = coro->stack; + + coro_deferred_run(coro, 0); + arena_reset(&coro->arena); + coro_defer_array_reset(&coro->defer); + +#if defined(__x86_64__) + /* coro_entry_point() for x86-64 has 3 arguments, but RDX isn't + * stored. Use R15 instead, and implement the trampoline + * function in assembly in order to use this register when + * calling the user function. */ + coro->context[5 /* R15 */] = (uintptr_t)data; + coro->context[6 /* RDI */] = (uintptr_t)coro; + coro->context[7 /* RSI */] = (uintptr_t)func; + coro->context[8 /* RIP */] = (uintptr_t)coro_entry_point_x86_64; + + /* Ensure stack is properly aligned: it should be aligned to a + * 16-bytes boundary so SSE will work properly, but should be + * aligned on an 8-byte boundary right after calling a function. */ + uintptr_t rsp = (uintptr_t)stack + CORO_STACK_SIZE; + +#define STACK_PTR 9 + coro->context[STACK_PTR] = (rsp & ~0xful) - 0x8ul; +#elif defined(__aarch64__) + coro->context[19/* x28 */] = (uintptr_t)data; + coro->context[0 /* x0 */] = (uintptr_t)coro; + coro->context[1 /* x1 */] = (uintptr_t)func; + coro->context[5 /* lr */] = (uintptr_t)coro_entry_point_arm64; + + uintptr_t rsp = (uintptr_t)stack + CORO_STACK_SIZE; +#define STACK_PTR 4 + coro->context[STACK_PTR] = rsp & ~0xful; +#elif defined(LWAN_HAVE_LIBUCONTEXT) + libucontext_getcontext(&coro->context); + + coro->context.uc_stack.ss_sp = stack; + coro->context.uc_stack.ss_size = CORO_STACK_SIZE; + coro->context.uc_stack.ss_flags = 0; + coro->context.uc_link = NULL; + + libucontext_makecontext(&coro->context, (void (*)())coro_entry_point, 3, + coro, func, data); + +#endif +} + +ALWAYS_INLINE struct coro * +coro_new(coro_context *switcher, coro_function_t function, void *data) +{ + struct coro *coro; + +#if defined(ALLOCATE_STACK_WITH_MMAP) + void *stack = mmap(NULL, CORO_STACK_SIZE, PROT_READ | PROT_WRITE, + MAP_STACK | MAP_ANON | MAP_PRIVATE, -1, 0); + if (UNLIKELY(stack == MAP_FAILED)) + return NULL; + + coro = lwan_aligned_alloc(sizeof(*coro), 64); + if (UNLIKELY(!coro)) { + munmap(stack, CORO_STACK_SIZE); + return NULL; + } + + coro->stack = stack; +#else + coro = lwan_aligned_alloc(sizeof(struct coro) + CORO_STACK_SIZE, 64); + + if (UNLIKELY(!coro)) + return NULL; +#endif + + coro_defer_array_init(&coro->defer); + arena_init(&coro->arena); + + coro->switcher = switcher; + coro_reset(coro, function, data); + +#if defined(INSTRUMENT_FOR_VALGRIND) + coro->vg_stack_id = VALGRIND_STACK_REGISTER( + coro->stack, (char *)coro->stack + CORO_STACK_SIZE); +#endif + + return coro; +} + +ALWAYS_INLINE int64_t coro_resume(struct coro *coro) +{ + assert(coro); + +#if defined(STACK_PTR) + assert(coro->context[STACK_PTR] >= (uintptr_t)coro->stack && + coro->context[STACK_PTR] <= + (uintptr_t)(coro->stack + CORO_STACK_SIZE)); +#endif + + coro_swapcontext(coro->switcher, &coro->context); + + return coro->yield_value; +} + +ALWAYS_INLINE int64_t coro_resume_value(struct coro *coro, int64_t value) +{ + assert(coro); + + coro->yield_value = value; + return coro_resume(coro); +} + +inline int64_t coro_yield(struct coro *coro, int64_t value) +{ + assert(coro); + + coro->yield_value = value; + coro_swapcontext(&coro->context, coro->switcher); + + return coro->yield_value; +} + +void coro_free(struct coro *coro) +{ + assert(coro); + + coro_deferred_run(coro, 0); + arena_reset(&coro->arena); + coro_defer_array_reset(&coro->defer); + +#if defined(INSTRUMENT_FOR_VALGRIND) + VALGRIND_STACK_DEREGISTER(coro->vg_stack_id); +#endif + +#if defined(ALLOCATE_STACK_WITH_MMAP) + int result = munmap(coro->stack, CORO_STACK_SIZE); + assert(result == 0); /* only fails if addr, len are invalid */ +#endif + + free(coro); +} + +static void disarmed_defer(void *data __attribute__((unused))) +{ +} + +static void coro_defer_disarm_internal(struct coro *coro, + struct coro_defer *defer) +{ + const size_t num_defers = coro_defer_array_len(&coro->defer); + + assert(num_defers != 0 && defer != NULL); + + if (defer == coro_defer_array_get_elem(&coro->defer, num_defers - 1)) { + /* If we're disarming the last defer we armed, there's no need to waste + * space of a deferred callback to an empty function like + * disarmed_defer(). */ + struct lwan_array *defer_base = (struct lwan_array *)&coro->defer; + defer_base->elements--; + } else { + defer->one.func = disarmed_defer; + defer->has_two_args = false; + } +} + +void coro_defer_disarm(struct coro *coro, coro_deferred d) +{ + assert(d >= 0); + + return coro_defer_disarm_internal( + coro, coro_defer_array_get_elem(&coro->defer, (size_t)d)); +} + +void coro_defer_fire_and_disarm(struct coro *coro, coro_deferred d) +{ + assert(d >= 0); + + struct coro_defer *defer = coro_defer_array_get_elem(&coro->defer, (size_t)d); + assert(coro); + + if (defer->has_two_args) + defer->two.func(defer->two.data1, defer->two.data2); + else + defer->one.func(defer->one.data); + + return coro_defer_disarm_internal(coro, defer); +} + +ALWAYS_INLINE coro_deferred +coro_defer(struct coro *coro, defer1_func func, void *data) +{ + struct coro_defer *defer = coro_defer_array_append(&coro->defer); + + if (UNLIKELY(!defer)) { + lwan_status_error("Could not add new deferred function for coro %p", + coro); + return -1; + } + + defer->one.func = func; + defer->one.data = data; + defer->has_two_args = false; + + return (coro_deferred)coro_defer_array_get_elem_index(&coro->defer, defer); +} + +ALWAYS_INLINE coro_deferred +coro_defer2(struct coro *coro, defer2_func func, void *data1, void *data2) +{ + struct coro_defer *defer = coro_defer_array_append(&coro->defer); + + if (UNLIKELY(!defer)) { + lwan_status_error("Could not add new deferred function for coro %p", + coro); + return -1; + } + + defer->two.func = func; + defer->two.data1 = data1; + defer->two.data2 = data2; + defer->has_two_args = true; + + return (coro_deferred)coro_defer_array_get_elem_index(&coro->defer, defer); +} + +void *coro_malloc_full(struct coro *coro, + size_t size, + void (*destroy_func)(void *data)) +{ + void *ptr = malloc(size); + if (LIKELY(ptr)) + coro_defer(coro, destroy_func, ptr); + + return ptr; +} + +void *coro_malloc(struct coro *coro, size_t size) +{ + return arena_alloc(&coro->arena, size); +} + +char *coro_strndup(struct coro *coro, const char *str, size_t max_len) +{ + const size_t len = strnlen(str, max_len) + 1; + char *dup = coro_memdup(coro, str, len); + + if (LIKELY(dup)) + dup[len - 1] = '\0'; + + return dup; +} + +char *coro_strdup(struct coro *coro, const char *str) +{ + return coro_memdup(coro, str, strlen(str) + 1); +} + +char *coro_printf(struct coro *coro, const char *fmt, ...) +{ + va_list values; + int len; + char *tmp_str; + + va_start(values, fmt); + len = vasprintf(&tmp_str, fmt, values); + va_end(values); + + if (UNLIKELY(len < 0)) + return NULL; + + coro_defer(coro, free, tmp_str); + return tmp_str; +} + +void *coro_memdup(struct coro *coro, const void *src, size_t len) +{ + void *ptr = coro_malloc(coro, len); + + return LIKELY(ptr) ? memcpy(ptr, src, len) : NULL; +} diff --git a/src/lib/lwan-coro.h b/src/lib/lwan-coro.h new file mode 100644 index 000000000..0ab27468d --- /dev/null +++ b/src/lib/lwan-coro.h @@ -0,0 +1,73 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include +#include +#include + +#if defined(__x86_64__) +typedef uintptr_t coro_context[10]; +#elif defined(__aarch64__) +typedef uintptr_t coro_context[22]; +#elif defined(LWAN_HAVE_LIBUCONTEXT) +#include +typedef libucontext_ucontext_t coro_context; +#else +#error Unsupported platform. +#endif + +struct coro; +typedef ssize_t coro_deferred; + +typedef int (*coro_function_t)(struct coro *coro, void *data); + +struct coro * +coro_new(coro_context *switcher, coro_function_t function, void *data); +void coro_free(struct coro *coro); + +void coro_reset(struct coro *coro, coro_function_t func, void *data); + +int64_t coro_resume(struct coro *coro); +int64_t coro_resume_value(struct coro *coro, int64_t value); +int64_t coro_yield(struct coro *coro, int64_t value); + +coro_deferred coro_defer(struct coro *coro, void (*func)(void *data), void *data); +coro_deferred coro_defer2(struct coro *coro, + void (*func)(void *data1, void *data2), + void *data1, + void *data2); +void coro_defer_disarm(struct coro *coro, coro_deferred defer); +void coro_defer_fire_and_disarm(struct coro *coro, coro_deferred defer); + +void coro_deferred_run(struct coro *coro, size_t generation); +size_t coro_deferred_get_generation(const struct coro *coro); + +void *coro_malloc(struct coro *coro, size_t sz) __attribute__((malloc)); +void *coro_malloc_full(struct coro *coro, + size_t size, + void (*destroy_func)(void *data)) + __attribute__((malloc)); +char *coro_strdup(struct coro *coro, const char *str); +char *coro_strndup(struct coro *coro, const char *str, size_t len); +char *coro_printf(struct coro *coro, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +void *coro_memdup(struct coro *coro, const void *src, size_t len); diff --git a/src/lib/lwan-h2-huffman.c b/src/lib/lwan-h2-huffman.c new file mode 100644 index 000000000..79f4bb839 --- /dev/null +++ b/src/lib/lwan-h2-huffman.c @@ -0,0 +1,439 @@ +/* + * This code has been automatically generated by gentables.py from + * the table provided in RFC7541. Do not modify! + * + * This is being included here so we can fuzz-test the decoder in + * oss-fuzz before we go forward with the HTTP/2 implementation. + * + * lwan - web server + * Copyright (c) 2022 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + +#include +#include +#include +#include +#include +#include +#include + +#include "ringbuffer.h" + +#define LIKELY(x) x +#define UNLIKELY(x) x + +static inline uint64_t read64be(const void *ptr) +{ + uint64_t v; + memcpy(&v, ptr, 8); + return htobe64(v); +} + +static inline uint32_t read32be(const void *ptr) +{ + uint32_t v; + memcpy(&v, ptr, 4); + return htobe32(v); +} + +struct h2_huffman_code { + uint8_t symbol; + int8_t num_bits; +}; + +static const struct h2_huffman_code level0[256] = { + [0 ... 7] = {48, 5}, [8 ... 15] = {49, 5}, + [16 ... 23] = {50, 5}, [24 ... 31] = {97, 5}, + [32 ... 39] = {99, 5}, [40 ... 47] = {101, 5}, + [48 ... 55] = {105, 5}, [56 ... 63] = {111, 5}, + [64 ... 71] = {115, 5}, [72 ... 79] = {116, 5}, + [80 ... 83] = {32, 6}, [84 ... 87] = {37, 6}, + [88 ... 91] = {45, 6}, [92 ... 95] = {46, 6}, + [96 ... 99] = {47, 6}, [100 ... 103] = {51, 6}, + [104 ... 107] = {52, 6}, [108 ... 111] = {53, 6}, + [112 ... 115] = {54, 6}, [116 ... 119] = {55, 6}, + [120 ... 123] = {56, 6}, [124 ... 127] = {57, 6}, + [128 ... 131] = {61, 6}, [132 ... 135] = {65, 6}, + [136 ... 139] = {95, 6}, [140 ... 143] = {98, 6}, + [144 ... 147] = {100, 6}, [148 ... 151] = {102, 6}, + [152 ... 155] = {103, 6}, [156 ... 159] = {104, 6}, + [160 ... 163] = {108, 6}, [164 ... 167] = {109, 6}, + [168 ... 171] = {110, 6}, [172 ... 175] = {112, 6}, + [176 ... 179] = {114, 6}, [180 ... 183] = {117, 6}, + [184 ... 185] = {58, 7}, [186 ... 187] = {66, 7}, + [188 ... 189] = {67, 7}, [190 ... 191] = {68, 7}, + [192 ... 193] = {69, 7}, [194 ... 195] = {70, 7}, + [196 ... 197] = {71, 7}, [198 ... 199] = {72, 7}, + [200 ... 201] = {73, 7}, [202 ... 203] = {74, 7}, + [204 ... 205] = {75, 7}, [206 ... 207] = {76, 7}, + [208 ... 209] = {77, 7}, [210 ... 211] = {78, 7}, + [212 ... 213] = {79, 7}, [214 ... 215] = {80, 7}, + [216 ... 217] = {81, 7}, [218 ... 219] = {82, 7}, + [220 ... 221] = {83, 7}, [222 ... 223] = {84, 7}, + [224 ... 225] = {85, 7}, [226 ... 227] = {86, 7}, + [228 ... 229] = {87, 7}, [230 ... 231] = {89, 7}, + [232 ... 233] = {106, 7}, [234 ... 235] = {107, 7}, + [236 ... 237] = {113, 7}, [238 ... 239] = {118, 7}, + [240 ... 241] = {119, 7}, [242 ... 243] = {120, 7}, + [244 ... 245] = {121, 7}, [246 ... 247] = {122, 7}, + [248] = {38, 8}, [249] = {42, 8}, + [250] = {44, 8}, [251] = {59, 8}, + [252] = {88, 8}, [253] = {90, 8}, +}; + +static inline const struct h2_huffman_code *next_level0(uint8_t peeked_byte) +{ + static const struct h2_huffman_code level0_11111111[256] = { + [0 ... 63] = {63, 2}, [64 ... 95] = {39, 3}, + [96 ... 127] = {43, 3}, [128 ... 159] = {124, 3}, + [160 ... 175] = {35, 4}, [176 ... 191] = {62, 4}, + [192 ... 199] = {0, 5}, [200 ... 207] = {36, 5}, + [208 ... 215] = {64, 5}, [216 ... 223] = {91, 5}, + [224 ... 231] = {93, 5}, [232 ... 239] = {126, 5}, + [240 ... 243] = {94, 6}, [244 ... 247] = {125, 6}, + [248 ... 249] = {60, 7}, [250 ... 251] = {96, 7}, + [252 ... 253] = {123, 7}, + }; + static const struct h2_huffman_code level0_11111110[256] = { + [0 ... 63] = {33, 2}, + [64 ... 127] = {34, 2}, + [128 ... 191] = {40, 2}, + [192 ... 255] = {41, 2}, + }; + return peeked_byte & 1 ? level0_11111111 : level0_11111110; +} + +static inline const struct h2_huffman_code *next_level1(uint8_t peeked_byte) +{ + static const struct h2_huffman_code level1_11111111[256] = { + [0 ... 7] = {176, 5}, [8 ... 15] = {177, 5}, + [16 ... 23] = {179, 5}, [24 ... 31] = {209, 5}, + [32 ... 39] = {216, 5}, [40 ... 47] = {217, 5}, + [48 ... 55] = {227, 5}, [56 ... 63] = {229, 5}, + [64 ... 71] = {230, 5}, [72 ... 75] = {129, 6}, + [76 ... 79] = {132, 6}, [80 ... 83] = {133, 6}, + [84 ... 87] = {134, 6}, [88 ... 91] = {136, 6}, + [92 ... 95] = {146, 6}, [96 ... 99] = {154, 6}, + [100 ... 103] = {156, 6}, [104 ... 107] = {160, 6}, + [108 ... 111] = {163, 6}, [112 ... 115] = {164, 6}, + [116 ... 119] = {169, 6}, [120 ... 123] = {170, 6}, + [124 ... 127] = {173, 6}, [128 ... 131] = {178, 6}, + [132 ... 135] = {181, 6}, [136 ... 139] = {185, 6}, + [140 ... 143] = {186, 6}, [144 ... 147] = {187, 6}, + [148 ... 151] = {189, 6}, [152 ... 155] = {190, 6}, + [156 ... 159] = {196, 6}, [160 ... 163] = {198, 6}, + [164 ... 167] = {228, 6}, [168 ... 171] = {232, 6}, + [172 ... 175] = {233, 6}, [176 ... 177] = {1, 7}, + [178 ... 179] = {135, 7}, [180 ... 181] = {137, 7}, + [182 ... 183] = {138, 7}, [184 ... 185] = {139, 7}, + [186 ... 187] = {140, 7}, [188 ... 189] = {141, 7}, + [190 ... 191] = {143, 7}, [192 ... 193] = {147, 7}, + [194 ... 195] = {149, 7}, [196 ... 197] = {150, 7}, + [198 ... 199] = {151, 7}, [200 ... 201] = {152, 7}, + [202 ... 203] = {155, 7}, [204 ... 205] = {157, 7}, + [206 ... 207] = {158, 7}, [208 ... 209] = {165, 7}, + [210 ... 211] = {166, 7}, [212 ... 213] = {168, 7}, + [214 ... 215] = {174, 7}, [216 ... 217] = {175, 7}, + [218 ... 219] = {180, 7}, [220 ... 221] = {182, 7}, + [222 ... 223] = {183, 7}, [224 ... 225] = {188, 7}, + [226 ... 227] = {191, 7}, [228 ... 229] = {197, 7}, + [230 ... 231] = {231, 7}, [232 ... 233] = {239, 7}, + [234] = {9, 8}, [235] = {142, 8}, + [236] = {144, 8}, [237] = {145, 8}, + [238] = {148, 8}, [239] = {159, 8}, + [240] = {171, 8}, [241] = {206, 8}, + [242] = {215, 8}, [243] = {225, 8}, + [244] = {236, 8}, [245] = {237, 8}, + }; + static const struct h2_huffman_code level1_11111110[256] = { + [0 ... 31] = {92, 3}, [32 ... 63] = {195, 3}, + [64 ... 95] = {208, 3}, [96 ... 111] = {128, 4}, + [112 ... 127] = {130, 4}, [128 ... 143] = {131, 4}, + [144 ... 159] = {162, 4}, [160 ... 175] = {184, 4}, + [176 ... 191] = {194, 4}, [192 ... 207] = {224, 4}, + [208 ... 223] = {226, 4}, [224 ... 231] = {153, 5}, + [232 ... 239] = {161, 5}, [240 ... 247] = {167, 5}, + [248 ... 255] = {172, 5}, + }; + return peeked_byte & 1 ? level1_11111111 : level1_11111110; +} + +static inline const struct h2_huffman_code *next_level2(uint8_t peeked_byte) +{ + static const struct h2_huffman_code level2_11111110[256] = { + [0 ... 31] = {254, 3}, [32 ... 47] = {2, 4}, + [48 ... 63] = {3, 4}, [64 ... 79] = {4, 4}, + [80 ... 95] = {5, 4}, [96 ... 111] = {6, 4}, + [112 ... 127] = {7, 4}, [128 ... 143] = {8, 4}, + [144 ... 159] = {11, 4}, [160 ... 175] = {12, 4}, + [176 ... 191] = {14, 4}, [192 ... 207] = {15, 4}, + [208 ... 223] = {16, 4}, [224 ... 239] = {17, 4}, + [240 ... 255] = {18, 4}, + }; + static const struct h2_huffman_code level2_11111111[256] = { + [0 ... 15] = {19, 4}, [16 ... 31] = {20, 4}, + [32 ... 47] = {21, 4}, [48 ... 63] = {23, 4}, + [64 ... 79] = {24, 4}, [80 ... 95] = {25, 4}, + [96 ... 111] = {26, 4}, [112 ... 127] = {27, 4}, + [128 ... 143] = {28, 4}, [144 ... 159] = {29, 4}, + [160 ... 175] = {30, 4}, [176 ... 191] = {31, 4}, + [192 ... 207] = {127, 4}, [208 ... 223] = {220, 4}, + [224 ... 239] = {249, 4}, [240 ... 243] = {10, 6}, + [244 ... 247] = {13, 6}, [248 ... 251] = {22, 6}, + [252 ... 255] = {0, -1}, + }; + static const struct h2_huffman_code level2_11111000[256] = { + [0 ... 63] = {192, 2}, + [64 ... 127] = {193, 2}, + [128 ... 191] = {200, 2}, + [192 ... 255] = {201, 2}, + }; + static const struct h2_huffman_code level2_11110110[256] = { + [0 ... 127] = {199, 1}, + [128 ... 255] = {207, 1}, + }; + static const struct h2_huffman_code level2_11111001[256] = { + [0 ... 63] = {202, 2}, + [64 ... 127] = {205, 2}, + [128 ... 191] = {210, 2}, + [192 ... 255] = {213, 2}, + }; + static const struct h2_huffman_code level2_11111011[256] = { + [0 ... 63] = {242, 2}, [64 ... 127] = {243, 2}, + [128 ... 191] = {255, 2}, [192 ... 223] = {203, 3}, + [224 ... 255] = {204, 3}, + }; + static const struct h2_huffman_code level2_11111100[256] = { + [0 ... 31] = {211, 3}, [32 ... 63] = {212, 3}, + [64 ... 95] = {214, 3}, [96 ... 127] = {221, 3}, + [128 ... 159] = {222, 3}, [160 ... 191] = {223, 3}, + [192 ... 223] = {241, 3}, [224 ... 255] = {244, 3}, + }; + static const struct h2_huffman_code level2_11111010[256] = { + [0 ... 63] = {218, 2}, + [64 ... 127] = {219, 2}, + [128 ... 191] = {238, 2}, + [192 ... 255] = {240, 2}, + }; + static const struct h2_huffman_code level2_11110111[256] = { + [0 ... 127] = {234, 1}, + [128 ... 255] = {235, 1}, + }; + static const struct h2_huffman_code level2_11111101[256] = { + [0 ... 31] = {245, 3}, [32 ... 63] = {246, 3}, + [64 ... 95] = {247, 3}, [96 ... 127] = {248, 3}, + [128 ... 159] = {250, 3}, [160 ... 191] = {251, 3}, + [192 ... 223] = {252, 3}, [224 ... 255] = {253, 3}, + }; + switch (peeked_byte) { + case 0b11111110: + return level2_11111110; + case 0b11111111: + return level2_11111111; + case 0b11111000: + return level2_11111000; + case 0b11110110: + return level2_11110110; + case 0b11111001: + return level2_11111001; + case 0b11111011: + return level2_11111011; + case 0b11111100: + return level2_11111100; + case 0b11111010: + return level2_11111010; + case 0b11110111: + return level2_11110111; + case 0b11111101: + return level2_11111101; + default: + return NULL; + } +} + +struct bit_reader { + const uint8_t *bitptr; + uint64_t bitbuf; + int64_t total_bitcount; + int bitcount; +}; + +static inline uint8_t peek_byte(struct bit_reader *reader) +{ + if (reader->bitcount < 8) { + if (reader->total_bitcount >= 64) { + reader->bitbuf |= read64be(reader->bitptr) >> reader->bitcount; + reader->bitptr += + (63 - reader->bitcount + (reader->bitcount & 1)) >> 3; + reader->bitcount |= 56; + } else if (reader->total_bitcount >= 32) { + reader->bitbuf |= read32be(reader->bitptr) >> reader->bitcount; + reader->bitptr += + (31 - reader->bitcount + (reader->bitcount & 1)) >> 3; + reader->bitcount |= 24; + } else { + reader->bitbuf |= *reader->bitptr >> reader->bitcount; + reader->bitptr += + (7 - reader->bitcount + (reader->bitcount & 1)) >> 3; + reader->bitcount |= 8; + } + } + return reader->bitbuf >> 56; +} + +static inline bool consume(struct bit_reader *reader, int count) +{ + assert(count > 0); + reader->bitbuf <<= count; + reader->bitcount -= count; + reader->total_bitcount -= count; + return reader->total_bitcount > 0; +} + +DEFINE_RING_BUFFER_TYPE(uint8_ring_buffer, uint8_t, 64) + +struct lwan_h2_huffman_decoder { + struct bit_reader bit_reader; + struct uint8_ring_buffer buffer; +}; + +void lwan_h2_huffman_init(struct lwan_h2_huffman_decoder *huff, + const uint8_t *input, + size_t input_len) +{ + huff->bit_reader = (struct bit_reader){ + .bitptr = input, + .total_bitcount = (int64_t)input_len * 8, + }; + uint8_ring_buffer_init(&huff->buffer); +} + +ssize_t lwan_h2_huffman_next(struct lwan_h2_huffman_decoder *huff) +{ + struct bit_reader *reader = &huff->bit_reader; + struct uint8_ring_buffer *buffer = &huff->buffer; + + while (reader->total_bitcount > 7) { + uint8_t peeked_byte = peek_byte(reader); + if (LIKELY(level0[peeked_byte].num_bits)) { + if (!uint8_ring_buffer_try_put_copy(buffer, + level0[peeked_byte].symbol)) + goto done; + consume(reader, level0[peeked_byte].num_bits); + assert(reader->total_bitcount >= 0); + continue; + } + + if (!consume(reader, 8)) + return -1; + + const struct h2_huffman_code *level1 = next_level0(peeked_byte); + peeked_byte = peek_byte(reader); + if (level1[peeked_byte].num_bits) { + if (!uint8_ring_buffer_try_put_copy(buffer, + level1[peeked_byte].symbol)) + goto done; + if (!consume(reader, level1[peeked_byte].num_bits)) + return -1; + continue; + } + + if (!consume(reader, 8)) + return -1; + + const struct h2_huffman_code *level2 = next_level1(peeked_byte); + peeked_byte = peek_byte(reader); + if (level2[peeked_byte].num_bits) { + if (!uint8_ring_buffer_try_put_copy(buffer, + level2[peeked_byte].symbol)) + goto done; + if (!consume(reader, level2[peeked_byte].num_bits)) + return -1; + continue; + } + + if (!consume(reader, 8)) + return -1; + + const struct h2_huffman_code *level3 = next_level2(peeked_byte); + if (LIKELY(level3)) { + peeked_byte = peek_byte(reader); + if (level3[peeked_byte].num_bits < 0) { + /* EOS found */ + goto done; + } + if (LIKELY(level3[peeked_byte].num_bits)) { + if (!uint8_ring_buffer_try_put_copy(buffer, + level3[peeked_byte].symbol)) + goto done; + if (!consume(reader, level3[peeked_byte].num_bits)) + return -1; + continue; + } + } + + return -1; + } + + /* FIXME: ensure we're not promoting types unnecessarily here */ + if (reader->total_bitcount) { + const uint8_t peeked_byte = peek_byte(reader); + const uint8_t eos_prefix = + (uint8_t)(((1u << reader->total_bitcount) - 1u) + << (8u - reader->total_bitcount)); + + if ((peeked_byte & eos_prefix) == eos_prefix) + goto done; + + if (level0[peeked_byte].num_bits == (int8_t)reader->total_bitcount) { + uint8_ring_buffer_try_put_copy(buffer, level0[peeked_byte].symbol); + goto done; + } + + /* If we get here, then the remaining bits are either: + * - Not a prefix of EOS + * - Incomplete sequence + * - Has overlong padding + */ + return -1; + } + +done: + return (ssize_t)uint8_ring_buffer_size(buffer); +} + +bool lwan_h2_huffman_decode_for_fuzzing(const uint8_t *input, size_t input_len) +{ + struct lwan_h2_huffman_decoder decoder; + + lwan_h2_huffman_init(&decoder, input, input_len); + + while (true) { + ssize_t n_decoded = lwan_h2_huffman_next(&decoder); + + if (UNLIKELY(n_decoded < 0)) + return false; + if (n_decoded < 64) + return true; + + uint8_ring_buffer_init(&decoder.buffer); + } +} +#endif diff --git a/src/lib/lwan-http-authorize.c b/src/lib/lwan-http-authorize.c new file mode 100644 index 000000000..cae2a97fd --- /dev/null +++ b/src/lib/lwan-http-authorize.c @@ -0,0 +1,207 @@ +/* + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include +#include +#include + +#include "base64.h" +#include "lwan-private.h" +#include "lwan-cache.h" +#include "lwan-config.h" +#include "lwan-http-authorize.h" + +struct realm_password_file_t { + struct cache_entry base; + struct hash *entries; +}; + +static struct cache *realm_password_cache = NULL; + +static void zero_and_free(void *str) +{ + if (LIKELY(str)) { + lwan_always_bzero(str, strlen(str)); + free(str); + } +} + +static struct cache_entry * +create_realm_file(const void *key, void *context __attribute__((unused)), + void *create_contex __attribute__((unused))) +{ + struct realm_password_file_t *rpf = malloc(sizeof(*rpf)); + const struct config_line *l; + struct config *f; + + if (UNLIKELY(!rpf)) + return NULL; + + rpf->entries = hash_str_new(zero_and_free, zero_and_free); + if (UNLIKELY(!rpf->entries)) + goto error_no_close; + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + static const uint8_t hardcoded_user_config[] = "user=password\n" + "root=hunter2\n"; + f = config_open_for_fuzzing(hardcoded_user_config, + sizeof(hardcoded_user_config)); +#else + f = config_open(key); +#endif + if (!f) + goto error_no_close; + + while ((l = config_read_line(f))) { + /* FIXME: Storing plain-text passwords in memory isn't a good idea. */ + switch (l->type) { + case CONFIG_LINE_TYPE_LINE: { + char *username = strdup(l->key); + if (!username) + goto error; + + char *password = strdup(l->value); + if (!password) { + free(username); + goto error; + } + + int err = hash_add_unique(rpf->entries, username, password); + if (LIKELY(!err)) + continue; + + zero_and_free(username); + zero_and_free(password); + + if (err == -EEXIST) { + lwan_status_warning( + "Username entry already exists, ignoring: \"%s\"", l->key); + continue; + } + + goto error; + } + default: + config_error(f, "Expected username = password"); + break; + } + } + + if (config_last_error(f)) { + lwan_status_error("Error on password file \"%s\", line %d: %s", (char *)key, + config_cur_line(f), config_last_error(f)); + goto error; + } + + config_close(f); + return (struct cache_entry *)rpf; + +error: + config_close(f); +error_no_close: + hash_unref(rpf->entries); + free(rpf); + return NULL; +} + +static void destroy_realm_file(struct cache_entry *entry, + void *context __attribute__((unused))) +{ + struct realm_password_file_t *rpf = (struct realm_password_file_t *)entry; + hash_unref(rpf->entries); + free(rpf); +} + +bool lwan_http_authorize_init(void) +{ + realm_password_cache = + cache_create(create_realm_file, destroy_realm_file, NULL, 60); + + return !!realm_password_cache; +} + +void lwan_http_authorize_shutdown(void) { cache_destroy(realm_password_cache); } + +static bool authorize(struct coro *coro, + const char *header, + size_t header_len, + const char *password_file) +{ + struct realm_password_file_t *rpf; + unsigned char *decoded; + char *colon; + char *password; + char *looked_password; + size_t decoded_len; + bool password_ok = false; + + rpf = (struct realm_password_file_t *)cache_coro_get_and_ref_entry( + realm_password_cache, coro, password_file); + if (UNLIKELY(!rpf)) + return false; + + decoded = base64_decode((unsigned char *)header, header_len, &decoded_len); + if (UNLIKELY(!decoded)) + return false; + + colon = memchr(decoded, ':', decoded_len); + if (UNLIKELY(!colon)) + goto out; + + *colon = '\0'; + password = colon + 1; + + looked_password = hash_find(rpf->entries, decoded); + if (looked_password) + password_ok = streq(password, looked_password); + +out: + free(decoded); + return password_ok; +} + +bool lwan_http_authorize(struct lwan_request *request, + const char *realm, + const char *password_file) +{ + static const char authenticate_tmpl[] = "Basic realm=\"%s\""; + static const size_t basic_len = sizeof("Basic ") - 1; + const char *authorization = + lwan_request_get_header(request, "Authorization"); + + if (LIKELY(authorization && !strncmp(authorization, "Basic ", basic_len))) { + const char *header = authorization + basic_len; + size_t header_len = strlen(authorization) - basic_len; + + if (authorize(request->conn->coro, header, header_len, password_file)) + return true; + } + + const struct lwan_key_value headers[] = { + {"WWW-Authenticate", + coro_printf(request->conn->coro, authenticate_tmpl, realm)}, + {}, + }; + request->response.headers = + coro_memdup(request->conn->coro, headers, sizeof(headers)); + + return false; +} diff --git a/src/lib/lwan-http-authorize.h b/src/lib/lwan-http-authorize.h new file mode 100644 index 000000000..3bc719418 --- /dev/null +++ b/src/lib/lwan-http-authorize.h @@ -0,0 +1,38 @@ +/* + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include "lwan.h" + +bool lwan_http_authorize_init(void); +void lwan_http_authorize_shutdown(void); + +bool lwan_http_authorize(struct lwan_request *request, + const char *realm, + const char *password_file); + +static inline bool +lwan_http_authorize_urlmap(struct lwan_request *request, + const struct lwan_url_map *url_map) +{ + return lwan_http_authorize(request, url_map->authorization.realm, + url_map->authorization.password_file); +} diff --git a/src/lib/lwan-http-status.h b/src/lib/lwan-http-status.h new file mode 100644 index 000000000..10fbda39f --- /dev/null +++ b/src/lib/lwan-http-status.h @@ -0,0 +1,25 @@ +#pragma once + +#define FOR_EACH_HTTP_STATUS(X) \ + X(SWITCHING_PROTOCOLS, 101, "Switching protocols", "Protocol is switching over from HTTP") \ + X(OK, 200, "OK", "Success") \ + X(PARTIAL_CONTENT, 206, "Partial content", "Delivering part of requested resource") \ + X(MOVED_PERMANENTLY, 301, "Moved permanently", "This content has moved to another place") \ + X(NOT_MODIFIED, 304, "Not modified", "The content has not changed since previous request") \ + X(TEMPORARY_REDIRECT, 307, "Temporary Redirect", "This content can be temporarily found at a different location") \ + X(BAD_REQUEST, 400, "Bad request", "The client has issued a bad request") \ + X(NOT_AUTHORIZED, 401, "Not authorized", "Client has no authorization to access this resource") \ + X(FORBIDDEN, 403, "Forbidden", "Access to this resource has been denied") \ + X(NOT_FOUND, 404, "Not found", "The requested resource could not be found on this server") \ + X(NOT_ALLOWED, 405, "Not allowed", "The requested method is not allowed by this server") \ + X(NOT_ACCEPTABLE, 406, "Not acceptable", "No suitable accepted-encoding header provided") \ + X(TIMEOUT, 408, "Request timeout", "Client did not produce a request within expected timeframe") \ + X(TOO_LARGE, 413, "Request too large", "The request entity is too large") \ + X(RANGE_UNSATISFIABLE, 416, "Requested range unsatisfiable", "The server can't supply the requested portion of the requested resource") \ + X(I_AM_A_TEAPOT, 418, "I'm a teapot", "Client requested to brew coffee but device is a teapot") \ + X(CLIENT_TOO_HIGH, 420, "Client too high", "Client is too high to make a request") \ + X(UNPROCESSABLE_CONTENT, 422, "Unprocessable content", "Request was understood by the server but it can't process the instructions") \ + X(INTERNAL_ERROR, 500, "Internal server error", "The server encountered an internal error that couldn't be recovered from") \ + X(NOT_IMPLEMENTED, 501, "Not implemented", "Server lacks the ability to fulfil the request") \ + X(UNAVAILABLE, 503, "Service unavailable", "The server is either overloaded or down for maintenance") \ + X(SERVER_TOO_HIGH, 520, "Server too high", "The server is too high to answer the request") diff --git a/src/lib/lwan-io-wrappers.c b/src/lib/lwan-io-wrappers.c new file mode 100644 index 000000000..8608a5e8e --- /dev/null +++ b/src/lib/lwan-io-wrappers.c @@ -0,0 +1,397 @@ +/* + * lwan - web server + * Copyright (c) 2013 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-io-wrappers.h" +#include "lwan-private.h" + +static const int MAX_FAILED_TRIES = 5; + +ssize_t lwan_writev_fd(struct lwan_request *request, + int fd, + struct iovec *iov, + int iov_count) +{ + ssize_t total_written = 0; + int curr_iov = 0; + int flags = (request->conn->flags & CONN_CORK) ? MSG_MORE : 0; + + for (int tries = MAX_FAILED_TRIES; tries;) { + const int remaining_len = (int)(iov_count - curr_iov); + ssize_t written; + + if (remaining_len == 1) { + const struct iovec *vec = &iov[curr_iov]; + return lwan_send_fd(request, fd, vec->iov_base, vec->iov_len, + flags); + } + + struct msghdr hdr = { + .msg_iov = iov + curr_iov, + .msg_iovlen = (size_t)remaining_len, + }; + written = sendmsg(fd, &hdr, flags); + + if (UNLIKELY(written < 0)) { + /* FIXME: Consider short writes as another try as well? */ + tries--; + + switch (errno) { + case EAGAIN: + case EINTR: + break; + default: + return -errno; + } + } else if (written == 0) { + return total_written; + } else { + total_written += written; + + while (curr_iov < iov_count && + written >= (ssize_t)iov[curr_iov].iov_len) { + written -= (ssize_t)iov[curr_iov].iov_len; + curr_iov++; + } + + if (curr_iov == iov_count) + return total_written; + + iov[curr_iov].iov_base = (char *)iov[curr_iov].iov_base + written; + iov[curr_iov].iov_len -= (size_t)written; + } + + lwan_request_await_read(request, fd); + } + + return -ETIMEDOUT; +} + +ssize_t lwan_readv_fd(struct lwan_request *request, + int fd, + struct iovec *iov, + int iov_count) +{ + ssize_t total_bytes_read = 0; + int curr_iov = 0; + + for (int tries = MAX_FAILED_TRIES; tries;) { + const int remaining_len = (int)(iov_count - curr_iov); + + if (remaining_len == 1) { + const struct iovec *vec = &iov[curr_iov]; + return lwan_recv_fd(request, fd, vec->iov_base, vec->iov_len, 0); + } + + struct msghdr hdr = { + .msg_iov = iov + curr_iov, + .msg_iovlen = (size_t)remaining_len, + }; + ssize_t bytes_read = recvmsg(fd, &hdr, 0); + if (UNLIKELY(bytes_read < 0)) { + /* FIXME: Consider short reads as another try as well? */ + tries--; + + switch (errno) { + case EAGAIN: + case EINTR: + break; + default: + return -errno; + } + } else if (bytes_read == 0) { + return total_bytes_read; + } else { + total_bytes_read += bytes_read; + + while (curr_iov < iov_count && + bytes_read >= (ssize_t)iov[curr_iov].iov_len) { + bytes_read -= (ssize_t)iov[curr_iov].iov_len; + curr_iov++; + } + + if (curr_iov == iov_count) + return total_bytes_read; + + iov[curr_iov].iov_base = + (char *)iov[curr_iov].iov_base + bytes_read; + iov[curr_iov].iov_len -= (size_t)bytes_read; + } + + lwan_request_await_read(request, fd); + } + + return -ETIMEDOUT; +} + +ssize_t lwan_send_fd(struct lwan_request *request, + int fd, + const void *buf, + size_t count, + int flags) +{ + size_t to_send = count; + + if (request->conn->flags & CONN_CORK) + flags |= MSG_MORE; + + for (int tries = MAX_FAILED_TRIES; tries;) { + ssize_t written = send(fd, buf, to_send, flags); + if (UNLIKELY(written < 0)) { + tries--; + + switch (errno) { + case EAGAIN: + case EINTR: + break; + default: + return -errno; + } + } else { + to_send -= (size_t)written; + if (!to_send) + return (ssize_t)count; + buf = (char *)buf + written; + } + + lwan_request_await_write(request, fd); + } + + return -ETIMEDOUT; +} + +ssize_t +lwan_recv_fd(struct lwan_request *request, int fd, void *buf, size_t count, int flags) +{ + size_t to_recv = count; + + for (int tries = MAX_FAILED_TRIES; tries;) { + ssize_t recvd = recv(fd, buf, to_recv, flags); + if (UNLIKELY(recvd < 0)) { + tries--; + + switch (errno) { + case EINTR: + case EAGAIN: + if (flags & MSG_DONTWAIT) + return -EAGAIN; + break; + default: + return -errno; + } + } else { + to_recv -= (size_t)recvd; + if (!to_recv) + return (ssize_t)count; + buf = (char *)buf + recvd; + } + + lwan_request_await_read(request, fd); + } + + return -ETIMEDOUT; +} + +#if defined(__linux__) +int lwan_sendfile_fd(struct lwan_request *request, + int out_fd, + int in_fd, + off_t offset, + size_t count, + const char *header, + size_t header_len) +{ + /* Clamp each chunk to 2^21 bytes[1] to balance throughput and + * scalability. This used to be capped to 2^14 bytes, as that's the + * maximum TLS record size[2], but was found to be hurtful for + * performance[2], so use the same default value that Nginx uses. + * + * First chunk is clamped to 2^21 - header_len, because the header is + * sent using MSG_MORE. Subsequent chunks are sized 2^21 bytes. (Do + * this regardless of this connection being TLS or not for simplicity.) + * + * [1] https://www.kernel.org/doc/html/v5.12/networking/tls.html#sending-tls-application-data + * [2] https://github.com/lpereira/lwan/issues/334 + */ + size_t chunk_size = LWAN_MIN(count, (1ul << 21) - header_len); + size_t to_be_written = count; + ssize_t r; + + assert(header_len < (1ul << 21)); + + r = lwan_send_fd(request, out_fd, header, header_len, MSG_MORE); + if (r < 0) + return (int)r; + + while (true) { + ssize_t written = sendfile(out_fd, in_fd, &offset, chunk_size); + if (UNLIKELY(written < 0)) { + switch (errno) { + case EAGAIN: + case EINTR: + break; + default: + return -errno; + } + } else { + to_be_written -= (size_t)written; + if (!to_be_written) + return 0; + + chunk_size = LWAN_MIN(to_be_written, 1ul << 21); + lwan_readahead_queue(in_fd, offset, chunk_size); + } + + lwan_request_await_write(request, out_fd); + } +} +#elif defined(__FreeBSD__) || defined(__APPLE__) +int lwan_sendfile_fd(struct lwan_request *request, + int out_fd, + int in_fd, + off_t offset, + size_t count, + const char *header, + size_t header_len) +{ + struct sf_hdtr headers = {.headers = + (struct iovec[]){{.iov_base = (void *)header, + .iov_len = header_len}}, + .hdr_cnt = 1}; + off_t sbytes = (off_t)count; + + if (!count) { + /* FreeBSD's sendfile() won't send the headers when count is 0. Why? */ + return lwan_writev_fd(request, out_fd, headers.headers, + headers.hdr_cnt); + } + + while (true) { + int r; + +#ifdef __APPLE__ + r = sendfile(in_fd, out_fd, offset, &sbytes, &headers, 0); +#else + r = sendfile(in_fd, out_fd, offset, count, &headers, &sbytes, + SF_MNOWAIT); +#endif + if (UNLIKELY(r < 0)) { + switch (errno) { + case EAGAIN: + case EBUSY: + case EINTR: + break; + default: + return -errno; + } + } else { + count -= (size_t)sbytes; + if (!count) + return 0; + } + + lwan_request_await_write(request, out_fd); + } +} +#else +static ssize_t try_pread_file(struct lwan_request *request, + int fd, + void *buffer, + size_t len, + off_t offset) +{ + size_t total_read = 0; + + for (int tries = MAX_FAILED_TRIES; tries;) { + ssize_t r = pread(fd, buffer, len, offset); + + if (UNLIKELY(r < 0)) { + tries--; + + switch (errno) { + case EAGAIN: + case EINTR: + /* fd is a file, re-read -- but give other coros some time */ + coro_yield(request->conn->coro, CONN_CORO_YIELD); + continue; + default: + return -errno; + } + } + + total_read += (size_t)r; + if (total_read == len) { + return total_read; + } + + offset += r; + } + + return -ETIMEDOUT; +} + +int lwan_sendfile_fd(struct lwan_request *request, + int out_fd, + int in_fd, + off_t offset, + size_t count, + const char *header, + size_t header_len) +{ + unsigned char buffer[512]; + unsigned int blocks_sent = 0; + ssize_t r; + + r = lwan_send_fd(request, out_fd, header, header_len, MSG_MORE); + if (UNLIKELY(r < 0)) { + return (int)r; + } + + while (count) { + r = try_pread_file(request, in_fd, buffer, + LWAN_MIN(count, sizeof(buffer)), offset); + if (UNLIKELY(r <= 0)) + return (int)r; + + size_t bytes_read = (size_t)r; + r = lwan_send_fd(request, out_fd, buffer, bytes_read, + bytes_read < sizeof(buffer) ? 0 : MSG_MORE); + if (UNLIKELY(r < 0)) + return (int)r; + + count -= bytes_read; + offset += bytes_read; + + blocks_sent++; + if ((blocks_sent & 3) == 0) { + coro_yield(request->conn->coro, CONN_CORO_WANT_WRITE); + } + } + + return 0; +} +#endif diff --git a/src/lib/lwan-io-wrappers.h b/src/lib/lwan-io-wrappers.h new file mode 100644 index 000000000..980c8db62 --- /dev/null +++ b/src/lib/lwan-io-wrappers.h @@ -0,0 +1,113 @@ +/* + * lwan - web server + * Copyright (c) 2013 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include +#include +#include +#include + +#include "lwan.h" + +ssize_t lwan_writev_fd(struct lwan_request *request, + int fd, + struct iovec *iov, + int iovcnt); +ssize_t lwan_send_fd(struct lwan_request *request, + int fd, + const void *buf, + size_t count, + int flags); +int lwan_sendfile_fd(struct lwan_request *request, + int out_fd, + int in_fd, + off_t offset, + size_t count, + const char *header, + size_t header_len); +ssize_t lwan_recv_fd( + struct lwan_request *request, int fd, void *buf, size_t count, int flags); +ssize_t lwan_readv_fd(struct lwan_request *request, + int fd, + struct iovec *iov, + int iov_count); + +static inline ssize_t +lwan_writev(struct lwan_request *request, struct iovec *iov, int iovcnt) +{ + ssize_t r = lwan_writev_fd(request, request->fd, iov, iovcnt); + if (UNLIKELY(r < 0)) { + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + return r; +} + +static inline ssize_t lwan_send(struct lwan_request *request, + const void *buf, + size_t count, + int flags) +{ + ssize_t r = lwan_send_fd(request, request->fd, buf, count, flags); + if (UNLIKELY(r < 0)) { + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + return r; +} + +static inline int lwan_sendfile(struct lwan_request *request, + int in_fd, + off_t offset, + size_t count, + const char *header, + size_t header_len) +{ + int r = lwan_sendfile_fd(request, request->fd, in_fd, offset, count, header, + header_len); + if (UNLIKELY(r < 0)) { + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + return r; +} + +static inline ssize_t +lwan_recv(struct lwan_request *request, void *buf, size_t count, int flags) +{ + ssize_t r = lwan_recv_fd(request, request->fd, buf, count, flags); + if (UNLIKELY(r < 0 && !(flags & MSG_NOSIGNAL))) { + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + return r; +} + +static inline ssize_t +lwan_readv(struct lwan_request *request, struct iovec *iov, int iov_count) +{ + ssize_t r = lwan_readv_fd(request, request->fd, iov, iov_count); + if (UNLIKELY(r < 0)) { + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + return r; +} diff --git a/common/lwan-job.c b/src/lib/lwan-job.c similarity index 65% rename from common/lwan-job.c rename to src/lib/lwan-job.c index 6770b4348..427f46aac 100644 --- a/common/lwan-job.c +++ b/src/lib/lwan-job.c @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2012, 2013 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2012, 2013 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -20,17 +20,19 @@ #define _GNU_SOURCE #include #include +#include #include #include #include #include +#include #include -#include "lwan.h" +#include "lwan-private.h" #include "lwan-status.h" #include "list.h" -struct job_t { +struct job { struct list_node jobs; bool (*cb)(void *data); void *data; @@ -41,16 +43,43 @@ static pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER; static bool running = false; static struct list_head jobs; -static void* -job_thread(void *data __attribute__((unused))) +static pthread_mutex_t job_wait_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t job_wait_cond = PTHREAD_COND_INITIALIZER; + +static void +timedwait(bool had_job) +{ + static int secs = 1; + struct timeval now; + + if (had_job) + secs = 1; + else if (secs <= 15) + secs++; + + gettimeofday(&now, NULL); + + struct timespec rgtp = { now.tv_sec + secs, now.tv_usec * 1000 }; + pthread_cond_timedwait(&job_wait_cond, &job_wait_mutex, &rgtp); +} + +void lwan_job_thread_main_loop(void) { - struct timespec rgtp = { 1, 0 }; + /* Idle priority for the calling thread. Magic value of `7` obtained from + * sample program in linux/Documentation/block/ioprio.txt. This is a no-op + * on anything but Linux. */ + ioprio_set(IOPRIO_WHO_PROCESS, 0, IOPRIO_PRIO_VALUE(IOPRIO_CLASS_IDLE, 7)); + + lwan_set_thread_name("job"); + if (pthread_mutex_lock(&job_wait_mutex)) + lwan_status_critical("Could not lock job wait mutex"); + while (running) { bool had_job = false; if (LIKELY(!pthread_mutex_lock(&queue_mutex))) { - struct job_t *job; + struct job *job; list_for_each(&jobs, job, jobs) had_job |= job->cb(job->data); @@ -58,18 +87,11 @@ job_thread(void *data __attribute__((unused))) pthread_mutex_unlock(&queue_mutex); } - if (had_job) - rgtp.tv_sec = 1; - else if (rgtp.tv_sec <= 15) - rgtp.tv_sec++; - - if (UNLIKELY(nanosleep(&rgtp, NULL) < 0)) { - if (errno == EINTR) - sleep(1); - } + timedwait(had_job); } - return NULL; + if (pthread_mutex_unlock(&job_wait_mutex)) + lwan_status_critical("Could not lock job wait mutex"); } void lwan_job_thread_init(void) @@ -80,9 +102,8 @@ void lwan_job_thread_init(void) list_head_init(&jobs); + self = pthread_self(); running = true; - if (pthread_create(&self, NULL, job_thread, NULL) < 0) - lwan_status_critical_perror("pthread_create"); #ifdef SCHED_IDLE struct sched_param sched_param = { @@ -98,14 +119,23 @@ void lwan_job_thread_shutdown(void) lwan_status_debug("Shutting down job thread"); if (LIKELY(!pthread_mutex_lock(&queue_mutex))) { - struct job_t *node, *next; + struct job *node, *next; + int r; + list_for_each_safe(&jobs, node, next, jobs) { list_del(&node->jobs); free(node); } running = false; - if (pthread_tryjoin_np(self, NULL) < 0) - lwan_status_critical_perror("pthread_join"); + + pthread_cond_signal(&job_wait_cond); + + r = pthread_join(self, NULL); + if (r) { + errno = r; + lwan_status_perror("pthread_join"); + } + pthread_mutex_unlock(&queue_mutex); } } @@ -114,7 +144,7 @@ void lwan_job_add(bool (*cb)(void *data), void *data) { assert(cb); - struct job_t *job = calloc(1, sizeof(*job)); + struct job *job = calloc(1, sizeof(*job)); if (!job) lwan_status_critical_perror("calloc"); @@ -132,7 +162,7 @@ void lwan_job_add(bool (*cb)(void *data), void *data) void lwan_job_del(bool (*cb)(void *data), void *data) { - struct job_t *node, *next; + struct job *node, *next; assert(cb); diff --git a/src/lib/lwan-lua.c b/src/lib/lwan-lua.c new file mode 100644 index 000000000..5561280a3 --- /dev/null +++ b/src/lib/lwan-lua.c @@ -0,0 +1,508 @@ +/* + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "lwan-lua.h" + +#if defined(LWAN_HAVE_LUA_JIT) +#define luaL_reg luaL_Reg +#endif + +static const char *request_metatable_name = "Lwan.Request"; + +ALWAYS_INLINE struct lwan_request *lwan_lua_get_request_from_userdata(lua_State *L) +{ + struct lwan_request **r = luaL_checkudata(L, 1, request_metatable_name); + + return *r; +} + +LWAN_LUA_METHOD(http_version) +{ + if (request->flags & REQUEST_IS_HTTP_1_0) + lua_pushstring(L, "HTTP/1.0"); + else + lua_pushstring(L, "HTTP/1.1"); + return 1; +} + +LWAN_LUA_METHOD(http_method) +{ + lua_pushstring(L, lwan_request_get_method_str(request)); + return 1; +} + +LWAN_LUA_METHOD(http_headers) +{ + const struct lwan_request_parser_helper *helper = request->helper; + + lua_newtable(L); + + for (size_t i = 0; i < helper->n_header_start; i++) { + const char *header = helper->header_start[i]; + const char *next_header = helper->header_start[i + 1]; + const char *colon = memchr(header, ':', (size_t)(next_header - header)); + + if (!colon) + continue; + + const ptrdiff_t header_len = colon - header; + const ptrdiff_t value_len = next_header - colon - 4; + + if (header_len < 0 || value_len < 0) + continue; + + lua_pushlstring(L, header, (size_t)header_len); + lua_pushlstring(L, colon + 2, (size_t)value_len); + lua_rawset(L, -3); + } + + return 1; +} + +LWAN_LUA_METHOD(say) +{ + size_t response_str_len; + const char *response_str = lua_tolstring(L, -1, &response_str_len); + + lwan_strbuf_set_static(request->response.buffer, response_str, + response_str_len); + lwan_response_send_chunk(request); + + return 0; +} + +LWAN_LUA_METHOD(send_event) +{ + size_t event_str_len; + const char *event_str = lua_tolstring(L, -1, &event_str_len); + const char *event_name = lua_tostring(L, -2); + + lwan_strbuf_set_static(request->response.buffer, event_str, event_str_len); + lwan_response_send_event(request, event_name); + + return 0; +} + +LWAN_LUA_METHOD(set_response) +{ + size_t response_str_len; + const char *response_str = lua_tolstring(L, -1, &response_str_len); + + lwan_strbuf_set(request->response.buffer, response_str, response_str_len); + + return 0; +} + +static int request_param_getter(lua_State *L, + struct lwan_request *request, + const char *(*getter)(struct lwan_request *req, + const char *key)) +{ + const char *key_str = lua_tostring(L, -1); + const char *value = getter(request, key_str); + + if (!value) + lua_pushnil(L); + else + lua_pushstring(L, value); + + return 1; +} + +LWAN_LUA_METHOD(remote_address) +{ + char ip_buffer[INET6_ADDRSTRLEN]; + lua_pushstring(L, lwan_request_get_remote_address(request, ip_buffer)); + return 1; +} + +LWAN_LUA_METHOD(header) +{ + return request_param_getter(L, request, lwan_request_get_header); +} + +LWAN_LUA_METHOD(is_https) +{ + lua_pushboolean(L, !!(request->conn->flags & CONN_TLS)); + return 1; +} + +LWAN_LUA_METHOD(path) +{ + lua_pushlstring(L, request->url.value, request->url.len); + return 1; +} + +LWAN_LUA_METHOD(host) +{ + const char *host = lwan_request_get_host(request); + + if (host) + lua_pushstring(L, host); + else + lua_pushnil(L); + + return 1; +} + +LWAN_LUA_METHOD(query_string) +{ + if (request->helper->query_string.len) { + lua_pushlstring(L, request->helper->query_string.value, request->helper->query_string.len); + } else { + lua_pushlstring(L, "", 0); + } + return 1; +} + +LWAN_LUA_METHOD(query_param) +{ + return request_param_getter(L, request, lwan_request_get_query_param); +} + +LWAN_LUA_METHOD(post_param) +{ + return request_param_getter(L, request, lwan_request_get_post_param); +} + +LWAN_LUA_METHOD(cookie) +{ + return request_param_getter(L, request, lwan_request_get_cookie); +} + +LWAN_LUA_METHOD(body) +{ + if (request->helper->body_data.len) { + lua_pushlstring(L, request->helper->body_data.value, request->helper->body_data.len); + } else { + lua_pushlstring(L, "", 0); + } + return 1; +} + +LWAN_LUA_METHOD(ws_upgrade) +{ + enum lwan_http_status status = lwan_request_websocket_upgrade(request); + + lua_pushinteger(L, status); + + return 1; +} + +LWAN_LUA_METHOD(ws_write_text) +{ + size_t data_len; + const char *data_str = lua_tolstring(L, -1, &data_len); + + lwan_strbuf_set_static(request->response.buffer, data_str, data_len); + lwan_response_websocket_write_text(request); + + return 0; +} + +LWAN_LUA_METHOD(ws_write_binary) +{ + size_t data_len; + const char *data_str = lua_tolstring(L, -1, &data_len); + + lwan_strbuf_set_static(request->response.buffer, data_str, data_len); + lwan_response_websocket_write_binary(request); + + return 0; +} + +LWAN_LUA_METHOD(ws_write) +{ + size_t data_len; + const char *data_str = lua_tolstring(L, -1, &data_len); + + lwan_strbuf_set_static(request->response.buffer, data_str, data_len); + + for (size_t i = 0; i < data_len; i++) { + if ((signed char)data_str[i] < 0) { + lwan_response_websocket_write_binary(request); + return 0; + } + } + + lwan_response_websocket_write_text(request); + return 0; +} + +LWAN_LUA_METHOD(ws_read) +{ + int r; + + /* FIXME: maybe return a table {status=r, content=buf}? */ + + r = lwan_response_websocket_read(request); + switch (r) { + case 0: + lua_pushlstring(L, lwan_strbuf_get_buffer(request->response.buffer), + lwan_strbuf_get_length(request->response.buffer)); + break; + case ENOTCONN: + case EAGAIN: + lua_pushinteger(L, r); + break; + default: + lua_pushinteger(L, ENOMSG); + break; + } + + return 1; +} + +static bool append_key_value(struct lwan_request *request, + lua_State *L, + struct coro *coro, + struct lwan_key_value_array *arr, + char *key, + int value_index) +{ + size_t len; + const char *lua_value = lua_tolstring(L, value_index, &len); + char *value = coro_memdup(coro, lua_value, len + 1); + + if (strcaseequal_neutral(key, "Content-Type")) { + request->response.mime_type = value; + } else { + struct lwan_key_value *kv; + + kv = lwan_key_value_array_append(arr); + if (!kv) + return false; + + kv->key = key; + kv->value = value; + } + + return value != NULL; +} + +LWAN_LUA_METHOD(set_headers) +{ + const int table_index = 2; + const int key_index = -2; + const int value_index = -1; + struct lwan_key_value_array *headers; + struct coro *coro = request->conn->coro; + struct lwan_key_value *kv; + + if (request->flags & RESPONSE_SENT_HEADERS) + goto out; + + if (!lua_istable(L, table_index)) + goto out; + + headers = coro_lwan_key_value_array_new(request->conn->coro); + if (!headers) + goto out; + + for (lua_pushnil(L); lua_next(L, table_index) != 0; lua_pop(L, 1)) { + char *key; + + if (lua_type(L, key_index) != LUA_TSTRING) + continue; + + key = coro_strdup(request->conn->coro, lua_tostring(L, key_index)); + if (!key) + goto out; + + switch (lua_type(L, value_index)) { + case LUA_TSTRING: + if (!append_key_value(request, L, coro, headers, key, value_index)) + goto out; + break; + case LUA_TTABLE: + for (lua_pushnil(L); lua_next(L, value_index - 1) != 0; lua_pop(L, 1)) { + if (!lua_isstring(L, value_index)) + continue; + if (!append_key_value(request, L, coro, headers, key, + value_index)) + goto out; + } + break; + } + } + + kv = lwan_key_value_array_append(headers); + if (!kv) + goto out; + kv->key = kv->value = NULL; + + request->response.headers = lwan_key_value_array_get_array(headers); + lua_pushinteger(L, (lua_Integer)headers->base.elements); + return 1; + +out: + lua_pushnil(L); + return 1; +} + +LWAN_LUA_METHOD(sleep) +{ + lua_Integer ms = lua_tointeger(L, -1); + + lwan_request_sleep(request, (uint64_t)ms); + + return 0; +} + +LWAN_LUA_METHOD(request_id) +{ + lua_pushfstring(L, "%016lx", lwan_request_get_id(request)); + return 1; +} + +LWAN_LUA_METHOD(request_date) +{ + lua_pushstring(L, request->conn->thread->date.date); + return 1; +} + +#define FOR_EACH_LOG_FUNCTION(X) X(info) X(warning) X(error) X(critical) X(debug) + +#define IMPLEMENT_FUNCTION(name) \ + static int lwan_lua_log_##name(lua_State *L) \ + { \ + size_t log_str_len = 0; \ + const char *log_str = lua_tolstring(L, -1, &log_str_len); \ + if (log_str_len) { \ + lwan_status_##name("%.*s", (int)log_str_len, log_str); \ + (void)log_str_len; \ + (void)log_str; \ + } \ + return 0; \ + } +FOR_EACH_LOG_FUNCTION(IMPLEMENT_FUNCTION) +#undef IMPLEMENT_FUNCTION + +static int luaopen_log(lua_State *L) +{ + static const char *metatable_name = "Lwan.log"; +#define LOG_FUNCTION(name) {#name, lwan_lua_log_##name}, + static const struct luaL_Reg functions[] = { + FOR_EACH_LOG_FUNCTION(LOG_FUNCTION) + {} + }; +#undef LOG_FUNCTION + + luaL_newmetatable(L, metatable_name); + luaL_register(L, metatable_name, functions); + + return 0; +} + +DEFINE_ARRAY_TYPE(lwan_lua_method_array, luaL_reg) + +LWAN_LAZY_GLOBAL(luaL_reg *, lua_methods) +{ + struct lwan_lua_method_array methods; + const struct lwan_lua_method_info *info; + luaL_reg *r; + + lwan_lua_method_array_init(&methods); + + LWAN_SECTION_FOREACH(lwan_lua_method, info) { + r = lwan_lua_method_array_append(&methods); + if (!r) { + lwan_status_critical("Could not register Lua method `%s`", + info->name); + } + + r->name = info->name; + r->func = info->func; + } + + r = lwan_lua_method_array_append(&methods); + if (!r) + lwan_status_critical("Could not add Lua method sentinel"); + + r->name = NULL; + r->func = NULL; + + return lwan_lua_method_array_get_array(&methods); +} + +const char *lwan_lua_state_last_error(lua_State *L) +{ + return lua_tostring(L, -1); +} + +lua_State *lwan_lua_create_state(const char *script_file, const char *script) +{ + lua_State *L; + + L = luaL_newstate(); + if (UNLIKELY(!L)) + return NULL; + + luaL_openlibs(L); + luaopen_log(L); + + luaL_newmetatable(L, request_metatable_name); + luaL_register(L, NULL, lua_methods()); + lua_setfield(L, -1, "__index"); + + if (script_file) { + if (UNLIKELY(luaL_dofile(L, script_file) != 0)) { + lwan_status_error("Error opening Lua script %s: %s", script_file, + lua_tostring(L, -1)); + goto close_lua_state; + } + } else if (script) { + if (UNLIKELY(luaL_dostring(L, script) != 0)) { + lwan_status_error("Error evaluating Lua script %s", + lua_tostring(L, -1)); + goto close_lua_state; + } + } else { + lwan_status_error("Either file or inline script has to be provided"); + goto close_lua_state; + } + + return L; + +close_lua_state: + lua_close(L); + return NULL; +} + +void lwan_lua_state_push_request(lua_State *L, struct lwan_request *request) +{ + struct lwan_request **userdata = + lua_newuserdata(L, sizeof(struct lwan_request *)); + + *userdata = request; + luaL_getmetatable(L, request_metatable_name); + lua_setmetatable(L, -2); +} diff --git a/src/lib/lwan-lua.h b/src/lib/lwan-lua.h new file mode 100644 index 000000000..dde1a0880 --- /dev/null +++ b/src/lib/lwan-lua.h @@ -0,0 +1,53 @@ +/* + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include + +struct lwan_request; + +struct lwan_lua_method_info { + const char *name; + int (*func)(); +}; + +#define LWAN_LUA_METHOD(name_) \ + static int lwan_lua_method_##name_##_wrapper(lua_State *L); \ + static int lwan_lua_method_##name_(lua_State *L, \ + struct lwan_request *request); \ + static const struct lwan_lua_method_info \ + __attribute__((used, section(LWAN_SECTION_NAME(lwan_lua_method)))) \ + lwan_lua_method_info_##name_ = { \ + .name = #name_, .func = lwan_lua_method_##name_##_wrapper}; \ + static int lwan_lua_method_##name_##_wrapper(lua_State *L) \ + { \ + struct lwan_request *request = lwan_lua_get_request_from_userdata(L); \ + return lwan_lua_method_##name_(L, request); \ + } \ + static ALWAYS_INLINE int lwan_lua_method_##name_( \ + lua_State *L, struct lwan_request *request) + + +const char *lwan_lua_state_last_error(lua_State *L); +lua_State *lwan_lua_create_state(const char *script_file, const char *script); + +void lwan_lua_state_push_request(lua_State *L, struct lwan_request *request); + +struct lwan_request *lwan_lua_get_request_from_userdata(lua_State *L); diff --git a/src/lib/lwan-mod-fastcgi.c b/src/lib/lwan-mod-fastcgi.c new file mode 100644 index 000000000..3ccc77e72 --- /dev/null +++ b/src/lib/lwan-mod-fastcgi.c @@ -0,0 +1,1063 @@ +/* + * lwan - web server + * Copyright (c) 2022 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ +/* Implementation of a (subset of) FastCGI according to + * https://fastcgi-archives.github.io/FastCGI_Specification.html + * Some things are not fully implemented, but this seems enough to get + * Lwan to proxy PHP-FPM through FastCGI. + */ +/* FIXME: not a lot of the private APIs that needed to be added to + * support this module have a good API. Revisit this someday. */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "int-to-str.h" +#include "realpathat.h" +#include "lwan-arena.h" +#include "lwan-cache.h" +#include "lwan-io-wrappers.h" +#include "lwan-mod-fastcgi.h" +#include "lwan-strbuf.h" + +#define FASTCGI_ROLE_RESPONDER 1 + +/* FIXME: use this to pool connections later? */ +#define FASTCGI_FLAGS_KEEP_CONN 1 + +#define FASTCGI_TYPE_BEGIN_REQUEST 1 +#define FASTCGI_TYPE_END_REQUEST 3 +#define FASTCGI_TYPE_PARAMS 4 +#define FASTCGI_TYPE_STDIN 5 +#define FASTCGI_TYPE_STDOUT 6 +#define FASTCGI_TYPE_STDERR 7 + +struct private_data { + union { + struct sockaddr_un un_addr; + struct sockaddr_in in_addr; + struct sockaddr_in6 in6_addr; + struct sockaddr_storage sock_addr; + }; + sa_family_t addr_family; + socklen_t addr_size; + + struct cache *script_name_cache; + + struct lwan_value default_index; + + char *script_path; + int script_path_fd; +}; + +struct record { + uint8_t version; + uint8_t type; + uint16_t id; + uint16_t len_content; + uint8_t len_padding; + uint8_t reserved; +} __attribute__((packed)); + +static_assert(sizeof(struct record) == 8, "Sane record size"); + +struct begin_request_body { + uint16_t role; + uint8_t flags; + uint8_t padding[5]; +} __attribute__((packed)); + +static_assert(sizeof(struct begin_request_body) == 8, + "Sane begin_request_body size"); + +struct request_header { + struct record begin_request; + struct begin_request_body begin_request_body; + struct record begin_params; +} __attribute__((packed)); + +struct script_name_cache_entry { + struct cache_entry base; + char *script_name; + char *script_filename; +}; + +static void close_fd(void *data) +{ + int fd = (int)(intptr_t)data; + + close(fd); +} + +static void add_param_len(struct lwan_strbuf *strbuf, + const char *key, + size_t len_key, + const char *value, + size_t len_value) +{ + /* FIXME: these should be enabled for release builds, too! */ + assert(len_key <= INT_MAX); + assert(len_value <= INT_MAX); + + if (len_key <= 127) { + lwan_strbuf_append_char(strbuf, (char)len_key); + } else { + uint32_t len_net = htonl((uint32_t)len_key) | 1u << 31; + lwan_strbuf_append_str(strbuf, (char *)&len_net, 4); + } + + if (len_value <= 127) { + lwan_strbuf_append_char(strbuf, (char)len_value); + } else { + uint32_t len_net = htonl((uint32_t)len_value) | 1u << 31; + lwan_strbuf_append_str(strbuf, (char *)&len_net, 4); + } + + lwan_strbuf_append_str(strbuf, key, len_key); + lwan_strbuf_append_str(strbuf, value, len_value); +} + +static inline void +add_param(struct lwan_strbuf *strbuf, const char *key, const char *value) +{ + return add_param_len(strbuf, key, strlen(key), value, strlen(value)); +} + +static inline void +add_int_param(struct lwan_strbuf *strbuf, const char *key, ssize_t value) +{ + size_t len; + char buffer[INT_TO_STR_BUFFER_SIZE]; + char *p = int_to_string(value, buffer, &len); + + return add_param_len(strbuf, key, strlen(key), p, len); +} + +static struct cache_entry *create_script_name(const void *keyptr, + void *context, + void *create_contex + __attribute__((unused))) +{ + struct private_data *pd = context; + struct script_name_cache_entry *entry; + const struct lwan_value *url = keyptr; + int r; + + entry = malloc(sizeof(*entry)); + if (!entry) + return NULL; + + if (!url->len) + url = &pd->default_index; + + /* SCRIPT_NAME */ + r = asprintf(&entry->script_name, "/%.*s", (int)url->len, url->value); + if (r < 0) + goto free_entry; + + /* SCRIPT_FILENAME */ + char temp[PATH_MAX]; + r = snprintf(temp, sizeof(temp), "%s/%.*s", pd->script_path, (int)url->len, + url->value); + if (r < 0 || r >= (int)sizeof(temp)) + goto free_script_name; + + entry->script_filename = + realpathat(pd->script_path_fd, pd->script_path, temp, NULL); + if (!entry->script_filename) + goto free_script_name; + + if (strncmp(entry->script_filename, pd->script_path, + strlen(pd->script_path))) + goto free_script_filename; + + return &entry->base; + +free_script_filename: + free(entry->script_filename); +free_script_name: + free(entry->script_name); +free_entry: + free(entry); + + return NULL; +} + +static void destroy_script_name(struct cache_entry *entry, + void *context __attribute__((unused))) +{ + struct script_name_cache_entry *snce = + (struct script_name_cache_entry *)entry; + + free(snce->script_name); + free(snce->script_filename); + free(snce); +} + +static enum lwan_http_status add_script_paths(const struct private_data *pd, + struct lwan_request *request, + struct lwan_response *response) +{ + struct script_name_cache_entry *snce = + (struct script_name_cache_entry *)cache_coro_get_and_ref_entry( + pd->script_name_cache, request->conn->coro, &request->url); + + if (snce) { + add_param(response->buffer, "SCRIPT_NAME", snce->script_name); + add_param(response->buffer, "SCRIPT_FILENAME", snce->script_filename); + return HTTP_OK; + } + + return HTTP_NOT_FOUND; +} + +static void add_header_to_strbuf(const char *header, + size_t header_len, + const char *value, + size_t value_len, + void *user_data) +{ + struct lwan_strbuf *strbuf = user_data; + return add_param_len(strbuf, header, header_len, value, value_len); +} + +static bool fill_addr_and_port(const struct lwan_request *r, + struct lwan_strbuf *strbuf) +{ + const struct lwan_thread *t = r->conn->thread; + char local_addr_buf[INET6_ADDRSTRLEN], remote_addr_buf[INET6_ADDRSTRLEN]; + struct sockaddr_storage sockaddr = {.ss_family = AF_UNSPEC}; + uint16_t local_port, remote_port; + socklen_t len = sizeof(sockaddr); + const char *local_addr, *remote_addr; + int listen_fd; + + if (r->conn->flags & CONN_TLS) { + listen_fd = t->tls_listen_fd; + add_param(strbuf, "HTTPS", "on"); + } else { + listen_fd = t->listen_fd; + add_param(strbuf, "HTTPS", ""); + } + + if (getsockname(listen_fd, (struct sockaddr *)&sockaddr, &len) < 0) + return false; + + if (sockaddr.ss_family == AF_INET6) { + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&sockaddr; + + local_addr = inet_ntop(AF_INET6, &sin6->sin6_addr, local_addr_buf, + sizeof(local_addr_buf)); + local_port = ntohs(sin6->sin6_port); + } else if (sockaddr.ss_family == AF_INET) { + struct sockaddr_in *sin = (struct sockaddr_in *)&sockaddr; + + local_addr = inet_ntop(AF_INET, &sin->sin_addr, local_addr_buf, + sizeof(local_addr_buf)); + local_port = ntohs(sin->sin_port); + } else { + return false; + } + + remote_addr = lwan_request_get_remote_address_and_port(r, remote_addr_buf, + &remote_port); + + if (!local_addr) + return false; + + if (!remote_addr) + return false; + + add_param(strbuf, "SERVER_ADDR", local_addr); + add_int_param(strbuf, "SERVER_PORT", local_port); + + add_param(strbuf, "REMOTE_ADDR", remote_addr); + add_int_param(strbuf, "REMOTE_PORT", remote_port); + + return true; +} + +static enum lwan_http_status add_params(const struct private_data *pd, + struct lwan_request *request, + struct lwan_response *response) +{ + const struct lwan_request_parser_helper *request_helper = request->helper; + struct lwan_strbuf *strbuf = response->buffer; + + /* Very compliant. Much CGI. Wow. */ + add_param(strbuf, "GATEWAY_INTERFACE", "CGI/1.1"); + + if (!fill_addr_and_port(request, strbuf)) + return HTTP_INTERNAL_ERROR; + + add_param(strbuf, "SERVER_SOFTWARE", "Lwan"); + add_param(strbuf, "SERVER_PROTOCOL", + request->flags & REQUEST_IS_HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1"); + + add_param(strbuf, "REQUEST_METHOD", lwan_request_get_method_str(request)); + + /* FIXME: Should we support PATH_INFO? This is pretty shady. See + * e.g. https://httpd.apache.org/docs/2.4/mod/core.html#acceptpathinfo */ + add_param(strbuf, "PATH_INFO", ""); + + add_param(strbuf, "DOCUMENT_URI", request->original_url.value); + add_param(strbuf, "DOCUMENT_ROOT", pd->script_path); + + const char *query_string = request_helper->query_string.value; + if (query_string) { + char *temp; + + /* FIXME: maybe we should add something that lets us + * forward a va_arg to strbuf_append_printf() instead? */ + if (asprintf(&temp, "%s?%s", request->original_url.value, + query_string) < 0) { + return HTTP_INTERNAL_ERROR; + } + + add_param(strbuf, "QUERY_STRING", query_string ? query_string : ""); + add_param(strbuf, "REQUEST_URI", temp); + + free(temp); + } else { + add_param(strbuf, "QUERY_STRING", ""); + add_param(strbuf, "REQUEST_URI", request->original_url.value); + } + + lwan_request_foreach_header_for_cgi(request, add_header_to_strbuf, strbuf); + + return HTTP_OK; +} + +static bool +handle_stdout(struct lwan_request *request, const struct record *record, int fd) +{ + size_t to_read = record->len_content; + char *buffer = lwan_strbuf_extend_unsafe(request->response.buffer, to_read); + + if (!buffer) + return false; + + while (to_read) { + ssize_t r = lwan_recv_fd(request, fd, buffer, to_read, 0); + + if (r < 0) + return false; + + to_read -= (size_t)r; + buffer += r; + } + + if (record->len_padding) { + char padding[256]; + if (lwan_recv_fd(request, fd, padding, (size_t)record->len_padding, + MSG_TRUNC) < 0) { + return false; + } + } + + return true; +} + +static bool +handle_stderr(struct lwan_request *request, const struct record *record, int fd) +{ + size_t to_read = record->len_content; + char *buffer = malloc(to_read); + + if (!buffer) + return false; + + coro_deferred buffer_free_defer = + coro_defer(request->conn->coro, free, buffer); + + for (char *p = buffer; to_read;) { + ssize_t r = lwan_recv_fd(request, fd, p, to_read, 0); + + if (r < 0) + return false; + + p += r; + to_read -= (size_t)r; + } + + lwan_status_error("FastCGI stderr output: %.*s", (int)record->len_content, + buffer); + + coro_defer_fire_and_disarm(request->conn->coro, buffer_free_defer); + + if (record->len_padding) { + char padding[256]; + if (lwan_recv_fd(request, fd, padding, (size_t)record->len_padding, + MSG_TRUNC) < 0) { + return false; + } + } + + return true; +} + +static bool discard_unknown_record(struct lwan_request *request, + const struct record *record, + int fd) +{ + char buffer[256]; + size_t to_read = (size_t)record->len_content + (size_t)record->len_padding; + + if (record->type > 11) { + /* Per the spec, 11 is the maximum (unknown type), so anything + * above it is unspecified. */ + lwan_status_warning( + "FastCGI server sent unknown/invalid record type %d", record->type); + return false; + } + + lwan_status_debug("Discarding record of type %d (%zu bytes incl. padding)", + record->type, to_read); + + while (to_read) { + ssize_t r; + + r = lwan_recv_fd(request, fd, buffer, LWAN_MIN(sizeof(buffer), to_read), + MSG_TRUNC); + if (r < 0) + return false; + + to_read -= (size_t)r; + } + + return true; +} + +DEFINE_ARRAY_TYPE_INLINEFIRST(header_array, struct lwan_key_value) + +static void reset_additional_header(void *data) +{ + struct header_array *array = data; + header_array_reset(array); +} + +static enum lwan_http_status +try_initiating_chunked_response(struct lwan_request *request) +{ + if (request->flags & REQUEST_IS_HTTP_1_0) { + /* Chunked encoding is not supported in HTTP/1.0. We don't have a + * way to buffer the responses in this module yet, so return an + * error here. */ + return HTTP_NOT_IMPLEMENTED; + } + + struct lwan_response *response = &request->response; + char *header_start[N_HEADER_START]; + char *next_request; + enum lwan_http_status status_code = HTTP_OK; + struct lwan_value buffer = lwan_strbuf_to_value(response->buffer); + + assert(!(request->flags & + (RESPONSE_CHUNKED_ENCODING | RESPONSE_SENT_HEADERS))); + + if (!memmem(buffer.value, buffer.len, "\r\n\r\n", 4)) + return HTTP_OK; + + ssize_t n_headers = lwan_find_headers(header_start, &buffer, &next_request); + if (n_headers < 0) + return HTTP_BAD_REQUEST; + + struct header_array additional_headers; + + header_array_init(&additional_headers); + coro_deferred additional_headers_reset = coro_defer(request->conn->coro, + reset_additional_header, &additional_headers); + + for (ssize_t i = 0; i < n_headers; i++) { + char *begin = header_start[i]; + char *end = header_start[i + 1]; + char *p; + + p = strchr(begin, ':'); + if (!p) /* Shouldn't happen, but... */ + continue; + *p = '\0'; + + *(end - 2) = '\0'; + + char *key = begin; + char *value = p + 2; + + if (strcaseequal_neutral(key, "X-Powered-By")) { + /* This is set by PHP-FPM. Do not advertise this for privacy + * reasons. */ + } else if (strcaseequal_neutral(key, "Content-Type")) { + response->mime_type = coro_strdup(request->conn->coro, value); + } else if (strcaseequal_neutral(key, "Status")) { + if (strlen(value) < 3) { + status_code = HTTP_INTERNAL_ERROR; + continue; + } + + value[3] = '\0'; + int status_as_int = parse_int(value, -1); + + if (status_as_int < 100 || status_as_int >= 600) + status_code = HTTP_INTERNAL_ERROR; + else + status_code = (enum lwan_http_status)status_as_int; + } else { + struct lwan_key_value *header = header_array_append(&additional_headers); + + if (!header) + goto free_array_and_disarm; + + *header = (struct lwan_key_value){.key = key, .value = value}; + } + } + + struct lwan_key_value *header = header_array_append(&additional_headers); + if (!header) + goto free_array_and_disarm; + *header = (struct lwan_key_value){}; + + if (!lwan_response_set_chunked_full(request, status_code, + header_array_get_array(&additional_headers))) { + goto free_array_and_disarm; + } + + coro_defer_fire_and_disarm(request->conn->coro, additional_headers_reset); + + char *chunk_start = header_start[n_headers]; + size_t chunk_len = buffer.len - (size_t)(chunk_start - buffer.value); + + if (chunk_len) { + struct lwan_strbuf first_chunk; + + lwan_strbuf_init(&first_chunk); + lwan_strbuf_set_static(&first_chunk, chunk_start, chunk_len); + lwan_response_send_chunk_full(request, &first_chunk); + + /* We sent the chunk using a temporary strbuf, so reset the + * actual buffer that had the headers+first chunk */ + lwan_strbuf_reset(response->buffer); + } + + return HTTP_OK; + +free_array_and_disarm: + coro_defer_fire_and_disarm(request->conn->coro, additional_headers_reset); + return HTTP_INTERNAL_ERROR; +} + +DEFINE_ARRAY_TYPE_INLINEFIRST(iovec_array, struct iovec) + +static bool build_stdin_records(struct lwan_request *request, + struct iovec_array *iovec_array, + int fcgi_fd, + const struct lwan_value *body_data) +{ + struct iovec *iovec; + + if (body_data) { + struct arena *arena = coro_arena_new(request->conn->coro); + size_t to_send = body_data->len; + char *buffer = body_data->value; + + while (to_send) { + struct record *record; + size_t block_size = LWAN_MIN(0xffffull, to_send); + + record = arena_alloc(arena, sizeof(*record)); + if (UNLIKELY(!record)) + return false; + *record = (struct record){ + .version = 1, + .type = FASTCGI_TYPE_STDIN, + .id = htons(1), + .len_content = htons((uint16_t)block_size), + }; + + iovec = iovec_array_append(iovec_array); + if (UNLIKELY(!iovec)) + return false; + *iovec = + (struct iovec){.iov_base = record, .iov_len = sizeof(*record)}; + + iovec = iovec_array_append(iovec_array); + if (UNLIKELY(!iovec)) + return false; + *iovec = (struct iovec){.iov_base = buffer, .iov_len = block_size}; + + if (iovec_array_len(iovec_array) == LWAN_ARRAY_INCREMENT) { + if (lwan_writev_fd(request, fcgi_fd, + iovec_array_get_array(iovec_array), + (int)iovec_array_len(iovec_array)) < 0) { + return false; + } + iovec_array_reset(iovec_array); + arena_reset(arena); + } + + to_send -= block_size; + buffer += block_size; + } + } + + iovec = iovec_array_append(iovec_array); + if (UNLIKELY(!iovec)) + return HTTP_INTERNAL_ERROR; + *iovec = (struct iovec){ + .iov_base = &(struct record){.version = 1, + .type = FASTCGI_TYPE_STDIN, + .id = htons(1)}, + .iov_len = sizeof(struct record), + }; + + return true; +} + +static enum lwan_http_status send_request(struct private_data *pd, + struct lwan_request *request, + struct lwan_response *response, + int fcgi_fd) +{ + enum lwan_http_status status; + + status = add_params(pd, request, response); + if (status != HTTP_OK) + return status; + + status = add_script_paths(pd, request, response); + if (status != HTTP_OK) + return status; + + if (UNLIKELY(lwan_strbuf_get_length(response->buffer) > 0xffffu)) { + /* Should not happen because DEFAULT_BUFFER_SIZE is a lot smaller + * than 65535, but check anyway. (If anything, we could send multiple + * PARAMS records, but that's very unlikely to happen anyway until + * we change how request headers are read.) */ + static_assert(DEFAULT_BUFFER_SIZE <= 0xffffu, + "only needs one PARAMS record"); + return HTTP_TOO_LARGE; + } + + struct iovec_array iovec_array; + struct iovec *iovec; + + /* This arrays should never go beyond the inlinefirst threshold, so it + * shouldn't leak -- thus requiring no defer to reset them. */ + iovec_array_init(&iovec_array); + + iovec = iovec_array_append(&iovec_array); + if (UNLIKELY(!iovec)) + return HTTP_INTERNAL_ERROR; + *iovec = (struct iovec){ + .iov_base = + &(struct request_header){ + .begin_request = + { + .version = 1, + .type = FASTCGI_TYPE_BEGIN_REQUEST, + .id = htons(1), + .len_content = + htons((uint16_t)sizeof(struct begin_request_body)), + }, + .begin_request_body = {.role = htons(FASTCGI_ROLE_RESPONDER)}, + .begin_params = + { + .version = 1, + .type = FASTCGI_TYPE_PARAMS, + .id = htons(1), + .len_content = htons( + (uint16_t)lwan_strbuf_get_length(response->buffer)), + }, + }, + .iov_len = sizeof(struct request_header), + }; + + iovec = iovec_array_append(&iovec_array); + if (UNLIKELY(!iovec)) + return HTTP_INTERNAL_ERROR; + *iovec = lwan_strbuf_to_iovec(response->buffer); + + iovec = iovec_array_append(&iovec_array); + if (UNLIKELY(!iovec)) + return HTTP_INTERNAL_ERROR; + *iovec = (struct iovec){ + .iov_base = &(struct record){.version = 1, + .type = FASTCGI_TYPE_PARAMS, + .id = htons(1)}, + .iov_len = sizeof(struct record), + }; + + if (!build_stdin_records(request, &iovec_array, fcgi_fd, + lwan_request_get_request_body(request))) { + return HTTP_INTERNAL_ERROR; + } + + if (lwan_writev_fd(request, fcgi_fd, iovec_array_get_array(&iovec_array), + (int)iovec_array_len(&iovec_array)) < 0) { + return HTTP_INTERNAL_ERROR; + } + iovec_array_reset(&iovec_array); + + lwan_strbuf_reset(response->buffer); + + return HTTP_OK; +} + +static bool try_connect(struct lwan_request *request, + int sock_fd, + struct sockaddr *sockaddr, + socklen_t socklen) +{ + if (LIKELY(!connect(sock_fd, sockaddr, socklen))) + return true; + + /* Since socket has been created in non-blocking mode, connection + * might not be completed immediately. Depending on the socket type, + * connect() might return EAGAIN or EINPROGRESS. */ + if (errno != EAGAIN && errno != EINPROGRESS) + return false; + + /* If we get any of the above errors, try checking for socket error + * codes and loop until we get no errors. We await for writing here + * because that's what the Linux man page for connect(2) says we should + * do in this case. */ + for (int try = 0; try < 10; try++) { + socklen_t sockerrnolen = (socklen_t)sizeof(int); + int sockerrno; + + lwan_request_await_write(request, sock_fd); + + if (getsockopt(sock_fd, SOL_SOCKET, SO_ERROR, &sockerrno, + &sockerrnolen) < 0) { + break; + } + + switch (sockerrno) { + case EISCONN: + case 0: + return true; + + case EAGAIN: + case EINPROGRESS: + case EINTR: + continue; + + default: + return false; + } + } + + return false; +} + +static enum lwan_http_status +fastcgi_handle_request(struct lwan_request *request, + struct lwan_response *response, + void *instance) +{ + struct private_data *pd = instance; + enum lwan_http_status status; + int remaining_tries_for_chunked = 10; + int fcgi_fd; + + fcgi_fd = + socket(pd->addr_family, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (fcgi_fd < 0) + return HTTP_INTERNAL_ERROR; + + coro_defer(request->conn->coro, close_fd, (void *)(intptr_t)fcgi_fd); + + if (!try_connect(request, fcgi_fd, (struct sockaddr *)&pd->sock_addr, + pd->addr_size)) { + return HTTP_UNAVAILABLE; + } + + status = send_request(pd, request, response, fcgi_fd); + if (status != HTTP_OK) + return status; + + /* FIXME: the header parser starts at the \r from an usual + * HTTP request with the verb line, etc. */ + lwan_strbuf_append_char(response->buffer, '\r'); + + request->flags |= RESPONSE_NO_EXPIRES; + + while (true) { + struct record record; + ssize_t r; + + r = lwan_recv_fd(request, fcgi_fd, &record, sizeof(record), 0); + if (r < 0) + return HTTP_UNAVAILABLE; + if (r != (ssize_t)sizeof(record)) + return HTTP_INTERNAL_ERROR; + + record.len_content = ntohs(record.len_content); + record.id = htons(record.id); + + switch (record.type) { + case FASTCGI_TYPE_STDOUT: + if (!handle_stdout(request, &record, fcgi_fd)) + return HTTP_INTERNAL_ERROR; + + /* Fallthrough */ + + case FASTCGI_TYPE_END_REQUEST: + if (request->flags & RESPONSE_CHUNKED_ENCODING) { + if (lwan_strbuf_get_length(response->buffer) != 0) { + /* Avoid buffering all the records from the FastCGI + * server and send chunks as we get them if we know we're + * already responding with chunked encoding. */ + lwan_response_send_chunk(request); + } + } else { + /* See if we can parse the headers at this point; if we can, + * then we can also send the first chunk with the additional + * headers we just parsed from FastCGI. */ + remaining_tries_for_chunked--; + if (!remaining_tries_for_chunked) + return HTTP_UNAVAILABLE; + + status = try_initiating_chunked_response(request); + if (status != HTTP_OK) + return status; + } + + if (record.type == FASTCGI_TYPE_END_REQUEST) + return HTTP_OK; + + break; + + case FASTCGI_TYPE_STDERR: + if (!handle_stderr(request, &record, fcgi_fd)) + return HTTP_INTERNAL_ERROR; + break; + + default: + if (!discard_unknown_record(request, &record, fcgi_fd)) + return HTTP_INTERNAL_ERROR; + break; + } + } + + __builtin_unreachable(); +} + +static void *fastcgi_create(const char *prefix __attribute__((unused)), + void *user_settings) +{ + struct lwan_fastcgi_settings *settings = user_settings; + struct private_data *pd; + + if (!settings->address) { + lwan_status_error("FastCGI: `address` not specified"); + return NULL; + } + if (!settings->script_path) { + lwan_status_error("FastCGI: `script_path` not specified"); + return NULL; + } + + if (!settings->default_index) + settings->default_index = "index.php"; + + pd = malloc(sizeof(*pd)); + if (!pd) { + lwan_status_perror("FastCGI: Could not allocate memory for module"); + return NULL; + } + + pd->script_name_cache = + cache_create(create_script_name, destroy_script_name, pd, 60); + if (!pd->script_name_cache) { + lwan_status_error("FastCGI: could not create cache for script_name"); + goto free_pd; + } + + pd->default_index = (struct lwan_value){ + .value = strdup(settings->default_index), + .len = strlen(settings->default_index), + }; + if (!pd->default_index.value) { + lwan_status_error("FastCGI: could not copy default_address for module"); + goto destroy_cache; + } + + pd->script_path = realpath(settings->script_path, NULL); + if (!pd->script_path) { + lwan_status_perror("FastCGI: `script_path` of '%s' is invalid", + settings->script_path); + goto free_default_index; + } + + pd->script_path_fd = open(pd->script_path, O_PATH | O_DIRECTORY | O_CLOEXEC); + if (pd->script_path_fd < 0) { + lwan_status_perror("FastCGI: Could not open `script_path` at '%s'", + pd->script_path); + goto free_script_path; + } + + if (*settings->address == '/') { + struct stat st; + + if (stat(settings->address, &st) < 0) { + lwan_status_perror("FastCGI: `address` not found: %s", + settings->address); + goto close_script_path_fd; + } + + if (!(st.st_mode & S_IFSOCK)) { + lwan_status_error("FastCGI: `address` is not a socket: %s", + settings->address); + goto close_script_path_fd; + } + + if (strlen(settings->address) >= sizeof(pd->un_addr.sun_path)) { + lwan_status_error( + "FastCGI: `address` is too long for a sockaddr_un: %s", + settings->address); + goto close_script_path_fd; + } + + pd->addr_family = AF_UNIX; + pd->un_addr = (struct sockaddr_un){.sun_family = AF_UNIX}; + pd->addr_size = sizeof(pd->un_addr); + memcpy(pd->un_addr.sun_path, settings->address, + strlen(settings->address) + 1); + return pd; + } + + char *node, *port, *address_copy; + + address_copy = strdup(settings->address); + if (!address_copy) + goto free_address_copy; + + pd->addr_family = lwan_socket_parse_address(address_copy, &node, &port); + + int int_port = parse_int(port, -1); + if (int_port < 0 || int_port > 0xffff) { + lwan_status_error("FastCGI: Port %d is not in valid range [0-65535]", + int_port); + goto free_address_copy; + } + + switch (pd->addr_family) { + case AF_MAX: + lwan_status_error("FastCGI: Could not parse '%s' as 'address:port'", + settings->address); + goto free_address_copy; + + case AF_INET: { + struct in_addr in_addr; + + if (inet_pton(AF_INET, node, &in_addr) < 0) { + lwan_status_perror("FastCGI: Could not parse IPv4 address '%s'", + node); + goto free_address_copy; + } + + pd->in_addr = + (struct sockaddr_in){.sin_family = AF_INET, + .sin_addr = in_addr, + .sin_port = htons((uint16_t)int_port)}; + pd->addr_size = sizeof(in_addr); + free(address_copy); + return pd; + } + + case AF_INET6: { + struct in6_addr in6_addr; + + if (inet_pton(AF_INET6, node, &in6_addr) < 0) { + lwan_status_perror("FastCGI: Could not parse IPv6 address '%s'", + node); + goto free_address_copy; + } + + pd->in6_addr = + (struct sockaddr_in6){.sin6_family = AF_INET6, + .sin6_addr = in6_addr, + .sin6_port = htons((uint16_t)int_port)}; + pd->addr_size = sizeof(in6_addr); + free(address_copy); + return pd; + } + } + + lwan_status_error("FastCGI: Address '%s' isn't a valid Unix Domain Socket, IPv4, or IPv6 address", + settings->address); + +free_address_copy: + free(address_copy); +close_script_path_fd: + close(pd->script_path_fd); +free_script_path: + free(pd->script_path); +free_default_index: + free(pd->default_index.value); +destroy_cache: + cache_destroy(pd->script_name_cache); +free_pd: + free(pd); + return NULL; +} + +static void fastcgi_destroy(void *instance) +{ + struct private_data *pd = instance; + + cache_destroy(pd->script_name_cache); + free(pd->default_index.value); + close(pd->script_path_fd); + free(pd->script_path); + free(pd); +} + +static void *fastcgi_create_from_hash(const char *prefix, + const struct hash *hash) +{ + struct lwan_fastcgi_settings settings = { + .address = hash_find(hash, "address"), + .script_path = hash_find(hash, "script_path"), + .default_index = hash_find(hash, "default_index"), + }; + return fastcgi_create(prefix, &settings); +} + +static const struct lwan_module module = { + .create = fastcgi_create, + .create_from_hash = fastcgi_create_from_hash, + .destroy = fastcgi_destroy, + .handle_request = fastcgi_handle_request, +}; + +LWAN_REGISTER_MODULE(fastcgi, &module); diff --git a/src/lib/lwan-mod-fastcgi.h b/src/lib/lwan-mod-fastcgi.h new file mode 100644 index 000000000..2954f7d37 --- /dev/null +++ b/src/lib/lwan-mod-fastcgi.h @@ -0,0 +1,48 @@ +/* + * lwan - web server + * Copyright (c) 2022 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#if defined(__cplusplus) +extern "C" { +#endif + +#include "lwan.h" + +struct lwan_fastcgi_settings { + const char *address; + const char *script_path; + const char *default_index; +}; + +LWAN_MODULE_FORWARD_DECL(fastcgi); + +#define FASTCGI(socket_path_, script_path_, default_index_) \ + .module = LWAN_MODULE_REF(fastcgi), \ + .args = ((struct lwan_fastcgi_settings[]){{ \ + .socket_path = socket_path_, \ + .script_path = script_path_, \ + .default_index = default_index_, \ + }}), \ + .flags = (enum lwan_handler_flags)0 + +#if defined(__cplusplus) +} +#endif diff --git a/src/lib/lwan-mod-lua.c b/src/lib/lwan-mod-lua.c new file mode 100644 index 000000000..44a830f60 --- /dev/null +++ b/src/lib/lwan-mod-lua.c @@ -0,0 +1,319 @@ +/* + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "lwan-array.h" +#include "lwan-cache.h" +#include "lwan-config.h" +#include "lwan-lua.h" +#include "lwan-mod-lua.h" + +struct lwan_lua_priv { + char *default_type; + char *script_file; + char *script; + pthread_key_t cache_key; + unsigned cache_period; +}; + +struct lwan_lua_state { + struct cache_entry base; + lua_State *L; +}; + +static struct cache_entry *state_create(const void *key __attribute__((unused)), + void *cache_ctx, + void *create_ctx __attribute__((unused))) +{ + struct lwan_lua_priv *priv = cache_ctx; + struct lwan_lua_state *state = malloc(sizeof(*state)); + + if (UNLIKELY(!state)) + return NULL; + + state->L = lwan_lua_create_state(priv->script_file, priv->script); + if (LIKELY(state->L)) + return (struct cache_entry *)state; + + free(state); + return NULL; +} + +static void state_destroy(struct cache_entry *entry, + void *context __attribute__((unused))) +{ + struct lwan_lua_state *state = (struct lwan_lua_state *)entry; + + lua_close(state->L); + free(state); +} + +static struct cache *get_or_create_cache(struct lwan_lua_priv *priv) +{ + struct cache *cache = pthread_getspecific(priv->cache_key); + + if (UNLIKELY(!cache)) { + lwan_status_debug("Creating cache for this thread"); + cache = + cache_create(state_create, state_destroy, priv, priv->cache_period); + if (UNLIKELY(!cache)) + lwan_status_error("Could not create cache"); + /* FIXME: This cache instance leaks: store it somewhere and + * free it on module shutdown */ + pthread_setspecific(priv->cache_key, cache); + } + + return cache; +} + +static void unref_thread(void *data1, void *data2) +{ + lua_State *L = data1; + int thread_ref = (int)(intptr_t)data2; + + luaL_unref(L, LUA_REGISTRYINDEX, thread_ref); +} + +static ALWAYS_INLINE struct lwan_value +get_handle_prefix(struct lwan_request *request) +{ +#define ENTRY(s) {.value = s, .len = sizeof(s) - 1} +#define GEN_TABLE_ENTRY(upper, lower, mask, constant, probability) \ + [REQUEST_METHOD_##upper] = ENTRY("handle_" #lower "_"), + + static const struct lwan_value method2name[REQUEST_METHOD_MASK] = { + FOR_EACH_REQUEST_METHOD(GEN_TABLE_ENTRY) + }; + +#undef GEN_TABLE_ENTRY +#undef ENTRY + + return method2name[lwan_request_get_method(request)]; +} + +static bool get_handler_function(lua_State *L, struct lwan_request *request) +{ + char handler_name[128]; + struct lwan_value handle_prefix = get_handle_prefix(request); + + if (UNLIKELY(!handle_prefix.len)) + return false; + if (UNLIKELY(request->url.len >= sizeof(handler_name) - handle_prefix.len)) + return false; + + char *url; + size_t url_len; + if (request->url.len) { + url = strndupa(request->url.value, request->url.len); + + for (char *c = url; *c; c++) { + if (*c == '/') { + *c = '\0'; + break; + } + + if (UNLIKELY(!isalnum(*c) && *c != '_')) + return false; + } + + url_len = strlen(url); + } else { + url = "root"; + url_len = 4; + } + + size_t total_len; + if (UNLIKELY(__builtin_add_overflow(handle_prefix.len, url_len, &total_len))) + return false; + if (UNLIKELY(total_len > sizeof(handler_name) - 1)) + return false; + + char *method_name = mempcpy(handler_name, handle_prefix.value, handle_prefix.len); + memcpy(method_name, url, url_len + 1); + + lua_getglobal(L, handler_name); + if (lua_isfunction(L, -1)) + return true; + + lua_pop(L, 1); + lua_getglobal(L, "handle"); + return lua_isfunction(L, -1); +} + +static lua_State *push_newthread(lua_State *L, struct coro *coro) +{ + lua_State *L1 = lua_newthread(L); + + if (UNLIKELY(!L1)) + return NULL; + + int thread_ref = luaL_ref(L, LUA_REGISTRYINDEX); + coro_defer2(coro, unref_thread, L, (void *)(intptr_t)thread_ref); + + return L1; +} + +static enum lwan_http_status lua_handle_request(struct lwan_request *request, + struct lwan_response *response, + void *instance) +{ + struct lwan_lua_priv *priv = instance; + + struct cache *cache = get_or_create_cache(priv); + if (UNLIKELY(!cache)) + return HTTP_INTERNAL_ERROR; + + struct lwan_lua_state *state = + (struct lwan_lua_state *)cache_coro_get_and_ref_entry( + cache, request->conn->coro, ""); + if (UNLIKELY(!state)) + return HTTP_NOT_FOUND; + + lua_State *L = push_newthread(state->L, request->conn->coro); + if (UNLIKELY(!L)) + return HTTP_INTERNAL_ERROR; + + if (UNLIKELY(!get_handler_function(L, request))) + return HTTP_NOT_FOUND; + + int n_arguments = 1; + lwan_lua_state_push_request(L, request); + response->mime_type = priv->default_type; + while (true) { + switch (lua_resume(L, n_arguments)) { + case LUA_YIELD: + coro_yield(request->conn->coro, CONN_CORO_YIELD); + n_arguments = 0; + break; + case 0: + if (lua_isnil(L, -1)) + return HTTP_OK; + + if (lua_isnumber(L, -1)) { + lua_Integer code = lua_tointeger(L, -1); + + if (code >= 100 && code <= 999) + return (enum lwan_http_status)code; + } + + return HTTP_INTERNAL_ERROR; + default: + lwan_status_error("Error from Lua script: %s", lua_tostring(L, -1)); + return HTTP_INTERNAL_ERROR; + } + } +} + +static void *lua_create(const char *prefix __attribute__((unused)), void *data) +{ + struct lwan_lua_settings *settings = data; + struct lwan_lua_priv *priv; + + priv = calloc(1, sizeof(*priv)); + if (!priv) { + lwan_status_error("Could not allocate memory for private Lua struct"); + return NULL; + } + + priv->default_type = + strdup(settings->default_type ? settings->default_type : "text/plain"); + if (!priv->default_type) { + lwan_status_perror("strdup"); + goto error; + } + + if (settings->script) { + priv->script = strdup(settings->script); + if (!priv->script) { + lwan_status_perror("strdup"); + goto error; + } + } else if (settings->script_file) { + priv->script_file = strdup(settings->script_file); + if (!priv->script_file) { + lwan_status_perror("strdup"); + goto error; + } + } else { + lwan_status_error("No Lua script_file or script provided"); + goto error; + } + + if (pthread_key_create(&priv->cache_key, NULL)) { + lwan_status_perror("pthread_key_create"); + goto error; + } + + priv->cache_period = settings->cache_period; + + return priv; + +error: + free(priv->script_file); + free(priv->default_type); + free(priv->script); + free(priv); + + return NULL; +} + +static void lua_destroy(void *instance) +{ + struct lwan_lua_priv *priv = instance; + + if (priv) { + pthread_key_delete(priv->cache_key); + free(priv->default_type); + free(priv->script_file); + free(priv->script); + free(priv); + } +} + +static void *lua_create_from_hash(const char *prefix, const struct hash *hash) +{ + struct lwan_lua_settings settings = { + .default_type = hash_find(hash, "default_type"), + .script_file = hash_find(hash, "script_file"), + .cache_period = parse_time_period(hash_find(hash, "cache_period"), 15), + .script = hash_find(hash, "script") + }; + + return lua_create(prefix, &settings); +} + +static const struct lwan_module module = { + .create = lua_create, + .create_from_hash = lua_create_from_hash, + .destroy = lua_destroy, + .handle_request = lua_handle_request, + .flags = HANDLER_EXPECTS_BODY_DATA, +}; + +LWAN_REGISTER_MODULE(lua, &module); diff --git a/common/lwan-lua.h b/src/lib/lwan-mod-lua.h similarity index 79% rename from common/lwan-lua.h rename to src/lib/lwan-mod-lua.h index f3063f84c..9c45bced9 100644 --- a/common/lwan-lua.h +++ b/src/lib/lwan-mod-lua.h @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -21,18 +21,18 @@ #include "lwan.h" -struct lwan_lua_settings_t { +struct lwan_lua_settings { const char *default_type; const char *script_file; + const char *script; unsigned int cache_period; }; +LWAN_MODULE_FORWARD_DECL(lua); + #define LUA(default_type_) \ - .module = lwan_module_lua(), \ - .args = ((struct lwan_lua_t[]) {{ \ + .module = LWAN_MODULE_REF(lua), \ + .args = ((struct lwan_lua_settings[]) {{ \ .default_type = default_type_ \ }}), \ .flags = 0 - -const lwan_module_t *lwan_module_lua(void); - diff --git a/src/lib/lwan-mod-redirect.c b/src/lib/lwan-mod-redirect.c new file mode 100644 index 000000000..d586054e5 --- /dev/null +++ b/src/lib/lwan-mod-redirect.c @@ -0,0 +1,115 @@ +/* + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include + +#include "lwan-private.h" +#include "lwan-mod-redirect.h" + +struct redirect_priv { + char *to; + enum lwan_http_status code; +}; + +static enum lwan_http_status +redirect_handle_request(struct lwan_request *request, + struct lwan_response *response, + void *instance) +{ + struct redirect_priv *priv = instance; + struct lwan_key_value headers[] = {{"Location", priv->to}, {}}; + + response->headers = + coro_memdup(request->conn->coro, headers, sizeof(headers)); + + return response->headers ? priv->code : HTTP_INTERNAL_ERROR; +} + +static void *redirect_create(const char *prefix __attribute__((unused)), + void *instance) +{ + struct lwan_redirect_settings *settings = instance; + struct redirect_priv *priv = malloc(sizeof(*priv)); + + if (!priv) + return NULL; + + priv->to = strdup(settings->to); + if (!priv->to) { + free(priv); + return NULL; + } + + priv->code = settings->code; + + return priv; +} + +static void redirect_destroy(void *data) +{ + struct redirect_priv *priv = data; + + if (priv) { + free(priv->to); + free(priv); + } +} + +static enum lwan_http_status parse_http_code(const char *code, + enum lwan_http_status fallback) +{ + const char *known; + int as_int; + + if (!code) + return fallback; + + as_int = parse_int(code, 999); + if (as_int == 999) + return fallback; + + known = lwan_http_status_as_string_with_code((enum lwan_http_status)as_int); + if (!strncmp(known, "999", 3)) + return fallback; + + return (enum lwan_http_status)as_int; +} + +static void *redirect_create_from_hash(const char *prefix, + const struct hash *hash) +{ + struct lwan_redirect_settings settings = { + .to = hash_find(hash, "to"), + .code = + parse_http_code(hash_find(hash, "code"), HTTP_MOVED_PERMANENTLY), + }; + + return redirect_create(prefix, &settings); +} + +static const struct lwan_module module = { + .create = redirect_create, + .create_from_hash = redirect_create_from_hash, + .destroy = redirect_destroy, + .handle_request = redirect_handle_request, +}; + +LWAN_REGISTER_MODULE(redirect, &module); diff --git a/src/lib/lwan-mod-redirect.h b/src/lib/lwan-mod-redirect.h new file mode 100644 index 000000000..aa203edaa --- /dev/null +++ b/src/lib/lwan-mod-redirect.h @@ -0,0 +1,40 @@ +/* + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include "lwan.h" + +struct lwan_redirect_settings { + char *to; + enum lwan_http_status code; +}; + +LWAN_MODULE_FORWARD_DECL(redirect) + +#define REDIRECT_CODE(to_, code_) \ + .module = LWAN_MODULE_REF(redirect), \ + .args = ((struct lwan_redirect_settings[]) {{ \ + .to = (to_), \ + .code = (code_), \ + }}), \ + .flags = (enum lwan_handler_flags)0 + +#define REDIRECT(to_) REDIRECT_CODE((to_), HTTP_MOVED_PERMANENTLY) diff --git a/src/lib/lwan-mod-response.c b/src/lib/lwan-mod-response.c new file mode 100644 index 000000000..0fa2ef870 --- /dev/null +++ b/src/lib/lwan-mod-response.c @@ -0,0 +1,79 @@ +/* + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include + +#include "lwan-private.h" +#include "lwan-mod-response.h" + +static enum lwan_http_status +response_handle_request(struct lwan_request *request __attribute__((unused)), + struct lwan_response *response __attribute__((unused)), + void *instance) +{ + return (enum lwan_http_status)(intptr_t)instance; +} + +static void *response_create(const char *prefix __attribute__((unused)), + void *instance) +{ + struct lwan_response_settings *settings = instance; + + const char *valid_code = + lwan_http_status_as_string_with_code(settings->code); + if (!strncmp(valid_code, "999 ", 4)) { + lwan_status_error("Code %d isn't a known HTTP status code", + settings->code); + return NULL; + } + + return (void *)(uintptr_t)settings->code; +} + +static void *response_create_from_hash(const char *prefix, + const struct hash *hash) +{ + const char *code = hash_find(hash, "code"); + + if (!code) { + lwan_status_error("`code` not supplied"); + return NULL; + } + + int code_as_int = parse_int(code, -1); + if (code_as_int < 0) { + lwan_status_error("Couldn't parse `code` as an integer"); + return NULL; + } + + struct lwan_response_settings settings = { + .code = (enum lwan_http_status)code_as_int, + }; + return response_create(prefix, &settings); +} + +static const struct lwan_module module = { + .create = response_create, + .create_from_hash = response_create_from_hash, + .handle_request = response_handle_request, +}; + +LWAN_REGISTER_MODULE(response, &module); diff --git a/common/lwan-redirect.h b/src/lib/lwan-mod-response.h similarity index 70% rename from common/lwan-redirect.h rename to src/lib/lwan-mod-response.h index d74967504..e57b766c2 100644 --- a/common/lwan-redirect.h +++ b/src/lib/lwan-mod-response.h @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -21,16 +21,15 @@ #include "lwan.h" -struct lwan_redirect_settings_t { - char *to; +struct lwan_response_settings { + enum lwan_http_status code; }; -#define REDIRECT(to_) \ - .module = lwan_module_redirect(), \ - .args = ((struct lwan_redirect_settings_t[]) {{ \ - .to = to_ \ +LWAN_MODULE_FORWARD_DECL(response) + +#define RESPONSE(code_) \ + .module = LWAN_MODULE_REF(response), \ + .args = ((struct lwan_response_settings[]) {{ \ + .code = code_ \ }}), \ .flags = 0 - -const lwan_module_t *lwan_module_redirect(void); - diff --git a/src/lib/lwan-mod-rewrite.c b/src/lib/lwan-mod-rewrite.c new file mode 100644 index 000000000..d9d304262 --- /dev/null +++ b/src/lib/lwan-mod-rewrite.c @@ -0,0 +1,993 @@ +/* + * lwan - web server + * Copyright (c) 2015 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "patterns.h" +#include "lwan-array.h" +#include "lwan-mod-rewrite.h" +#include "lwan-strbuf.h" + +#ifdef LWAN_HAVE_LUA +#include +#include +#include + +#include "lwan-lua.h" +#endif + +enum pattern_flag { + PATTERN_HANDLE_REWRITE = 1 << 0, + PATTERN_HANDLE_REDIRECT = 1 << 1, + PATTERN_HANDLE_MASK = PATTERN_HANDLE_REWRITE | PATTERN_HANDLE_REDIRECT, + + PATTERN_EXPAND_LWAN = 1 << 2, + PATTERN_EXPAND_LUA = 1 << 3, + PATTERN_EXPAND_MASK = PATTERN_EXPAND_LWAN | PATTERN_EXPAND_LUA, + + PATTERN_COND_COOKIE = 1 << 4, + PATTERN_COND_ENV_VAR = 1 << 5, + PATTERN_COND_STAT = 1 << 6, + PATTERN_COND_QUERY_VAR = 1 << 7, + PATTERN_COND_POST_VAR = 1 << 8, + PATTERN_COND_HEADER = 1 << 9, + PATTERN_COND_LUA = 1 << 10, + PATTERN_COND_METHOD = 1 << 11, + PATTERN_COND_ACCEPT_ENCODING = 1 << 12, + PATTERN_COND_PROXIED = 1 << 13, + PATTERN_COND_HTTP10 = 1 << 14, + PATTERN_COND_HAS_QUERY_STRING = 1 << 15, + PATTERN_COND_HTTPS = 1 << 16, + PATTERN_COND_BACKREF = 1 << 17, + PATTERN_COND_MASK = PATTERN_COND_COOKIE | PATTERN_COND_ENV_VAR | + PATTERN_COND_STAT | PATTERN_COND_QUERY_VAR | + PATTERN_COND_POST_VAR | PATTERN_COND_HEADER | + PATTERN_COND_LUA | PATTERN_COND_METHOD | + PATTERN_COND_ACCEPT_ENCODING | + PATTERN_COND_PROXIED | PATTERN_COND_HTTP10 | + PATTERN_COND_HAS_QUERY_STRING | + PATTERN_COND_HTTPS | PATTERN_COND_BACKREF, + + PATTERN_COND_STAT__HAS_IS_FILE = 1 << 18, + PATTERN_COND_STAT__HAS_IS_DIR = 1 << 19, + PATTERN_COND_STAT__IS_FILE = 1 << 20, + PATTERN_COND_STAT__IS_DIR = 1 << 21, + + PATTERN_COND_STAT__FILE_CHECK = + PATTERN_COND_STAT__HAS_IS_FILE | PATTERN_COND_STAT__IS_FILE, + PATTERN_COND_STAT__DIR_CHECK = + PATTERN_COND_STAT__HAS_IS_DIR | PATTERN_COND_STAT__IS_DIR, + + PATTERN_COND_HTTPS__IS_HTTPS = 1 << 22, +}; + +struct pattern { + char *pattern; + char *expand_pattern; + struct { + struct lwan_key_value cookie; + struct lwan_key_value env_var; + struct lwan_key_value query_var; + struct lwan_key_value post_var; + struct lwan_key_value header; + struct { + char *path; + } stat; + struct { + char *script; + } lua; + struct { + int index; + char *str; + } backref; + enum lwan_request_flags request_flags; + /* FIXME: Use pahole to find alignment holes? */ + } condition; + enum pattern_flag flags; +}; + +DEFINE_ARRAY_TYPE(pattern_array, struct pattern) + +struct private_data { + struct pattern_array patterns; +}; + +static enum lwan_http_status module_redirect_to(struct lwan_request *request, + const char *url) +{ + const struct lwan_key_value headers[] = { + {"Location", coro_strdup(request->conn->coro, url)}, + {}, + }; + + request->response.headers = + coro_memdup(request->conn->coro, headers, sizeof(headers)); + + if (LIKELY(headers[0].value && request->response.headers)) + return HTTP_MOVED_PERMANENTLY; + + return HTTP_INTERNAL_ERROR; +} + +static enum lwan_http_status module_rewrite_as(struct lwan_request *request, + const char *url) +{ + request->url.value = coro_strdup(request->conn->coro, url); + + if (UNLIKELY(!request->url.value)) + return HTTP_INTERNAL_ERROR; + + request->url.len = strlen(request->url.value); + request->original_url = request->url; + request->flags |= RESPONSE_URL_REWRITTEN; + + return HTTP_OK; +} + +#define MAX_INT_DIGITS (3 * sizeof(int)) + +static __attribute__((noinline)) int parse_int_len(const char *s, size_t len, + int default_value) +{ + if (UNLIKELY(len > MAX_INT_DIGITS)) + return default_value; + + return parse_int(strndupa(s, len), default_value); +} + +static const char *expand_string(const char *expand_pattern, + const char *orig, + char buffer[static PATH_MAX], + const struct str_find *sf, + int captures) +{ + struct lwan_strbuf strbuf; + const char *ptr; + + ptr = strchr(expand_pattern, '%'); + if (!ptr) + return expand_pattern; + + if (!lwan_strbuf_init_with_fixed_buffer(&strbuf, buffer, PATH_MAX)) + return NULL; + + do { + size_t index_len = strspn(ptr + 1, "0123456789"); + + if (ptr > expand_pattern) { + const size_t len = (size_t)(ptr - expand_pattern); + + if (UNLIKELY(!lwan_strbuf_append_str(&strbuf, expand_pattern, len))) + return NULL; + + expand_pattern += len; + } + + if (LIKELY(index_len > 0)) { + const int index = parse_int_len(ptr + 1, index_len, -1); + + if (UNLIKELY(index < 0 || index > captures)) + return NULL; + + if (UNLIKELY(!lwan_strbuf_append_str( + &strbuf, orig + sf[index].sm_so, + (size_t)(sf[index].sm_eo - sf[index].sm_so)))) + return NULL; + + expand_pattern += index_len; + } else if (UNLIKELY(!lwan_strbuf_append_char(&strbuf, '%'))) { + return NULL; + } + + expand_pattern++; + } while ((ptr = strchr(expand_pattern, '%'))); + + const size_t remaining_len = strlen(expand_pattern); + if (remaining_len && + !lwan_strbuf_append_str(&strbuf, expand_pattern, remaining_len)) + return NULL; + + if (UNLIKELY(!lwan_strbuf_get_length(&strbuf))) + return NULL; + + return lwan_strbuf_get_buffer(&strbuf); +} + +static ALWAYS_INLINE const char *expand(const struct pattern *pattern, + const char *orig, + char buffer[static PATH_MAX], + const struct str_find *sf, + int captures) +{ + return expand_string(pattern->expand_pattern, orig, buffer, sf, captures); +} + +#ifdef LWAN_HAVE_LUA +static void +lua_close_defer(void *data) +{ + lua_close((lua_State *)data); +} + +static const char *expand_lua(struct lwan_request *request, + struct pattern *pattern, const char *orig, + char buffer[static PATH_MAX], + const struct str_find *sf, int captures) +{ + const char *output; + size_t output_len; + int i; + lua_State *L; + + L = lwan_lua_create_state(NULL, pattern->expand_pattern); + if (UNLIKELY(!L)) + return NULL; + coro_defer(request->conn->coro, lua_close_defer, L); + + lua_getglobal(L, "handle_rewrite"); + if (!lua_isfunction(L, -1)) { + lwan_status_error( + "Could not obtain reference to `handle_rewrite()` function: %s", + lwan_lua_state_last_error(L)); + return NULL; + } + + lwan_lua_state_push_request(L, request); + + lua_createtable(L, captures, 0); + for (i = 0; i < captures; i++) { + lua_pushinteger(L, i); + lua_pushlstring(L, orig + sf[i].sm_so, + (size_t)(sf[i].sm_eo - sf[i].sm_so)); + lua_settable(L, -3); + } + + if (lua_pcall(L, 2, 1, 0) != 0) { + lwan_status_error("Could not execute `handle_rewrite()` function: %s", + lwan_lua_state_last_error(L)); + return NULL; + } + + output = lua_tolstring(L, -1, &output_len); + if (output_len >= PATH_MAX) { + lwan_status_error("Rewritten URL exceeds %d bytes (got %zu bytes)", + PATH_MAX, output_len); + return NULL; + } + + return memcpy(buffer, output, output_len + 1); +} +#endif + +static bool condition_matches(struct lwan_request *request, + const struct pattern *p, + const struct str_find *sf, + int captures, + char expanded_buf[static PATH_MAX]) +{ + if (LIKELY(!(p->flags & PATTERN_COND_MASK))) + return true; + + const char *url = request->url.value; + + if (p->flags & PATTERN_COND_METHOD) { + const enum lwan_request_flags method = + p->condition.request_flags & REQUEST_METHOD_MASK; + if (lwan_request_get_method(request) != method) + return false; + } + + if (p->flags & PATTERN_COND_BACKREF) { + assert(p->condition.backref.index >= 0); + assert(p->condition.backref.str); + + if (p->condition.backref.index > captures) + return false; + + const struct str_find *s = &sf[p->condition.backref.index]; + const size_t len = strlen(p->condition.backref.str); + + if ((size_t)(s->sm_eo - s->sm_so) != len) + return false; + + if (memcmp(request->url.value + s->sm_so, p->condition.backref.str, len)) + return false; + } + + if (p->flags & PATTERN_COND_HTTPS) { + bool is_tls = request->conn->flags & CONN_TLS; + if (p->flags & PATTERN_COND_HTTPS__IS_HTTPS) { + if (!is_tls) + return false; + } else if (is_tls) { + return false; + } + } + + if (p->flags & PATTERN_COND_ACCEPT_ENCODING) { + const enum lwan_request_flags accept = + p->condition.request_flags & REQUEST_ACCEPT_MASK; + if (!(lwan_request_get_accept_encoding(request) & accept)) + return false; + } + + if (p->flags & PATTERN_COND_PROXIED) { + if (!(request->flags & p->condition.request_flags & REQUEST_PROXIED)) + return false; + } + + if (p->flags & PATTERN_COND_HTTP10) { + if (!(request->flags & p->condition.request_flags & REQUEST_IS_HTTP_1_0)) + return false; + } + + if (p->flags & PATTERN_COND_HAS_QUERY_STRING) { + if (!(request->flags & p->condition.request_flags & REQUEST_HAS_QUERY_STRING)) + return false; + } + + if (p->flags & PATTERN_COND_COOKIE) { + assert(p->condition.cookie.key); + assert(p->condition.cookie.value); + + const char *cookie = + lwan_request_get_cookie(request, p->condition.cookie.key); + if (!cookie) + return false; + + const char *val = expand_string(p->condition.cookie.value, url, + expanded_buf, sf, captures); + if (!val || !streq(val, cookie)) + return false; + } + + if (p->flags & PATTERN_COND_ENV_VAR) { + assert(p->condition.env_var.key); + assert(p->condition.env_var.value); + + const char *env_var = secure_getenv(p->condition.env_var.key); + if (!env_var) + return false; + + const char *val = expand_string(p->condition.env_var.value, url, + expanded_buf, sf, captures); + if (!val || !streq(val, env_var)) + return false; + } + + if (p->flags & PATTERN_COND_QUERY_VAR) { + assert(p->condition.query_var.key); + assert(p->condition.query_var.value); + + const char *query = + lwan_request_get_query_param(request, p->condition.query_var.key); + + if (!query) + return false; + + const char *val = expand_string(p->condition.query_var.value, url, + expanded_buf, sf, captures); + if (!val || !streq(val, query)) + return false; + } + + if (p->flags & PATTERN_COND_POST_VAR) { + assert(p->condition.post_var.key); + assert(p->condition.post_var.value); + + const char *post = + lwan_request_get_post_param(request, p->condition.post_var.key); + if (!post) + return false; + + const char *val = expand_string(p->condition.post_var.value, url, + expanded_buf, sf, captures); + if (!val || !streq(val, post)) + return false; + } + + if (p->flags & PATTERN_COND_STAT) { + assert(p->condition.stat.path); + + struct stat st; + + /* FIXME: Expanding path from a user-controlled URL and use the + * resulting path to call stat(2) on could lead to some information + * disclosure vulnerability. Would require the server to be configured + * in a certain way, though. + */ + const char *path = expand_string(p->condition.stat.path, url, + expanded_buf, sf, captures); + if (!path || stat(path, &st) < 0) + return false; + + if ((p->flags & PATTERN_COND_STAT__FILE_CHECK) == + PATTERN_COND_STAT__FILE_CHECK && + !S_ISREG(st.st_mode)) + return false; + if ((p->flags & PATTERN_COND_STAT__DIR_CHECK) == + PATTERN_COND_STAT__DIR_CHECK && + !S_ISDIR(st.st_mode)) + return false; + } + +#ifdef LWAN_HAVE_LUA + if (p->flags & PATTERN_COND_LUA) { + assert(p->condition.lua.script); + + lua_State *L = lwan_lua_create_state(NULL, p->condition.lua.script); + if (!L) + return false; + coro_defer(request->conn->coro, lua_close_defer, L); + + lua_getglobal(L, "matches"); + if (!lua_isfunction(L, -1)) { + lwan_status_error( + "Could not obtain reference to `matches()` function: %s", + lwan_lua_state_last_error(L)); + return false; + } + + lwan_lua_state_push_request(L, request); + + if (lua_pcall(L, 1, 1, 0) != 0) { + lwan_status_error("Could not execute `matches()` function: %s", + lwan_lua_state_last_error(L)); + return false; + } + + if (!lua_toboolean(L, -1)) + return false; + } +#else + assert(!(p->flags & PATTERN_COND_LUA)); +#endif + + return true; +} + +static enum lwan_http_status +rewrite_handle_request(struct lwan_request *request, + struct lwan_response *response __attribute__((unused)), + void *instance) +{ + struct private_data *pd = instance; + const char *url = request->url.value; + char final_url[PATH_MAX]; + struct pattern *p; + + LWAN_ARRAY_FOREACH(&pd->patterns, p) { + struct str_find sf[MAXCAPTURES]; + const char *expanded = NULL; + const char *errmsg; + int captures; + + captures = str_find(url, p->pattern, sf, MAXCAPTURES, &errmsg); + if (captures <= 0) + continue; + + if (!condition_matches(request, p, sf, captures, final_url)) + continue; + + switch (p->flags & PATTERN_EXPAND_MASK) { +#ifdef LWAN_HAVE_LUA + case PATTERN_EXPAND_LUA: + expanded = expand_lua(request, p, url, final_url, sf, captures); + break; +#endif + case PATTERN_EXPAND_LWAN: + expanded = expand(p, url, final_url, sf, captures); + break; + } + + if (LIKELY(expanded)) { + switch (p->flags & PATTERN_HANDLE_MASK) { + case PATTERN_HANDLE_REDIRECT: + return module_redirect_to(request, expanded); + case PATTERN_HANDLE_REWRITE: + return module_rewrite_as(request, expanded); + } + } + + return HTTP_INTERNAL_ERROR; + } + + return HTTP_NOT_FOUND; +} + +static void *rewrite_create(const char *prefix __attribute__((unused)), + void *instance __attribute__((unused))) +{ + struct private_data *pd = malloc(sizeof(*pd)); + + if (!pd) + return NULL; + + pattern_array_init(&pd->patterns); + + return pd; +} + +static void rewrite_destroy(void *instance) +{ + struct private_data *pd = instance; + struct pattern *iter; + + LWAN_ARRAY_FOREACH(&pd->patterns, iter) { + free(iter->pattern); + free(iter->expand_pattern); + if (iter->flags & PATTERN_COND_COOKIE) { + free(iter->condition.cookie.key); + free(iter->condition.cookie.value); + } + if (iter->flags & PATTERN_COND_ENV_VAR) { + free(iter->condition.env_var.key); + free(iter->condition.env_var.value); + } + if (iter->flags & PATTERN_COND_QUERY_VAR) { + free(iter->condition.query_var.key); + free(iter->condition.query_var.value); + } + if (iter->flags & PATTERN_COND_POST_VAR) { + free(iter->condition.post_var.key); + free(iter->condition.post_var.value); + } + if (iter->flags & PATTERN_COND_HEADER) { + free(iter->condition.header.key); + free(iter->condition.header.value); + } + if (iter->flags & PATTERN_COND_STAT) { + free(iter->condition.stat.path); + } +#ifdef LWAN_HAVE_LUA + if (iter->flags & PATTERN_COND_LUA) { + free(iter->condition.lua.script); + } +#endif + if (iter->flags & PATTERN_COND_BACKREF) { + free(iter->condition.backref.str); + } + } + + pattern_array_reset(&pd->patterns); + free(pd); +} + +static void *rewrite_create_from_hash(const char *prefix, + const struct hash *hash + __attribute__((unused))) +{ + return rewrite_create(prefix, NULL); +} + +static void parse_condition_key_value(struct pattern *pattern, + struct lwan_key_value *key_value, + enum pattern_flag condition_type, + struct config *config, + const struct config_line *line) +{ + char *key = NULL, *value = NULL; + + while ((line = config_read_line(config))) { + switch (line->type) { + case CONFIG_LINE_TYPE_SECTION: + config_error(config, "Unexpected section: %s", line->key); + goto out; + + case CONFIG_LINE_TYPE_SECTION_END: + if (!key || !value) { + config_error(config, "Key/value has not been specified"); + goto out; + } + + *key_value = (struct lwan_key_value){key, value}; + pattern->flags |= condition_type & PATTERN_COND_MASK; + return; + + case CONFIG_LINE_TYPE_LINE: + if (key || value) { + config_error(config, + "Can only condition on a single key/value pair. " + "Currently has: %s=%s", + key, value); + goto out; + } + + key = strdup(line->key); + if (!key) { + config_error(config, + "Could not copy key while parsing condition"); + goto out; + } + + value = strdup(line->value); + if (!value) { + config_error(config, + "Could not copy value while parsing condition"); + goto out; + } + break; + } + } + +out: + free(key); + free(value); +} + +static void parse_condition_stat(struct pattern *pattern, + struct config *config, + const struct config_line *line) +{ + char *path = NULL; + bool has_is_dir = false, is_dir = false; + bool has_is_file = false, is_file = false; + + while ((line = config_read_line(config))) { + switch (line->type) { + case CONFIG_LINE_TYPE_SECTION: + config_error(config, "Unexpected section: %s", line->key); + goto out; + + case CONFIG_LINE_TYPE_SECTION_END: + if (!path) { + config_error(config, "Path not specified"); + goto out; + } + + pattern->condition.stat.path = path; + if (has_is_dir) { + pattern->flags |= PATTERN_COND_STAT__HAS_IS_DIR; + if (is_dir) + pattern->flags |= PATTERN_COND_STAT__IS_DIR; + } + if (has_is_file) { + pattern->flags |= PATTERN_COND_STAT__HAS_IS_FILE; + if (is_file) + pattern->flags |= PATTERN_COND_STAT__IS_FILE; + } + pattern->flags |= PATTERN_COND_STAT; + return; + + case CONFIG_LINE_TYPE_LINE: + if (streq(line->key, "path")) { + if (path) { + config_error(config, "Path `%s` already specified", path); + goto out; + } + path = strdup(line->value); + if (!path) { + config_error(config, "Could not copy path"); + goto out; + } + } else if (streq(line->key, "is_dir")) { + is_dir = parse_bool(line->value, false); + has_is_dir = true; + } else if (streq(line->key, "is_file")) { + is_file = parse_bool(line->value, false); + has_is_file = true; + } else { + config_error(config, "Unexpected key: %s", line->key); + goto out; + } + + break; + } + } + +out: + free(path); +} + +static void parse_condition_accept_encoding(struct pattern *pattern, + struct config *config) +{ + const struct config_line *line; + + while ((line = config_read_line(config))) { + switch (line->type) { + case CONFIG_LINE_TYPE_SECTION: + config_error(config, "Unexpected section: %s", line->key); + return; + + case CONFIG_LINE_TYPE_SECTION_END: + pattern->flags |= PATTERN_COND_ACCEPT_ENCODING; + return; + + case CONFIG_LINE_TYPE_LINE: + if (streq(line->key, "deflate")) { + if (parse_bool(line->value, false)) + pattern->condition.request_flags |= REQUEST_ACCEPT_DEFLATE; + } else if (streq(line->key, "gzip")) { + if (parse_bool(line->value, false)) + pattern->condition.request_flags |= REQUEST_ACCEPT_GZIP; + } else if (streq(line->key, "brotli")) { + if (parse_bool(line->value, false)) + pattern->condition.request_flags |= REQUEST_ACCEPT_BROTLI; + } else if (streq(line->key, "zstd")) { + if (parse_bool(line->value, false)) + pattern->condition.request_flags |= REQUEST_ACCEPT_ZSTD; + } else if (!streq(line->key, "none")) { + config_error(config, "Unsupported encoding for condition: %s", + line->key); + return; + } + break; + } + } +} + +static void parse_condition_backref(struct pattern *pattern, + struct config *config) +{ + const struct config_line *line; + int index = -1; + char *str = NULL; + + while ((line = config_read_line(config))) { + switch (line->type) { + case CONFIG_LINE_TYPE_SECTION: + config_error(config, "Unexpected section: %s", line->key); + goto out; + + case CONFIG_LINE_TYPE_SECTION_END: + if (index < 0) { + config_error(config, "Backref index not provided"); + goto out; + } + + pattern->flags |= PATTERN_COND_BACKREF; + pattern->condition.backref.index = index; + pattern->condition.backref.str = str; + return; + + case CONFIG_LINE_TYPE_LINE: + index = parse_int(line->key, -1); + if (index < 0) { + config_error(config, "Expecting backref index, got ``%s''", line->key); + goto out; + } + + free(str); + str = strdup(line->value); + if (!str) { + lwan_status_critical( + "Couldn't allocate memory for backref key"); + } + + break; + } + } + +out: + free(str); +} + +static bool get_method_from_string(struct pattern *pattern, const char *string) +{ +#define GENERATE_CMP(upper, lower, mask, constant, probability) \ + if (strcaseequal_neutral(string, #upper)) { \ + pattern->condition.request_flags |= (mask); \ + return true; \ + } + + FOR_EACH_REQUEST_METHOD(GENERATE_CMP) + +#undef GENERATE_CMP + + return false; +} + +static void parse_condition(struct pattern *pattern, + struct config *config, + const struct config_line *line) +{ + if (streq(line->value, "cookie")) { + return parse_condition_key_value(pattern, &pattern->condition.cookie, + PATTERN_COND_COOKIE, config, line); + } + if (streq(line->value, "query")) { + return parse_condition_key_value(pattern, &pattern->condition.query_var, + PATTERN_COND_QUERY_VAR, config, line); + } + if (streq(line->value, "post")) { + return parse_condition_key_value(pattern, &pattern->condition.post_var, + PATTERN_COND_POST_VAR, config, line); + } + if (streq(line->value, "environment")) { + return parse_condition_key_value(pattern, &pattern->condition.env_var, + PATTERN_COND_ENV_VAR, config, line); + } + if (streq(line->value, "header")) { + return parse_condition_key_value(pattern, &pattern->condition.header, + PATTERN_COND_HEADER, config, line); + } + if (streq(line->value, "stat")) { + return parse_condition_stat(pattern, config, line); + } + if (streq(line->value, "encoding")) { + return parse_condition_accept_encoding(pattern, config); + } + if (streq(line->value, "backref")) { + return parse_condition_backref(pattern, config); + } + + config_error(config, "Condition `%s' not supported", line->value); +} + +static bool rewrite_parse_conf_pattern(struct private_data *pd, + struct config *config, + const struct config_line *line) +{ + struct pattern *pattern; + char *redirect_to = NULL, *rewrite_as = NULL; + bool expand_with_lua = false; + + pattern = pattern_array_append0(&pd->patterns); + if (!pattern) + goto out_no_free; + + pattern->pattern = strdup(line->value); + if (!pattern->pattern) + goto out; + + while ((line = config_read_line(config))) { + switch (line->type) { + case CONFIG_LINE_TYPE_LINE: + if (streq(line->key, "redirect_to")) { + free(redirect_to); + + redirect_to = strdup(line->value); + if (!redirect_to) + goto out; + } else if (streq(line->key, "rewrite_as")) { + free(rewrite_as); + + rewrite_as = strdup(line->value); + if (!rewrite_as) + goto out; + } else if (streq(line->key, "expand_with_lua")) { + expand_with_lua = parse_bool(line->value, false); + } else if (streq(line->key, "condition_proxied")) { + if (parse_bool(line->value, false)) + pattern->flags |= PATTERN_COND_PROXIED; + } else if (streq(line->key, "condition_http_1.0")) { + if (parse_bool(line->value, false)) + pattern->flags |= PATTERN_COND_HTTP10; + } else if (streq(line->key, "condition_has_query_string")) { + if (parse_bool(line->value, false)) + pattern->flags |= PATTERN_COND_HAS_QUERY_STRING; + } else if (streq(line->key, "condition_is_https")) { + if (parse_bool(line->value, false)) + pattern->flags |= PATTERN_COND_HTTPS__IS_HTTPS; + pattern->flags |= PATTERN_COND_HTTPS; + } else if (streq(line->key, "condition_method")) { + if (!get_method_from_string(pattern, line->value)) { + config_error(config, "Unknown HTTP method: %s", line->value); + goto out; + } + pattern->flags |= PATTERN_COND_METHOD; + } else +#ifdef LWAN_HAVE_LUA + if (streq(line->key, "condition_lua")) { + pattern->condition.lua.script = strdup(line->value); + if (!pattern->condition.lua.script) + lwan_status_critical("Couldn't copy Lua script"); + pattern->flags |= PATTERN_COND_LUA; + } else +#endif + { + config_error(config, "Unexpected key: %s", line->key); + goto out; + } + break; + case CONFIG_LINE_TYPE_SECTION: + if (streq(line->key, "condition")) { + parse_condition(pattern, config, line); + } else { + config_error(config, "Unexpected section: %s", line->key); + } + break; + case CONFIG_LINE_TYPE_SECTION_END: + if (redirect_to && rewrite_as) { + config_error( + config, + "`redirect to` and `rewrite as` are mutually exclusive"); + goto out; + } + if (redirect_to) { + pattern->expand_pattern = redirect_to; + pattern->flags |= PATTERN_HANDLE_REDIRECT; + } else if (rewrite_as) { + pattern->expand_pattern = rewrite_as; + pattern->flags |= PATTERN_HANDLE_REWRITE; + } else { + config_error( + config, + "either `redirect to` or `rewrite as` are required"); + goto out; + } + if (expand_with_lua) { +#ifdef LWAN_HAVE_LUA + pattern->flags |= PATTERN_EXPAND_LUA; +#else + config_error(config, "Lwan has been built without Lua. " + "`expand_with_lua` is not available"); + goto out; +#endif + } else { + pattern->flags |= PATTERN_EXPAND_LWAN; + } + + return true; + } + } + +out: + free(pattern->pattern); + free(redirect_to); + free(rewrite_as); +out_no_free: + config_error(config, "Could not copy pattern"); + return false; +} + +static bool rewrite_parse_conf(void *instance, struct config *config) +{ + struct private_data *pd = instance; + const struct config_line *line; + + while ((line = config_read_line(config))) { + switch (line->type) { + case CONFIG_LINE_TYPE_LINE: + config_error(config, "Unknown option: %s", line->key); + break; + case CONFIG_LINE_TYPE_SECTION: + if (streq(line->key, "pattern")) { + rewrite_parse_conf_pattern(pd, config, line); + } else { + config_error(config, "Unknown section: %s", line->key); + } + break; + case CONFIG_LINE_TYPE_SECTION_END: + break; + } + } + + return !config_last_error(config); +} + +static const struct lwan_module module = { + .create = rewrite_create, + .create_from_hash = rewrite_create_from_hash, + .parse_conf = rewrite_parse_conf, + .destroy = rewrite_destroy, + .handle_request = rewrite_handle_request, + .flags = HANDLER_CAN_REWRITE_URL +}; + +LWAN_REGISTER_MODULE(rewrite, &module); diff --git a/common/lwan-rewrite.h b/src/lib/lwan-mod-rewrite.h similarity index 83% rename from common/lwan-rewrite.h rename to src/lib/lwan-mod-rewrite.h index fdadba697..0ce48e689 100644 --- a/common/lwan-rewrite.h +++ b/src/lib/lwan-mod-rewrite.h @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -21,5 +21,4 @@ #include "lwan.h" -const lwan_module_t *lwan_module_rewrite(void); - +LWAN_MODULE_FORWARD_DECL(rewrite) diff --git a/src/lib/lwan-mod-serve-files.c b/src/lib/lwan-mod-serve-files.c new file mode 100644 index 000000000..4680fc9c7 --- /dev/null +++ b/src/lib/lwan-mod-serve-files.c @@ -0,0 +1,1387 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "hash.h" +#include "realpathat.h" +#include "lwan-cache.h" +#include "lwan-config.h" +#include "lwan-io-wrappers.h" +#include "lwan-mod-serve-files.h" +#include "lwan-template.h" +#include "int-to-str.h" + +#include "servefile-data.h" + +#if defined(LWAN_HAVE_BROTLI) +#include +#endif + +#if defined(LWAN_HAVE_ZSTD) +#include +#endif + +#define MMAP_SIZE_THRESHOLD 16384 +#define MINCORE_CALL_THRESHOLD 10 +#define MINCORE_VEC_LEN(len) (((len) + PAGE_SIZE - 1) / PAGE_SIZE) + +static const struct lwan_key_value deflate_compression_hdr[] = { + {"Content-Encoding", "deflate"}, {} +}; +static const struct lwan_key_value gzip_compression_hdr[] = { + {"Content-Encoding", "gzip"}, {} +}; +#if defined(LWAN_HAVE_BROTLI) +static const struct lwan_key_value br_compression_hdr[] = { + {"Content-Encoding", "br"}, {} +}; +#endif +#if defined(LWAN_HAVE_ZSTD) +static const struct lwan_key_value zstd_compression_hdr[] = { + {"Content-Encoding", "zstd"}, {} +}; +#endif + +static const int open_mode = O_RDONLY | O_NONBLOCK | O_CLOEXEC; + +struct file_cache_entry; + +enum serve_files_priv_flags { + SERVE_FILES_SERVE_PRECOMPRESSED = 1 << 0, + SERVE_FILES_AUTO_INDEX = 1 << 1, + SERVE_FILES_AUTO_INDEX_README = 1 << 2, +}; + +struct serve_files_priv { + struct cache *cache; + + char *root_path; + size_t root_path_len; + int root_fd; + + enum serve_files_priv_flags flags; + + const char *index_html; + char *prefix; + + struct lwan_tpl *directory_list_tpl; + + size_t read_ahead; +}; + +struct cache_funcs { + enum lwan_http_status (*serve)(struct lwan_request *request, void *data); + bool (*init)(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st); + void (*free)(struct file_cache_entry *ce); +}; + +struct mmap_cache_data { + struct lwan_value uncompressed; + struct lwan_value gzip; + struct lwan_value deflated; +#if defined(LWAN_HAVE_BROTLI) + struct lwan_value brotli; +#endif +#if defined(LWAN_HAVE_ZSTD) + struct lwan_value zstd; +#endif + unsigned int mincore_call_threshold; +}; + +struct sendfile_cache_data { + struct { + int fd; + size_t size; + } compressed, uncompressed; +}; + +struct dir_list_cache_data { + struct lwan_strbuf rendered; + struct lwan_value deflated; +#if defined(LWAN_HAVE_BROTLI) + struct lwan_value brotli; +#endif +}; + +struct redir_cache_data { + char *redir_to; +}; + +struct file_cache_entry { + struct cache_entry base; + + struct { + char string[30]; + time_t integer; + } last_modified; + + const char *mime_type; + const struct cache_funcs *funcs; + + union { + struct mmap_cache_data mmap_cache_data; + struct sendfile_cache_data sendfile_cache_data; + struct dir_list_cache_data dir_list_cache_data; + struct redir_cache_data redir_cache_data; + }; +}; + +struct file_list { + const char *full_path; + const char *rel_path; + const char *readme; + struct { + coro_function_t generator; + + const char *icon; + const char *icon_alt; + const char *name; + const char *type; + + int size; + const char *unit; + + const char *zebra_class; + const char *slash_if_dir; + } file_list; +}; + +static int directory_list_generator(struct coro *coro, void *data); + +static bool mmap_init(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st); +static void mmap_free(struct file_cache_entry *ce); +static enum lwan_http_status mmap_serve(struct lwan_request *request, + void *data); + +static bool sendfile_init(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st); +static void sendfile_free(struct file_cache_entry *ce); +static enum lwan_http_status sendfile_serve(struct lwan_request *request, + void *data); + +static bool dirlist_init(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st); +static void dirlist_free(struct file_cache_entry *ce); +static enum lwan_http_status dirlist_serve(struct lwan_request *request, + void *data); + +static bool redir_init(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st); +static void redir_free(struct file_cache_entry *ce); +static enum lwan_http_status redir_serve(struct lwan_request *request, + void *data); + +static const struct cache_funcs mmap_funcs = { + .init = mmap_init, + .free = mmap_free, + .serve = mmap_serve, +}; + +static const struct cache_funcs sendfile_funcs = { + .init = sendfile_init, + .free = sendfile_free, + .serve = sendfile_serve, +}; + +static const struct cache_funcs dirlist_funcs = { + .init = dirlist_init, + .free = dirlist_free, + .serve = dirlist_serve, +}; + +static const struct cache_funcs redir_funcs = { + .init = redir_init, + .free = redir_free, + .serve = redir_serve, +}; + +#undef TPL_STRUCT +#define TPL_STRUCT struct file_list +static const struct lwan_var_descriptor file_list_desc[] = { + TPL_VAR_STR_ESCAPE(full_path), + TPL_VAR_STR_ESCAPE(rel_path), + TPL_VAR_STR_ESCAPE(readme), + TPL_VAR_SEQUENCE(file_list, + directory_list_generator, + ((const struct lwan_var_descriptor[]){ + TPL_VAR_STR(file_list.icon), + TPL_VAR_STR(file_list.icon_alt), + TPL_VAR_STR(file_list.name), + TPL_VAR_STR(file_list.type), + TPL_VAR_INT(file_list.size), + TPL_VAR_STR(file_list.unit), + TPL_VAR_STR(file_list.zebra_class), + TPL_VAR_STR(file_list.slash_if_dir), + TPL_VAR_SENTINEL, + })), + TPL_VAR_SENTINEL, +}; + +static int directory_list_generator(struct coro *coro, void *data) +{ + static const char *zebra_classes[] = {"odd", "even"}; + struct file_list *fl = data; + struct dirent *entry; + int zebra_class = 0; + DIR *dir; + int fd; + + dir = opendir(fl->full_path); + if (!dir) + return 0; + + fd = dirfd(dir); + if (fd < 0) + goto out; + + while ((entry = readdir(dir))) { + struct stat st; + + if (entry->d_name[0] == '.') + continue; + + if (fstatat(fd, entry->d_name, &st, 0) < 0) + continue; + + if (S_ISDIR(st.st_mode)) { + fl->file_list.icon = "folder"; + fl->file_list.icon_alt = "DIR"; + fl->file_list.type = "directory"; + fl->file_list.slash_if_dir = "/"; + } else if (S_ISREG(st.st_mode)) { + fl->file_list.icon = "file"; + fl->file_list.icon_alt = "FILE"; + fl->file_list.type = + lwan_determine_mime_type_for_file_name(entry->d_name); + fl->file_list.slash_if_dir = ""; + } else { + continue; + } + + if (st.st_size < 1024) { + fl->file_list.size = (int)st.st_size; + fl->file_list.unit = "B"; + } else if (st.st_size < 1024 * 1024) { + fl->file_list.size = (int)(st.st_size / 1024); + fl->file_list.unit = "KiB"; + } else if (st.st_size < 1024 * 1024 * 1024) { + fl->file_list.size = (int)(st.st_size / (1024 * 1024)); + fl->file_list.unit = "MiB"; + } else { + fl->file_list.size = (int)(st.st_size / (1024 * 1024 * 1024)); + fl->file_list.unit = "GiB"; + } + + fl->file_list.name = entry->d_name; + fl->file_list.zebra_class = zebra_classes[zebra_class++ % 2]; + + if (coro_yield(coro, 1)) + break; + } + +out: + closedir(dir); + return 0; +} + +static ALWAYS_INLINE bool is_compression_worthy(const size_t compressed_sz, + const size_t uncompressed_sz) +{ + /* FIXME: gzip encoding is also supported but not considered here */ + static const size_t deflated_header_size = + sizeof("Content-Encoding: deflate\r\n") - 1; + return ((compressed_sz + deflated_header_size) < uncompressed_sz); +} + +static void realloc_if_needed(struct lwan_value *value, size_t bound) +{ + if (bound > value->len) { + char *tmp = realloc(value->value, value->len); + + if (tmp) + value->value = tmp; + } +} + +static void deflate_value(const struct lwan_value *uncompressed, + struct lwan_value *compressed) +{ + const unsigned long bound = compressBound(uncompressed->len); + + compressed->len = bound; + + if (UNLIKELY(!(compressed->value = malloc(bound)))) + goto error_zero_out; + + if (UNLIKELY(compress((Bytef *)compressed->value, &compressed->len, + (Bytef *)uncompressed->value, + uncompressed->len) != Z_OK)) + goto error_free_compressed; + + if (is_compression_worthy(compressed->len, uncompressed->len)) + return realloc_if_needed(compressed, bound); + +error_free_compressed: + free(compressed->value); + compressed->value = NULL; +error_zero_out: + compressed->len = 0; +} + +#if defined(LWAN_HAVE_BROTLI) +static void brotli_value(const struct lwan_value *uncompressed, + struct lwan_value *brotli, + const struct lwan_value *deflated) +{ + const unsigned long bound = + BrotliEncoderMaxCompressedSize(uncompressed->len); + + brotli->len = bound; + + if (UNLIKELY(!(brotli->value = malloc(bound)))) + goto error_zero_out; + + if (UNLIKELY( + BrotliEncoderCompress(BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_WINDOW, + BROTLI_DEFAULT_MODE, uncompressed->len, + (uint8_t *)uncompressed->value, &brotli->len, + (uint8_t *)brotli->value) != BROTLI_TRUE)) + goto error_free_compressed; + + /* is_compression_worthy() is already called for deflate-compressed data, + * so only consider brotli-compressed data if it's worth it WRT deflate */ + if (LIKELY(brotli->len < deflated->len)) + return realloc_if_needed(brotli, bound); + +error_free_compressed: + free(brotli->value); + brotli->value = NULL; +error_zero_out: + brotli->len = 0; +} +#endif + +#if defined(LWAN_HAVE_ZSTD) +static void zstd_value(const struct lwan_value *uncompressed, + struct lwan_value *zstd, + const struct lwan_value *deflated) +{ + const size_t bound = ZSTD_compressBound(uncompressed->len); + + zstd->len = bound; + + if (UNLIKELY(!(zstd->value = malloc(zstd->len)))) + goto error_zero_out; + + zstd->len = ZSTD_compress(zstd->value, zstd->len, uncompressed->value, + uncompressed->len, 1); + if (UNLIKELY(ZSTD_isError(zstd->len))) + goto error_free_compressed; + + /* is_compression_worthy() is already called for deflate-compressed data, + * so only consider zstd-compressed data if it's worth it WRT deflate */ + if (LIKELY(zstd->len < deflated->len)) + return realloc_if_needed(zstd, bound); + +error_free_compressed: + free(zstd->value); + zstd->value = NULL; +error_zero_out: + zstd->len = 0; +} +#endif + +static void +try_readahead(const struct serve_files_priv *priv, int fd, size_t size) +{ + lwan_readahead_queue(fd, 0, LWAN_MIN(size, priv->read_ahead)); +} + +static inline bool is_world_readable(mode_t mode) +{ + const mode_t world_readable = S_IRUSR | S_IRGRP | S_IROTH; + + return (mode & world_readable) == world_readable; +} + +static int try_open_compressed(const char *relpath, + const struct serve_files_priv *priv, + const struct stat *uncompressed, + size_t *compressed_sz) +{ + char gzpath[PATH_MAX]; + struct stat st; + int ret, fd; + + /* Try to serve a compressed file using sendfile() if $FILENAME.gz exists */ + ret = snprintf(gzpath, PATH_MAX, "%s.gz", relpath); + if (UNLIKELY(ret < 0 || ret >= PATH_MAX)) + goto out; + + fd = openat(priv->root_fd, gzpath, open_mode); + if (UNLIKELY(fd < 0)) + goto out; + + ret = fstat(fd, &st); + if (UNLIKELY(ret < 0)) + goto close_and_out; + + if (UNLIKELY(st.st_mtime < uncompressed->st_mtime)) + goto close_and_out; + + if (UNLIKELY(!is_world_readable(st.st_mode))) + goto close_and_out; + + if (LIKELY(is_compression_worthy((size_t)st.st_size, + (size_t)uncompressed->st_size))) { + *compressed_sz = (size_t)st.st_size; + + try_readahead(priv, fd, *compressed_sz); + + return fd; + } + +close_and_out: + close(fd); +out: + *compressed_sz = 0; + return -ENOENT; +} + +static bool mmap_fd(const struct serve_files_priv *priv __attribute__((unused)), + int fd, + const size_t size, + struct lwan_value *value) +{ + void *ptr; + + if (UNLIKELY(fd < 0)) + goto fail; + + ptr = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0); + close(fd); + if (UNLIKELY(ptr == MAP_FAILED)) + goto fail; + + *value = (struct lwan_value){.value = ptr, .len = size}; + return true; + +fail: + *value = (struct lwan_value){}; + return false; +} + +static bool mmap_init(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st) +{ + struct mmap_cache_data *md = &ce->mmap_cache_data; + const char *path = full_path + priv->root_path_len; + int file_fd; + + file_fd = openat(priv->root_fd, path, open_mode); + if (UNLIKELY(file_fd < 0)) + return false; + if (!mmap_fd(priv, file_fd, (size_t)st->st_size, &md->uncompressed)) + return false; + lwan_madvise_queue(md->uncompressed.value, md->uncompressed.len); + + if (LIKELY(priv->flags & SERVE_FILES_SERVE_PRECOMPRESSED)) { + size_t compressed_size; + + file_fd = try_open_compressed(path, priv, st, &compressed_size); + mmap_fd(priv, file_fd, compressed_size, &md->gzip); + } else { + md->gzip = (struct lwan_value){}; + } + + md->uncompressed.len = (size_t)st->st_size; + deflate_value(&md->uncompressed, &md->deflated); +#if defined(LWAN_HAVE_BROTLI) + brotli_value(&md->uncompressed, &md->brotli, &md->deflated); +#endif +#if defined(LWAN_HAVE_ZSTD) + zstd_value(&md->uncompressed, &md->zstd, &md->deflated); +#endif + + ce->mime_type = + lwan_determine_mime_type_for_file_name(full_path + priv->root_path_len); + + md->mincore_call_threshold = MINCORE_CALL_THRESHOLD; + + return true; +} + +static bool sendfile_init(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st) +{ + struct sendfile_cache_data *sd = &ce->sendfile_cache_data; + const char *relpath = full_path + priv->root_path_len; + + ce->mime_type = lwan_determine_mime_type_for_file_name(relpath); + + sd->uncompressed.fd = openat(priv->root_fd, relpath, open_mode); + if (UNLIKELY(sd->uncompressed.fd < 0)) { + switch (errno) { + case ENFILE: + case EMFILE: + case EACCES: + /* These errors should produce responses other than 404, so + * store errno as the file descriptor. */ + sd->uncompressed.fd = sd->compressed.fd = -errno; + sd->compressed.size = sd->uncompressed.size = 0; + + return true; + } + + return false; + } + + /* If precompressed files can be served, try opening it */ + if (LIKELY(priv->flags & SERVE_FILES_SERVE_PRECOMPRESSED)) { + size_t compressed_sz; + int fd = try_open_compressed(relpath, priv, st, &compressed_sz); + + sd->compressed.fd = fd; + sd->compressed.size = compressed_sz; + } else { + sd->compressed.fd = -ENOENT; + sd->compressed.size = 0; + } + + sd->uncompressed.size = (size_t)st->st_size; + try_readahead(priv, sd->uncompressed.fd, sd->uncompressed.size); + + return true; +} + +static const char *get_rel_path(const char *full_path, + struct serve_files_priv *priv) +{ + const char *root_path = full_path + priv->root_path_len; + + if (priv->root_path_len == 1) { + /* If root path length is 1, it's actually "/". Don't skip + * the first forward slash if serving from root directory. */ + root_path--; + } + + if (*root_path) + return root_path; + + if (streq(priv->prefix, "/")) + return ""; + + return priv->prefix; +} + +static const char *dirlist_find_readme(struct lwan_strbuf *readme, + struct serve_files_priv *priv, + const char *full_path) +{ + static const char *candidates[] = {"readme", "readme.txt", "read.me", + "README.TXT", "README"}; + + if (!(priv->flags & SERVE_FILES_AUTO_INDEX_README)) + return NULL; + + for (size_t i = 0; i < N_ELEMENTS(candidates); i++) { + char readme_path[PATH_MAX]; + int r; + + r = snprintf(readme_path, PATH_MAX, "%s/%s", full_path, candidates[i]); + if (r < 0 || r >= PATH_MAX) + continue; + + if (lwan_strbuf_init_from_file(readme, readme_path)) + return lwan_strbuf_get_buffer(readme); + } + + return NULL; +} + +static bool dirlist_init(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st __attribute__((unused))) +{ + struct dir_list_cache_data *dd = &ce->dir_list_cache_data; + struct lwan_strbuf readme; + bool ret = false; + + if (!lwan_strbuf_init(&readme)) + return false; + if (!lwan_strbuf_init(&dd->rendered)) + goto out_free_readme; + + struct file_list vars = { + .full_path = full_path, + .rel_path = get_rel_path(full_path, priv), + .readme = dirlist_find_readme(&readme, priv, full_path), + }; + + if (!lwan_tpl_apply_with_buffer(priv->directory_list_tpl, &dd->rendered, + &vars)) + goto out_free_rendered; + + ce->mime_type = "text/html"; + + struct lwan_value rendered = lwan_strbuf_to_value(&dd->rendered); + deflate_value(&rendered, &dd->deflated); +#if defined(LWAN_HAVE_BROTLI) + brotli_value(&rendered, &dd->brotli, &dd->deflated); +#endif + + ret = true; + goto out_free_readme; + +out_free_rendered: + lwan_strbuf_free(&dd->rendered); +out_free_readme: + lwan_strbuf_free(&readme); + return ret; +} + +static bool redir_init(struct file_cache_entry *ce, + struct serve_files_priv *priv, + const char *full_path, + struct stat *st __attribute__((unused))) +{ + struct redir_cache_data *rd = &ce->redir_cache_data; + + return asprintf(&rd->redir_to, "%s%s/", priv->prefix, + get_rel_path(full_path, priv)) >= 0; +} + +static const struct cache_funcs *get_funcs(struct serve_files_priv *priv, + const char *key, + char *full_path, + struct stat *st) +{ + char index_html_path_buf[PATH_MAX]; + char *index_html_path = index_html_path_buf; + + if (S_ISDIR(st->st_mode)) { + size_t index_html_path_len; + + /* It is a directory. It might be the root directory (empty key), or + * something else. In either case, tack priv->index_html to the + * path. */ + if (*key == '\0') { + index_html_path = (char *)priv->index_html; + index_html_path_len = strlen(index_html_path); + } else { + /* Redirect /path to /path/. This is to help cases where there's + * something like , so that actually + * /path/../foo.png is served instead of /path../foo.png. */ + const char *key_end = key + strlen(key); + if (*(key_end - 1) != '/') + return &redir_funcs; + + int ret = snprintf(index_html_path, PATH_MAX, "%s%s", key, + priv->index_html); + if (UNLIKELY(ret < 0 || ret >= PATH_MAX)) + return NULL; + + index_html_path_len = (size_t)ret; + } + + /* See if it exists. */ + if (fstatat(priv->root_fd, index_html_path, st, 0) < 0) { + if (UNLIKELY(errno != ENOENT)) + return NULL; + + if (LIKELY(priv->flags & SERVE_FILES_AUTO_INDEX)) { + /* If it doesn't, we want to generate a directory list. */ + return &dirlist_funcs; + } + + /* Auto index is disabled. */ + return NULL; + } + + /* Only serve world-readable indexes. */ + if (UNLIKELY(!is_world_readable(st->st_mode))) + return NULL; + + /* If it does, we want its full path. */ + if (UNLIKELY(priv->root_path_len + index_html_path_len + 1 >= PATH_MAX)) + return NULL; + + strncpy(full_path + priv->root_path_len, index_html_path, + PATH_MAX - priv->root_path_len); + } + + /* Only serve regular files. */ + if (UNLIKELY(!S_ISREG(st->st_mode))) + return NULL; + + /* It's not a directory: choose the fastest way to serve the file + * judging by its size. */ + if (st->st_size < MMAP_SIZE_THRESHOLD) + return &mmap_funcs; + + return &sendfile_funcs; +} + +static struct file_cache_entry * +create_cache_entry_from_funcs(struct serve_files_priv *priv, + const char *full_path, + struct stat *st, + const struct cache_funcs *funcs) +{ + struct file_cache_entry *fce; + + fce = malloc(sizeof(*fce)); + if (UNLIKELY(!fce)) + return NULL; + + if (LIKELY(funcs->init(fce, priv, full_path, st))) { + fce->funcs = funcs; + return fce; + } + + free(fce); + + if (funcs != &mmap_funcs) + return NULL; + + return create_cache_entry_from_funcs(priv, full_path, st, &sendfile_funcs); +} + +static void destroy_cache_entry(struct cache_entry *entry, + void *context __attribute__((unused))) +{ + struct file_cache_entry *fce = (struct file_cache_entry *)entry; + + fce->funcs->free(fce); + free(fce); +} + +static struct cache_entry *create_cache_entry(const void *key, + void *cache_ctx, + void *create_ctx + __attribute__((unused))) +{ + struct serve_files_priv *priv = cache_ctx; + struct file_cache_entry *fce; + struct stat st; + const struct cache_funcs *funcs; + char full_path[PATH_MAX]; + + if (UNLIKELY( + !realpathat2(priv->root_fd, priv->root_path, key, full_path, &st))) + return NULL; + + if (UNLIKELY(!is_world_readable(st.st_mode))) + return NULL; + + if (UNLIKELY(strncmp(full_path, priv->root_path, priv->root_path_len))) + return NULL; + + funcs = get_funcs(priv, key, full_path, &st); + if (UNLIKELY(!funcs)) + return NULL; + + fce = create_cache_entry_from_funcs(priv, full_path, &st, funcs); + if (UNLIKELY(!fce)) + return NULL; + + if (UNLIKELY(lwan_format_rfc_time(st.st_mtime, fce->last_modified.string) < + 0)) { + destroy_cache_entry((struct cache_entry *)fce, NULL); + return NULL; + } + fce->last_modified.integer = st.st_mtime; + + return (struct cache_entry *)fce; +} + +static void mmap_free(struct file_cache_entry *fce) +{ + struct mmap_cache_data *md = &fce->mmap_cache_data; + + munmap(md->uncompressed.value, md->uncompressed.len); + if (md->gzip.value) + munmap(md->gzip.value, md->gzip.len); + free(md->deflated.value); +#if defined(LWAN_HAVE_BROTLI) + free(md->brotli.value); +#endif +#if defined(LWAN_HAVE_ZSTD) + free(md->zstd.value); +#endif +} + +static void sendfile_free(struct file_cache_entry *fce) +{ + struct sendfile_cache_data *sd = &fce->sendfile_cache_data; + + if (sd->compressed.fd >= 0) + close(sd->compressed.fd); + if (sd->uncompressed.fd >= 0) + close(sd->uncompressed.fd); +} + +static void dirlist_free(struct file_cache_entry *fce) +{ + struct dir_list_cache_data *dd = &fce->dir_list_cache_data; + + lwan_strbuf_free(&dd->rendered); + free(dd->deflated.value); +#if defined(LWAN_HAVE_BROTLI) + free(dd->brotli.value); +#endif +} + +static void redir_free(struct file_cache_entry *fce) +{ + struct redir_cache_data *rd = &fce->redir_cache_data; + + free(rd->redir_to); +} + +static char *get_real_root_path(const char *root_path) +{ + char path_buf[PATH_MAX]; + char *path; + + path = realpath(root_path, path_buf); + if (!path) + return NULL; + + char *last_slash = strrchr(path, '/'); + if (!last_slash) + return NULL; + + if (*(last_slash + 1) == '\0') + return strdup(path); + + char *ret; + if (asprintf(&ret, "%s/", path)) + return ret; + + return NULL; +} + +static void *serve_files_create(const char *prefix, void *args) +{ + struct lwan_serve_files_settings *settings = args; + struct serve_files_priv *priv; + char *canonical_root; + int root_fd; + + if (!settings->root_path) { + lwan_status_error("root_path not specified"); + return NULL; + } + + canonical_root = get_real_root_path(settings->root_path); + if (!canonical_root) { + lwan_status_perror("Could not obtain real path of \"%s\"", + settings->root_path); + goto out_realpath; + } + + root_fd = open(canonical_root, open_mode | O_DIRECTORY | O_PATH); + if (root_fd < 0) { + lwan_status_perror("Could not open directory \"%s\"", canonical_root); + goto out_open; + } + + priv = malloc(sizeof(*priv)); + if (!priv) { + lwan_status_perror("malloc"); + goto out_malloc; + } + + priv->cache = cache_create(create_cache_entry, destroy_cache_entry, priv, + settings->cache_for); + if (!priv->cache) { + lwan_status_error("Couldn't create cache"); + goto out_cache_create; + } + + if (settings->directory_list_template) { + priv->directory_list_tpl = lwan_tpl_compile_file( + settings->directory_list_template, file_list_desc); + } else { + priv->directory_list_tpl = + lwan_tpl_compile_value_full(servefile_template_value, + file_list_desc, + LWAN_TPL_FLAG_CONST_TEMPLATE); + } + if (!priv->directory_list_tpl) { + lwan_status_error("Could not compile directory list template"); + goto out_tpl_compile; + } + + priv->prefix = strdup(prefix); + if (!priv->prefix) { + lwan_status_error("Could not copy prefix"); + goto out_tpl_prefix_copy; + } + + priv->root_path = canonical_root; + priv->root_path_len = strlen(canonical_root); + priv->root_fd = root_fd; + priv->index_html = + settings->index_html ? settings->index_html : "index.html"; + + priv->read_ahead = settings->read_ahead; + + if (settings->serve_precompressed_files) + priv->flags |= SERVE_FILES_SERVE_PRECOMPRESSED; + if (settings->auto_index) + priv->flags |= SERVE_FILES_AUTO_INDEX; + if (settings->auto_index_readme) + priv->flags |= SERVE_FILES_AUTO_INDEX_README; + + return priv; + +out_tpl_prefix_copy: +out_tpl_compile: + cache_destroy(priv->cache); +out_cache_create: + free(priv); +out_malloc: + close(root_fd); +out_open: + free(canonical_root); +out_realpath: + return NULL; +} + +static void *serve_files_create_from_hash(const char *prefix, + const struct hash *hash) +{ + struct lwan_serve_files_settings settings = { + .root_path = hash_find(hash, "path"), + .index_html = hash_find(hash, "index_path"), + .serve_precompressed_files = + parse_bool(hash_find(hash, "serve_precompressed_files"), true), + .auto_index = parse_bool(hash_find(hash, "auto_index"), true), + .directory_list_template = hash_find(hash, "directory_list_template"), + .read_ahead = (size_t)parse_long(hash_find(hash, "read_ahead"), + SERVE_FILES_READ_AHEAD_BYTES), + .auto_index_readme = + parse_bool(hash_find(hash, "auto_index_readme"), true), + .cache_for = (time_t)parse_time_period(hash_find(hash, "cache_for"), + SERVE_FILES_CACHE_FOR), + }; + + return serve_files_create(prefix, &settings); +} + +static void serve_files_destroy(void *data) +{ + struct serve_files_priv *priv = data; + + if (!priv) { + lwan_status_warning("Nothing to shutdown"); + return; + } + + lwan_tpl_free(priv->directory_list_tpl); + cache_destroy(priv->cache); + close(priv->root_fd); + free(priv->root_path); + free(priv->prefix); + free(priv); +} + +static ALWAYS_INLINE bool client_has_fresh_content(struct lwan_request *request, + time_t mtime) +{ + time_t header; + int r = lwan_request_get_if_modified_since(request, &header); + + return LIKELY(!r) ? mtime <= header : false; +} + +static size_t prepare_headers(struct lwan_request *request, + enum lwan_http_status return_status, + struct file_cache_entry *fce, + size_t size, + const struct lwan_key_value *user_hdr, + char header_buf[static DEFAULT_HEADERS_SIZE]) +{ + char content_length[INT_TO_STR_BUFFER_SIZE]; + size_t discard; + struct lwan_key_value additional_headers[4] = { + { + .key = "Last-Modified", + .value = fce->last_modified.string, + }, + { + .key = "Content-Length", + .value = uint_to_string(size, content_length, &discard), + }, + }; + + if (user_hdr) + additional_headers[2] = *user_hdr; + + return lwan_prepare_response_header_full(request, return_status, header_buf, + DEFAULT_HEADERS_SIZE, + additional_headers); +} + +static enum lwan_http_status +compute_range(struct lwan_request *request, off_t *from, off_t *to, off_t size) +{ + off_t f, t; + int r = lwan_request_get_range(request, &f, &t); + + /* No Range: header present */ + if (LIKELY(r < 0 || (f < 0 && t < 0))) { + *from = 0; + *to = size; + + return HTTP_OK; + } + + /* To must be greater than From; it doesn't make any sense to be + * equal, either. */ + if (UNLIKELY(f >= t && t >= 0)) + return HTTP_RANGE_UNSATISFIABLE; + + /* Range goes beyond the size of the file */ + if (UNLIKELY(f >= size || t >= size)) + return HTTP_RANGE_UNSATISFIABLE; + + /* t < 0: ranges from f to the file size */ + if (t < 0) { + *to = size; + } else { + if (UNLIKELY(__builtin_sub_overflow(t, f, to))) + return HTTP_RANGE_UNSATISFIABLE; + } + + *from = f; + + return HTTP_PARTIAL_CONTENT; +} + +static inline bool accepts_encoding(struct lwan_request *request, + const enum lwan_request_flags encoding) +{ + return lwan_request_get_accept_encoding(request) & encoding; +} + +static enum lwan_http_status sendfile_serve(struct lwan_request *request, + void *data) +{ + const struct lwan_key_value *compression_hdr; + struct file_cache_entry *fce = data; + struct sendfile_cache_data *sd = &fce->sendfile_cache_data; + char headers[DEFAULT_HEADERS_SIZE]; + size_t header_len; + enum lwan_http_status return_status; + off_t from, to; + size_t size; + int fd; + + if (sd->compressed.size && accepts_encoding(request, REQUEST_ACCEPT_GZIP)) { + from = 0; + to = (off_t)sd->compressed.size; + + compression_hdr = gzip_compression_hdr; + fd = sd->compressed.fd; + size = sd->compressed.size; + + return_status = HTTP_OK; + } else { + return_status = + compute_range(request, &from, &to, (off_t)sd->uncompressed.size); + if (UNLIKELY(return_status == HTTP_RANGE_UNSATISFIABLE)) + return HTTP_RANGE_UNSATISFIABLE; + + compression_hdr = NULL; + fd = sd->uncompressed.fd; + size = (size_t)(to - from); + } + if (UNLIKELY(fd < 0)) { + switch (-fd) { + case EACCES: + return HTTP_FORBIDDEN; + case EMFILE: + case ENFILE: + return HTTP_UNAVAILABLE; + default: + return HTTP_INTERNAL_ERROR; + } + } + + header_len = prepare_headers(request, return_status, fce, size, + compression_hdr, headers); + if (UNLIKELY(!header_len)) + return HTTP_INTERNAL_ERROR; + + if (lwan_request_get_method(request) == REQUEST_METHOD_HEAD) { + lwan_send(request, headers, header_len, 0); + } else { + lwan_sendfile(request, fd, from, (size_t)to, headers, header_len); + } + + return return_status; +} + +static enum lwan_http_status serve_buffer(struct lwan_request *request, + const char *mime_type, + const void *buffer, + size_t buffer_len, + const struct lwan_key_value *headers, + enum lwan_http_status status_code) +{ + request->response.mime_type = mime_type; + request->response.headers = headers; + + lwan_strbuf_set_static(request->response.buffer, buffer, buffer_len); + + return status_code; +} + +static ALWAYS_INLINE enum lwan_http_status +serve_value(struct lwan_request *request, + const char *mime_type, + const struct lwan_value *value, + const struct lwan_key_value *headers, + enum lwan_http_status status_code) +{ + return serve_buffer(request, mime_type, value->value, value->len, headers, + status_code); +} + +static ALWAYS_INLINE enum lwan_http_status +serve_value_ok(struct lwan_request *request, + const char *mime_type, + const struct lwan_value *value, + const struct lwan_key_value *headers) +{ + return serve_value(request, mime_type, value, headers, HTTP_OK); +} + +static const struct lwan_value * +mmap_best_data(struct lwan_request *request, + struct mmap_cache_data *md, + const struct lwan_key_value **header) +{ + const struct lwan_value *best = &md->uncompressed; + + *header = NULL; + +#if defined(LWAN_HAVE_ZSTD) + if (md->zstd.len && md->zstd.len < best->len && + accepts_encoding(request, REQUEST_ACCEPT_ZSTD)) { + best = &md->zstd; + *header = zstd_compression_hdr; + } +#endif + +#if defined(LWAN_HAVE_BROTLI) + if (md->brotli.len && md->brotli.len < best->len && + accepts_encoding(request, REQUEST_ACCEPT_BROTLI)) { + best = &md->brotli; + *header = br_compression_hdr; + } +#endif + + if (md->gzip.len && md->gzip.len < best->len && + accepts_encoding(request, REQUEST_ACCEPT_GZIP)) { + best = &md->gzip; + *header = gzip_compression_hdr; + } + + if (md->deflated.len && md->deflated.len < best->len && + accepts_encoding(request, REQUEST_ACCEPT_DEFLATE)) { + best = &md->deflated; + *header = deflate_compression_hdr; + } + + return best; +} + +static enum lwan_http_status mmap_serve(struct lwan_request *request, + void *data) +{ + struct file_cache_entry *fce = data; + struct mmap_cache_data *md = &fce->mmap_cache_data; + const struct lwan_key_value *compression_hdr; + const struct lwan_value *to_serve = + mmap_best_data(request, md, &compression_hdr); + + if (compression_hdr) + return serve_value_ok(request, fce->mime_type, to_serve, + compression_hdr); + +#ifdef LWAN_HAVE_MINCORE + if (ATOMIC_DEC(md->mincore_call_threshold) == 0) { + unsigned char mincore_vec[MINCORE_VEC_LEN(MMAP_SIZE_THRESHOLD)]; + + md->mincore_call_threshold = MINCORE_CALL_THRESHOLD; + + if (!mincore(to_serve->value, to_serve->len, mincore_vec)) { + const size_t pgs = MINCORE_VEC_LEN(to_serve->len); + + for (size_t pg = 0; pg < pgs; pg++) { + if (mincore_vec[pg] & 0x01) + continue; + + /* FIXME: madvise only the page that's not in core */ + lwan_madvise_queue(to_serve->value, to_serve->len); + coro_yield(request->conn->coro, CONN_CORO_WANT_WRITE); + break; + } + } + } +#endif + + off_t from, to; + enum lwan_http_status status = + compute_range(request, &from, &to, (off_t)to_serve->len); + if (status != HTTP_OK && status != HTTP_PARTIAL_CONTENT) + return status; + + return serve_buffer(request, fce->mime_type, (char *)to_serve->value + from, + (size_t)(to - from), NULL, status); +} + +static enum lwan_http_status dirlist_serve(struct lwan_request *request, + void *data) +{ + struct file_cache_entry *fce = data; + struct dir_list_cache_data *dd = &fce->dir_list_cache_data; + const char *icon = lwan_request_get_query_param(request, "icon"); + + if (!icon) { +#if defined(LWAN_HAVE_BROTLI) + if (dd->brotli.len && accepts_encoding(request, REQUEST_ACCEPT_BROTLI)) { + return serve_value_ok(request, fce->mime_type, &dd->brotli, + br_compression_hdr); + } +#endif + + if (dd->deflated.len && accepts_encoding(request, REQUEST_ACCEPT_DEFLATE)) { + return serve_value_ok(request, fce->mime_type, &dd->deflated, + deflate_compression_hdr); + } + + return serve_buffer( + request, fce->mime_type, lwan_strbuf_get_buffer(&dd->rendered), + lwan_strbuf_get_length(&dd->rendered), NULL, HTTP_OK); + } + + STRING_SWITCH (icon) { + case STR4_INT('b', 'a', 'c', 'k'): + return serve_value_ok(request, "image/gif", &back_gif_value, NULL); + + case STR4_INT('f', 'i', 'l', 'e'): + return serve_value_ok(request, "image/gif", &file_gif_value, NULL); + + case STR4_INT('f', 'o', 'l', 'd'): + return serve_value_ok(request, "image/gif", &folder_gif_value, NULL); + } + + return HTTP_NOT_FOUND; +} + +static enum lwan_http_status redir_serve(struct lwan_request *request, + void *data) +{ + struct file_cache_entry *fce = data; + struct redir_cache_data *rd = &fce->redir_cache_data; + struct lwan_key_value headers[] = {{"Location", rd->redir_to}, {}}; + + lwan_strbuf_set_staticz(request->response.buffer, rd->redir_to); + request->response.mime_type = "text/plain"; + request->response.headers = + coro_memdup(request->conn->coro, headers, sizeof(headers)); + + return request->response.headers ? HTTP_MOVED_PERMANENTLY + : HTTP_INTERNAL_ERROR; +} + +static enum lwan_http_status +serve_files_handle_request(struct lwan_request *request, + struct lwan_response *response, + void *instance) +{ + struct serve_files_priv *priv = instance; + struct file_cache_entry *fce; + struct cache_entry *ce; + + ce = cache_coro_get_and_ref_entry(priv->cache, request->conn->coro, + request->url.value); + if (UNLIKELY(!ce)) + return HTTP_NOT_FOUND; + + fce = (struct file_cache_entry *)ce; + if (client_has_fresh_content(request, fce->last_modified.integer)) + return HTTP_NOT_MODIFIED; + + if (fce->funcs->serve == sendfile_serve) { + response->mime_type = fce->mime_type; + response->stream.callback = fce->funcs->serve; + response->stream.data = fce; + + request->flags |= RESPONSE_STREAM; + + return HTTP_OK; + } + + return fce->funcs->serve(request, fce); +} + +static const struct lwan_module module = { + .create = serve_files_create, + .create_from_hash = serve_files_create_from_hash, + .destroy = serve_files_destroy, + .handle_request = serve_files_handle_request, +}; + +LWAN_REGISTER_MODULE(serve_files, &module); diff --git a/common/lwan-serve-files.h b/src/lib/lwan-mod-serve-files.h similarity index 60% rename from common/lwan-serve-files.h rename to src/lib/lwan-mod-serve-files.h index a972e246c..f4413bed8 100644 --- a/common/lwan-serve-files.h +++ b/src/lib/lwan-mod-serve-files.h @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -25,26 +25,39 @@ extern "C" { #include "lwan.h" -struct lwan_serve_files_settings_t { +#define SERVE_FILES_READ_AHEAD_BYTES (128 * 1024) +#define SERVE_FILES_CACHE_FOR 5 + +struct lwan_serve_files_settings { const char *root_path; const char *index_html; + const char *directory_list_template; + size_t read_ahead; + time_t cache_for; bool serve_precompressed_files; + bool auto_index; + bool auto_index_readme; }; +LWAN_MODULE_FORWARD_DECL(serve_files); + #define SERVE_FILES_SETTINGS(root_path_, index_html_, serve_precompressed_files_) \ - .module = lwan_module_serve_files(), \ - .args = ((struct lwan_serve_files_settings_t[]) {{ \ + .module = LWAN_MODULE_REF(serve_files), \ + .args = ((struct lwan_serve_files_settings[]) {{ \ + .read_ahead = SERVE_FILES_READ_AHEAD_BYTES, \ .root_path = root_path_, \ .index_html = index_html_, \ - .serve_precompressed_files = serve_precompressed_files_ \ + .serve_precompressed_files = serve_precompressed_files_, \ + .directory_list_template = NULL, \ + .auto_index = true, \ + .auto_index_readme = true, \ + .cache_for = SERVE_FILES_CACHE_FOR, \ }}), \ - .flags = (lwan_handler_flags_t)0 + .flags = (enum lwan_handler_flags)0 #define SERVE_FILES(root_path) \ SERVE_FILES_SETTINGS(root_path, NULL, true) -const lwan_module_t *lwan_module_serve_files(void); - #if defined (__cplusplus) } #endif diff --git a/src/lib/lwan-private.h b/src/lib/lwan-private.h new file mode 100644 index 000000000..ef81a7e1a --- /dev/null +++ b/src/lib/lwan-private.h @@ -0,0 +1,358 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include +#include + +#include "lwan.h" + +#define N_HEADER_START 64 +#define DEFAULT_BUFFER_SIZE 4096 +#define DEFAULT_HEADERS_SIZE 4096 + +#define LWAN_LAZY_GLOBAL(type_, name_) \ + static type_ lazy_global_##name_; \ + static type_ new_lazy_global_##name_(void); \ + __attribute__((cold)) static void initialize_lazy_global_##name_(void) \ + { \ + lazy_global_##name_ = new_lazy_global_##name_(); \ + } \ + static type_ name_(void) \ + { \ + static pthread_once_t once = PTHREAD_ONCE_INIT; \ + pthread_once(&once, initialize_lazy_global_##name_); \ + return lazy_global_##name_; \ + } \ + __attribute__(( \ + cold, \ + always_inline)) static inline type_ new_lazy_global_##name_(void) + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) +/* Workaround for: + * https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=15216 */ +#define LWAN_LAZY_THREAD_LOCAL(type_, name_) static inline type_ name_(void) +#else +#define LWAN_LAZY_THREAD_LOCAL(type_, name_) \ + static type_ new_lazy_thread_local_##name_(void); \ + static type_ name_(void) \ + { \ + static __thread type_ val; \ + static __thread bool initialized; \ + if (UNLIKELY(!initialized)) { \ + val = new_lazy_thread_local_##name_(); \ + initialized = true; \ + } \ + return val; \ + } \ + __attribute__((cold, \ + noinline)) static type_ new_lazy_thread_local_##name_(void) +#endif + +struct lwan_constructor_callback_info { + void (*func)(struct lwan *); + int prio; +}; + +#define LWAN_CONSTRUCTOR(name_, prio_) \ + __attribute__((no_sanitize_address)) static void lwan_constructor_##name_( \ + struct lwan *l __attribute__((unused))); \ + static const struct lwan_constructor_callback_info __attribute__(( \ + used, section(LWAN_SECTION_NAME( \ + lwan_constructor)))) lwan_constructor_info_##name_ = { \ + .func = lwan_constructor_##name_, \ + .prio = (prio_), \ + }; \ + static ALWAYS_INLINE void lwan_constructor_##name_( \ + struct lwan *l __attribute__((unused))) + +struct lwan_request_parser_helper { + struct lwan_value *buffer; /* The whole request buffer */ + char *next_request; /* For pipelined requests */ + + struct lwan_value accept_encoding; /* Accept-Encoding: */ + + struct lwan_value query_string; /* Stuff after ? and before # */ + + struct lwan_value body_data; /* Request body for POST and PUT */ + struct lwan_value content_type; /* Content-Type: for POST and PUT */ + struct lwan_value content_length; /* Content-Length: */ + + struct lwan_value connection; /* Connection: */ + + struct lwan_value host; /* Host: */ + + struct lwan_key_value_array cookies, query_params, post_params; + + char **header_start; /* Headers: n: start, n+1: end */ + size_t n_header_start; /* len(header_start) */ + + struct { /* If-Modified-Since: */ + struct lwan_value raw; + time_t parsed; + } if_modified_since; + + struct { /* Range: */ + struct lwan_value raw; + off_t from, to; + } range; + + uint64_t request_id; /* Request ID for debugging purposes */ + + time_t error_when_time; /* Time to abort request read */ + int error_when_n_packets; /* Max. number of packets */ + int urls_rewritten; /* Times URLs have been rewritten */ +}; + +struct lwan_thread { + struct lwan *lwan; + struct { + char date[30]; + char expires[30]; + } date; + int epoll_fd; + struct timeouts *wheel; + int listen_fd; + int tls_listen_fd; + unsigned int cpu; + pthread_t self; +}; + +#define LWAN_CONCAT(a_, b_) a_ ## b_ +#define LWAN_TMP_ID_DETAIL(n_) LWAN_CONCAT(lwan_tmp_id, n_) +#define LWAN_TMP_ID LWAN_TMP_ID_DETAIL(__COUNTER__) + +#define LWAN_MIN_MAX_DETAIL(a_, b_, name_a_, name_b_, op_) \ + ({ \ + const __typeof__((a_) + 0) name_a_ = (a_); \ + const __typeof__((b_) + 0) name_b_ = (b_); \ + name_a_ op_ name_b_ ? name_b_ : name_a_; \ + }) + +#define LWAN_MIN(a_, b_) LWAN_MIN_MAX_DETAIL(a_, b_, LWAN_TMP_ID, LWAN_TMP_ID, >) + +#define LWAN_MAX(a_, b_) LWAN_MIN_MAX_DETAIL(a_, b_, LWAN_TMP_ID, LWAN_TMP_ID, <) + +void lwan_set_thread_name(const char *name); + +void lwan_response_init(struct lwan *l); +void lwan_response_shutdown(struct lwan *l); + +int lwan_create_listen_socket(const struct lwan *l, + bool print_listening_msg, + bool is_https); + +void lwan_thread_init(struct lwan *l); +void lwan_thread_shutdown(struct lwan *l); + +void lwan_status_init(struct lwan *l); +void lwan_status_shutdown(struct lwan *l); + +void lwan_job_thread_init(void); +void lwan_job_thread_main_loop(void); +void lwan_job_thread_shutdown(void); +void lwan_job_add(bool (*cb)(void *data), void *data); +void lwan_job_del(bool (*cb)(void *data), void *data); + +void lwan_tables_init(void); +void lwan_tables_shutdown(void); + +void lwan_readahead_init(void); +void lwan_readahead_shutdown(void); +void lwan_readahead_queue(int fd, off_t off, size_t size); +void lwan_madvise_queue(void *addr, size_t size); + +char *lwan_strbuf_extend_unsafe(struct lwan_strbuf *s, size_t by); +bool lwan_strbuf_has_grow_buffer_failed_flag(const struct lwan_strbuf *s); + +void lwan_process_request(struct lwan *l, struct lwan_request *request); +size_t lwan_prepare_response_header_full(struct lwan_request *request, + enum lwan_http_status status, char headers[], + size_t headers_buf_size, const struct lwan_key_value *additional_headers); + +void lwan_response(struct lwan_request *request, enum lwan_http_status status); +void lwan_default_response(struct lwan_request *request, + enum lwan_http_status status); +void lwan_fill_default_response(struct lwan_strbuf *buffer, + enum lwan_http_status status); + + +const char *lwan_get_config_path(char *path_buf, size_t path_buf_len); + +uint8_t lwan_char_isspace(char ch) __attribute__((pure)); +uint8_t lwan_char_isxdigit(char ch) __attribute__((pure)); +uint8_t lwan_char_isdigit(char ch) __attribute__((pure)); +uint8_t lwan_char_isalpha(char ch) __attribute__((pure)); +uint8_t lwan_char_isalnum(char ch) __attribute__((pure)); +uint8_t lwan_char_iscgiheader(char ch) __attribute__((pure)); + +static ALWAYS_INLINE __attribute__((pure)) size_t lwan_nextpow2(size_t number) +{ +#if defined(LWAN_HAVE_BUILTIN_CLZLL) + static const int size_bits = (int)sizeof(number) * CHAR_BIT; + + if (sizeof(size_t) == sizeof(unsigned int)) { + return (size_t)1 << (size_bits - __builtin_clz((unsigned int)number)); + } else if (sizeof(size_t) == sizeof(unsigned long)) { + return (size_t)1 << (size_bits - __builtin_clzl((unsigned long)number)); + } else if (sizeof(size_t) == sizeof(unsigned long long)) { + return (size_t)1 << (size_bits - __builtin_clzll((unsigned long long)number)); + } else { + (void)size_bits; + } +#endif + + number--; + number |= number >> 1; + number |= number >> 2; + number |= number >> 4; + number |= number >> 8; + number |= number >> 16; +#if __SIZE_WIDTH__ == 64 + number |= number >> 32; +#endif + + return number + 1; +} + +#if defined(LWAN_HAVE_MBEDTLS) +#include +#include +#include + +struct lwan_tls_context { + mbedtls_ssl_config config; + mbedtls_x509_crt server_cert; + mbedtls_pk_context server_key; + + mbedtls_entropy_context entropy; + + mbedtls_ctr_drbg_context ctr_drbg; +}; +#endif + +#ifdef LWAN_HAVE_LUA +#include + +lua_State *lwan_lua_create_state(const char *script_file, const char *script); +void lwan_lua_state_push_request(lua_State *L, struct lwan_request *request); +const char *lwan_lua_state_last_error(lua_State *L); +#endif + +/* This macro is used as an attempt to convince the compiler that it should + * never elide an expression -- for instance, when writing fuzz-test or + * micro-benchmarks. */ +#define LWAN_NO_DISCARD(...) \ + do { \ + __typeof__(__VA_ARGS__) no_discard_ = __VA_ARGS__; \ + __asm__ __volatile__("" ::"g"(no_discard_) : "memory"); \ + } while (0) + +static inline void lwan_always_bzero(void *ptr, size_t len) +{ + LWAN_NO_DISCARD(memset(ptr, 0, len)); +} + +#ifdef __APPLE__ +#define SECTION_START(name_) __start_##name_[] __asm("section$start$__DATA$" #name_) +#define SECTION_END(name_) __stop_##name_[] __asm("section$end$__DATA$" #name_) +#else +#define SECTION_START(name_) __start_##name_[] +#define SECTION_END(name_) __stop_##name_[] +#endif + +#define SECTION_START_SYMBOL(section_name_, iter_) \ + ({ \ + extern const typeof(*iter_) SECTION_START(section_name_); \ + __start_##section_name_; \ + }) + +#define SECTION_STOP_SYMBOL(section_name_, iter_) \ + ({ \ + extern const typeof(*iter_) SECTION_END(section_name_); \ + __stop_##section_name_; \ + }) + +#define LWAN_SECTION_FOREACH(section_name_, iter_) \ + for (iter_ = SECTION_START_SYMBOL(section_name_, iter_); \ + iter_ < SECTION_STOP_SYMBOL(section_name_, iter_); (iter_)++) + +extern clockid_t monotonic_clock_id; + +static inline void * +lwan_aligned_alloc(size_t n, size_t alignment) +{ + void *ret; + + assert((alignment & (alignment - 1)) == 0); + assert((alignment % (sizeof(void *))) == 0); + + n = (n + alignment - 1) & ~(alignment - 1); + if (UNLIKELY(posix_memalign(&ret, alignment, n))) + return NULL; + + return ret; +} + +static ALWAYS_INLINE int lwan_calculate_n_packets(size_t total) +{ + /* 740 = 1480 (a common MTU) / 2, so that Lwan'll optimistically error out + * after ~2x number of expected packets to fully read the request body.*/ + return LWAN_MAX(5, (int)(total / 740)); +} + +long int lwan_getentropy(void *buffer, size_t buffer_len, int flags); +uint64_t lwan_random_uint64(); + +const char *lwan_http_status_as_string(enum lwan_http_status status) + __attribute__((const)) __attribute__((warn_unused_result)); +const char *lwan_http_status_as_string_with_code(enum lwan_http_status status) + __attribute__((const)) __attribute__((warn_unused_result)); +const char *lwan_http_status_as_descriptive_string(enum lwan_http_status status) + __attribute__((const)) __attribute__((warn_unused_result)); + +static ALWAYS_INLINE __attribute__((pure, warn_unused_result)) int +lwan_connection_get_fd(const struct lwan *lwan, + const struct lwan_connection *conn) +{ + return (int)(intptr_t)(conn - lwan->conns); +} + +int lwan_format_rfc_time(const time_t in, char out LWAN_ARRAY_PARAM(30)); +int lwan_parse_rfc_time(const char in LWAN_ARRAY_PARAM(30), time_t *out); + +void lwan_straitjacket_enforce_from_config(struct config *c); + +uint64_t lwan_request_get_id(struct lwan_request *request); + +ssize_t lwan_find_headers(char **header_start, struct lwan_value *buffer, + char **next_request); + +sa_family_t lwan_socket_parse_address(char *listener, char **node, char **port); + +void lwan_request_foreach_header_for_cgi(struct lwan_request *request, + void (*cb)(const char *header_name, + size_t header_len, + const char *value, + size_t value_len, + void *user_data), + void *user_data); + +bool lwan_send_websocket_ping_for_tq(struct lwan_connection *conn); diff --git a/src/lib/lwan-pubsub.c b/src/lib/lwan-pubsub.c new file mode 100644 index 000000000..ddd8de1c5 --- /dev/null +++ b/src/lib/lwan-pubsub.c @@ -0,0 +1,368 @@ +/* + * lwan - web server + * Copyright (c) 2020 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#if defined(LWAN_HAVE_EVENTFD) +#include +#endif + +#include "list.h" +#include "ringbuffer.h" +#include "lwan-private.h" + +struct lwan_pubsub_topic { + struct list_head subscribers; + pthread_rwlock_t lock; +}; + +struct lwan_pubsub_msg { + struct lwan_value value; + unsigned int refcount; +}; + +DEFINE_RING_BUFFER_TYPE(lwan_pubsub_msg_ref_ring, struct lwan_pubsub_msg *, 16) + +struct lwan_pubsub_msg_ref { + struct list_node ref; + struct lwan_pubsub_msg_ref_ring ring; +}; + +struct lwan_pubsub_subscriber { + struct list_node subscriber; + + pthread_mutex_t lock; + struct list_head msg_refs; + + int event_fd[2]; +}; + +static void lwan_pubsub_queue_init(struct lwan_pubsub_subscriber *sub) +{ + list_head_init(&sub->msg_refs); +} + +static bool lwan_pubsub_queue_put(struct lwan_pubsub_subscriber *sub, + const struct lwan_pubsub_msg *msg) +{ + struct lwan_pubsub_msg_ref *ref; + + ref = list_tail(&sub->msg_refs, struct lwan_pubsub_msg_ref, ref); + if (ref) { + /* Try putting the message in the last ringbuffer in this queue: if it's + * full, will need to allocate a new ring buffer, even if others might + * have space in them: the FIFO order must be preserved, and short of + * compacting the queue at this point -- which will eventually happen + * as it is consumed -- this is the only option. */ + if (lwan_pubsub_msg_ref_ring_try_put(&ref->ring, &msg)) + return true; + } + + ref = malloc(sizeof(*ref)); + if (!ref) + return false; + + lwan_pubsub_msg_ref_ring_init(&ref->ring); + lwan_pubsub_msg_ref_ring_put(&ref->ring, &msg); + list_add_tail(&sub->msg_refs, &ref->ref); + + return true; +} + +static struct lwan_pubsub_msg * +lwan_pubsub_queue_get(struct lwan_pubsub_subscriber *sub) +{ + struct lwan_pubsub_msg_ref *ref, *next; + + list_for_each_safe (&sub->msg_refs, ref, next, ref) { + struct lwan_pubsub_msg *msg; + + if (lwan_pubsub_msg_ref_ring_empty(&ref->ring)) { + list_del(&ref->ref); + free(ref); + continue; + } + + msg = lwan_pubsub_msg_ref_ring_get(&ref->ring); + + if (ref->ref.next != ref->ref.prev) { + /* If this segment isn't the last one, try pulling in just one + * element from the next segment, as there's space in the + * current segment now. + * + * This might lead to an empty ring buffer segment in the middle + * of the linked list. This is by design, to introduce some + * hysteresis and avoid the pathological case where malloc churn + * will happen when subscribers consume at the same rate as + * publishers are able to publish. + * + * The condition above will take care of these empty segments + * once they're dealt with, eventually compacting the queue + * completely (and ultimately reducing it to an empty list + * without any ring buffers). + */ + struct lwan_pubsub_msg_ref *next_ring; + + next_ring = container_of(ref->ref.next, struct lwan_pubsub_msg_ref, ref); + if (!lwan_pubsub_msg_ref_ring_empty(&next_ring->ring)) { + const struct lwan_pubsub_msg *next_msg; + + next_msg = lwan_pubsub_msg_ref_ring_get(&next_ring->ring); + lwan_pubsub_msg_ref_ring_put(&ref->ring, &next_msg); + } + } + + return msg; + } + + return NULL; +} + +static void lwan_pubsub_unsubscribe_internal(struct lwan_pubsub_topic *topic, + struct lwan_pubsub_subscriber *sub, + bool take_topic_lock); + +struct lwan_pubsub_topic *lwan_pubsub_new_topic(void) +{ + struct lwan_pubsub_topic *topic = calloc(1, sizeof(*topic)); + + if (!topic) + return NULL; + + list_head_init(&topic->subscribers); + pthread_rwlock_init(&topic->lock, NULL); + + return topic; +} + +void lwan_pubsub_free_topic(struct lwan_pubsub_topic *topic) +{ + struct lwan_pubsub_subscriber *iter, *next; + + pthread_rwlock_wrlock(&topic->lock); + list_for_each_safe (&topic->subscribers, iter, next, subscriber) + lwan_pubsub_unsubscribe_internal(topic, iter, false); + pthread_rwlock_unlock(&topic->lock); + + pthread_rwlock_destroy(&topic->lock); + + free(topic); +} + +void lwan_pubsub_msg_done(struct lwan_pubsub_msg *msg) +{ + if (!ATOMIC_DEC(msg->refcount)) { + free(msg->value.value); + free(msg); + } +} + +static bool lwan_pubsub_publish_value(struct lwan_pubsub_topic *topic, + const struct lwan_value value) +{ + struct lwan_pubsub_msg *msg = malloc(sizeof(*msg)); + struct lwan_pubsub_subscriber *sub; + + if (!msg) + return false; + + /* Initialize refcount to 1, so we can drop one ref after publishing to + * all subscribers. If it drops to 0, it means we didn't publish the + * message and we can free it. */ + msg->refcount = 1; + msg->value = value; + + pthread_rwlock_rdlock(&topic->lock); + list_for_each (&topic->subscribers, sub, subscriber) { + ATOMIC_INC(msg->refcount); + + /* FIXME: use trylock and a local queue to try again? */ + pthread_mutex_lock(&sub->lock); + if (!lwan_pubsub_queue_put(sub, msg)) { + lwan_status_warning("Couldn't enqueue message, dropping"); + ATOMIC_DEC(msg->refcount); + } + pthread_mutex_unlock(&sub->lock); + + if (sub->event_fd[1] < 0) { + continue; + } + while (true) { + ssize_t written = + write(sub->event_fd[1], &(uint64_t){1}, sizeof(uint64_t)); + + if (LIKELY(written == (ssize_t)sizeof(uint64_t))) + break; + + if (UNLIKELY(written < 0)) { + if (errno == EINTR || errno == EAGAIN) + continue; + lwan_status_perror("write to eventfd failed, ignoring"); + break; + } + } + } + pthread_rwlock_unlock(&topic->lock); + + lwan_pubsub_msg_done(msg); + + return true; +} + +static void *my_memdup(const void *src, size_t len) +{ + void *dup = malloc(len); + + return dup ? memcpy(dup, src, len) : NULL; +} + +bool lwan_pubsub_publish(struct lwan_pubsub_topic *topic, + const void *contents, + size_t len) +{ + const struct lwan_value value = { .value = my_memdup(contents, len), .len = len }; + + if (!value.value) + return false; + + return lwan_pubsub_publish_value(topic, value); +} + +bool lwan_pubsub_publishf(struct lwan_pubsub_topic *topic, + const char *format, + ...) +{ + char *msg; + int len; + va_list ap; + + va_start(ap, format); + len = vasprintf(&msg, format, ap); + va_end(ap); + + if (len < 0) + return false; + + const struct lwan_value value = { .value = msg, .len = (size_t)len }; + return lwan_pubsub_publish_value(topic, value); +} + +struct lwan_pubsub_subscriber * +lwan_pubsub_subscribe(struct lwan_pubsub_topic *topic) +{ + struct lwan_pubsub_subscriber *sub = calloc(1, sizeof(*sub)); + + if (!sub) + return NULL; + + sub->event_fd[0] = -1; + sub->event_fd[1] = -1; + + pthread_mutex_init(&sub->lock, NULL); + lwan_pubsub_queue_init(sub); + + pthread_rwlock_wrlock(&topic->lock); + list_add(&topic->subscribers, &sub->subscriber); + pthread_rwlock_unlock(&topic->lock); + + return sub; +} + +struct lwan_pubsub_msg *lwan_pubsub_consume(struct lwan_pubsub_subscriber *sub) +{ + struct lwan_pubsub_msg *msg; + + pthread_mutex_lock(&sub->lock); + msg = lwan_pubsub_queue_get(sub); + pthread_mutex_unlock(&sub->lock); + + if (msg && sub->event_fd[0] >= 0) { + uint64_t discard; + LWAN_NO_DISCARD(read(sub->event_fd[0], &discard, sizeof(uint64_t))); + } + + return msg; +} + +static void lwan_pubsub_unsubscribe_internal(struct lwan_pubsub_topic *topic, + struct lwan_pubsub_subscriber *sub, + bool take_topic_lock) +{ + struct lwan_pubsub_msg *iter; + + if (take_topic_lock) + pthread_rwlock_wrlock(&topic->lock); + list_del(&sub->subscriber); + if (take_topic_lock) + pthread_rwlock_unlock(&topic->lock); + + pthread_mutex_lock(&sub->lock); + while ((iter = lwan_pubsub_queue_get(sub))) + lwan_pubsub_msg_done(iter); + pthread_mutex_unlock(&sub->lock); + + pthread_mutex_destroy(&sub->lock); + + if (sub->event_fd[0] != sub->event_fd[1]) { + close(sub->event_fd[0]); + close(sub->event_fd[1]); + } else if (LIKELY(sub->event_fd[0] >= 0)) { + close(sub->event_fd[0]); + } + + free(sub); +} + +void lwan_pubsub_unsubscribe(struct lwan_pubsub_topic *topic, + struct lwan_pubsub_subscriber *sub) +{ + return (void)lwan_pubsub_unsubscribe_internal(topic, sub, true); +} + +const struct lwan_value *lwan_pubsub_msg_value(const struct lwan_pubsub_msg *msg) +{ + return &msg->value; +} + +int lwan_pubsub_get_notification_fd(struct lwan_pubsub_subscriber *sub) +{ + if (sub->event_fd[0] < 0) { +#if defined(LWAN_HAVE_EVENTFD) + int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK | EFD_SEMAPHORE); + if (efd < 0) { + return -1; + } + + sub->event_fd[0] = sub->event_fd[1] = efd; +#else + if (pipe2(sub->event_fd, O_CLOEXEC | O_NONBLOCK) < 0) { + return -1; + } +#endif + } + + return sub->event_fd[0]; +} diff --git a/src/lib/lwan-pubsub.h b/src/lib/lwan-pubsub.h new file mode 100644 index 000000000..c3e1c78fb --- /dev/null +++ b/src/lib/lwan-pubsub.h @@ -0,0 +1,48 @@ +/* + * lwan - web server + * Copyright (c) 2020 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include "lwan.h" + +struct lwan_pubsub_topic; +struct lwan_pubsub_msg; +struct lwan_pubsub_subscriber; + +struct lwan_pubsub_topic *lwan_pubsub_new_topic(void); +void lwan_pubsub_free_topic(struct lwan_pubsub_topic *topic); + +bool lwan_pubsub_publish(struct lwan_pubsub_topic *topic, + const void *contents, + size_t len); +bool lwan_pubsub_publishf(struct lwan_pubsub_topic *topic, + const char *format, + ...) __attribute__((format(printf, 2, 3))); + +struct lwan_pubsub_subscriber * +lwan_pubsub_subscribe(struct lwan_pubsub_topic *topic); +void lwan_pubsub_unsubscribe(struct lwan_pubsub_topic *topic, + struct lwan_pubsub_subscriber *sub); + +struct lwan_pubsub_msg *lwan_pubsub_consume(struct lwan_pubsub_subscriber *sub); +const struct lwan_value *lwan_pubsub_msg_value(const struct lwan_pubsub_msg *msg); +void lwan_pubsub_msg_done(struct lwan_pubsub_msg *msg); + +int lwan_pubsub_get_notification_fd(struct lwan_pubsub_subscriber *sub); diff --git a/src/lib/lwan-readahead.c b/src/lib/lwan-readahead.c new file mode 100644 index 000000000..48e1e0ca0 --- /dev/null +++ b/src/lib/lwan-readahead.c @@ -0,0 +1,244 @@ +/* + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#if defined(__linux__) && defined(O_DIRECT) && O_DIRECT +#define PIPE_DIRECT_FLAG O_DIRECT +#else +#define PIPE_DIRECT_FLAG 0 +#endif + +enum readahead_cmd { + READAHEAD, + MADVISE, + SHUTDOWN, +}; + +struct lwan_readahead_cmd { + enum readahead_cmd cmd; + union { + struct { + size_t size; + off_t off; + int fd; + } readahead; + struct { + void *addr; + size_t length; + } madvise; + }; +} __attribute__((packed)); + +static int readahead_pipe_fd[2] = {-1, -1}; +static pthread_t readahead_self; +static long page_size = PAGE_SIZE; + +#ifdef _SC_PAGESIZE +LWAN_CONSTRUCTOR(get_page_size, 0) +{ + long ps = sysconf(_SC_PAGESIZE); + + if (ps >= 0) + page_size = ps; +} +#endif + +void lwan_readahead_shutdown(void) +{ + struct lwan_readahead_cmd cmd = { + .cmd = SHUTDOWN, + }; + + if (readahead_pipe_fd[0] == readahead_pipe_fd[1] && + readahead_pipe_fd[0] == -1) + return; + + lwan_status_debug("Shutting down readahead thread"); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-result" + write(readahead_pipe_fd[1], &cmd, sizeof(cmd)); +#pragma GCC diagnostic pop + pthread_join(readahead_self, NULL); + + close(readahead_pipe_fd[0]); + close(readahead_pipe_fd[1]); + readahead_pipe_fd[0] = readahead_pipe_fd[1] = -1; +} + +void lwan_readahead_queue(int fd, off_t off, size_t size) +{ + if (size < (size_t)page_size) + return; + + struct lwan_readahead_cmd cmd = { + .readahead = {.size = size, .fd = fd, .off = off}, + .cmd = READAHEAD, + }; + + /* Readahead is just a hint. Failing to write is not an error. */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-result" + write(readahead_pipe_fd[1], &cmd, sizeof(cmd)); +#pragma GCC diagnostic pop +} + +void lwan_madvise_queue(void *addr, size_t length) +{ + if (length < (size_t)page_size) + return; + + struct lwan_readahead_cmd cmd = { + .madvise = {.addr = addr, .length = length}, + .cmd = MADVISE, + }; + + /* Madvise is just a hint. Failing to write is not an error. */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-result" + write(readahead_pipe_fd[1], &cmd, sizeof(cmd)); +#pragma GCC diagnostic pop +} + +static void *lwan_readahead_loop(void *data __attribute__((unused))) +{ + /* Idle priority for the calling thread. Magic value of `7` obtained from + * sample program in linux/Documentation/block/ioprio.txt. This is a no-op + * on anything but Linux. */ + ioprio_set(IOPRIO_WHO_PROCESS, 0, IOPRIO_PRIO_VALUE(IOPRIO_CLASS_IDLE, 7)); + + lwan_set_thread_name("readahead"); + + while (true) { + struct lwan_readahead_cmd cmd[16]; + ssize_t n_bytes = read(readahead_pipe_fd[0], cmd, sizeof(cmd)); + ssize_t cmds; + + if (UNLIKELY(n_bytes < 0)) { + if (errno == EAGAIN || errno == EINTR) + continue; + lwan_status_perror("Ignoring error while reading from pipe (%d)", + readahead_pipe_fd[0]); + continue; +#if PIPE_DIRECT_FLAG + } else if (UNLIKELY(n_bytes % (ssize_t)sizeof(cmd[0]))) { + lwan_status_warning("Ignoring readahead packet read of %zd bytes", + n_bytes); + continue; +#endif + } + + cmds = n_bytes / (ssize_t)sizeof(struct lwan_readahead_cmd); + for (ssize_t i = 0; i < cmds; i++) { + switch (cmd[i].cmd) { + case READAHEAD: + readahead(cmd[i].readahead.fd, cmd[i].readahead.off, + cmd[i].readahead.size); + break; + case MADVISE: + madvise(cmd[i].madvise.addr, cmd[i].madvise.length, + MADV_WILLNEED); + + if (cmd[i].madvise.length >= 10 * 1024) { + /* On Linux, SO_ZEROCOPY is only useful to transmit + * 10kB or more because it uses page pinning (what + * mlock(2) does!), so consider the same threshold + * here. */ + mlock(cmd[i].madvise.addr, cmd[i].madvise.length); + } + + break; + case SHUTDOWN: + goto out; + } + } + } + +out: + return NULL; +} + +void lwan_readahead_init(void) +{ + int flags; + + if (readahead_pipe_fd[0] != readahead_pipe_fd[1]) + return; + + lwan_status_debug("Initializing low priority readahead thread"); + + if (pipe2(readahead_pipe_fd, O_CLOEXEC | PIPE_DIRECT_FLAG) < 0) { + lwan_status_warning("Could not create pipe for readahead queue"); + goto disable_readahead; + } + + /* Only write side should be non-blocking. */ + flags = fcntl(readahead_pipe_fd[1], F_GETFL); + if (flags < 0) { + lwan_status_warning( + "Could not get flags for readahead pipe write side"); + goto disable_readahead_close_pipe; + } + if (fcntl(readahead_pipe_fd[1], F_SETFL, flags | O_NONBLOCK) < 0) { + lwan_status_warning( + "Could not set readahead write side to be no-blocking"); + goto disable_readahead_close_pipe; + } + + if (pthread_create(&readahead_self, NULL, lwan_readahead_loop, NULL)) { + lwan_status_warning("Could not create low-priority readahead thread"); + goto disable_readahead_close_pipe; + } + +#ifdef SCHED_IDLE + struct sched_param sched_param = {.sched_priority = 0}; + if (pthread_setschedparam(readahead_self, SCHED_IDLE, &sched_param) < 0) + lwan_status_perror( + "Could not set scheduling policy of readahead thread to idle"); +#endif /* SCHED_IDLE */ + + return; + +disable_readahead_close_pipe: + close(readahead_pipe_fd[0]); + close(readahead_pipe_fd[1]); + +disable_readahead: + /* Set these to -1 just to ensure that even if the page_size check inside + * the enqueuing functions fail, we don't write stuff to a file descriptor + * that's not the readahead queue. */ + readahead_pipe_fd[0] = readahead_pipe_fd[1] = -1; + + /* If page_size is 0, then the enqueuing functions won't write to the pipe. + * This way, we don't need to introduce new checks there for + * this corner case of not being able to create/set up the pipe. */ + page_size = 0; + + lwan_status_warning("Readahead thread has been disabled"); +} diff --git a/src/lib/lwan-request.c b/src/lib/lwan-request.c new file mode 100644 index 000000000..839a6c1ca --- /dev/null +++ b/src/lib/lwan-request.c @@ -0,0 +1,2065 @@ +/* + * lwan - web server + * Copyright (c) 2012-2014 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "base64.h" +#include "list.h" +#include "lwan-config.h" +#include "lwan-http-authorize.h" +#include "lwan-io-wrappers.h" +#include "sha1.h" + +#define HEADER_VALUE_SEPARATOR_LEN (sizeof(": ") - 1) +#define HEADER_TERMINATOR_LEN (sizeof("\r\n") - 1) +#define MIN_REQUEST_SIZE (sizeof("GET / HTTP/1.1\r\n\r\n") - 1) + +enum lwan_read_finalizer { + FINALIZER_DONE, + FINALIZER_TRY_AGAIN, + FINALIZER_TIMEOUT, +}; + +struct proxy_header_v2 { + uint8_t sig[12]; + uint8_t cmd_ver; + uint8_t fam; + uint16_t len; + union { + struct { + in_addr_t src_addr; + in_addr_t dst_addr; + uint16_t src_port; + uint16_t dst_port; + } ip4; + struct { + struct in6_addr src_addr; + struct in6_addr dst_addr; + uint16_t src_port; + uint16_t dst_port; + } ip6; + } addr; +}; + + +LWAN_ACCESS_PARAM(read_only, 1) +static bool +parse_ascii_port(char *port, unsigned short *out) +{ + unsigned long parsed; + char *end_ptr; + + errno = 0; + parsed = strtoul(port, &end_ptr, 10); + + if (UNLIKELY(errno != 0)) + return false; + + if (UNLIKELY(*end_ptr != '\0')) + return false; + + if (UNLIKELY((unsigned long)(unsigned short)parsed != parsed)) + return false; + + *out = htons((unsigned short)parsed); + return true; +} + +LWAN_ACCESS_PARAM(read_write, 1) +static char * +strsep_char(char *strp, const char *end, char delim) +{ + char *ptr; + + if (UNLIKELY(!strp)) + return NULL; + + if (UNLIKELY(strp > end)) + return NULL; + + ptr = strchr(strp, delim); + if (UNLIKELY(!ptr)) + return NULL; + + *ptr = '\0'; + return ptr + 1; +} + +LWAN_ACCESS_PARAM(read_only, 2) +static char * +parse_proxy_protocol_v1(struct lwan_request *request, char *buffer) +{ + static const size_t line_size = 108; + char *end, *protocol, *src_addr, *dst_addr, *src_port, *dst_port; + unsigned int size; + struct lwan_proxy *const proxy = request->proxy; + + end = memchr(buffer, '\r', line_size); + if (UNLIKELY(!end || end[1] != '\n')) + return NULL; + *end = '\0'; + size = (unsigned int) (end + 2 - buffer); + + protocol = buffer + sizeof("PROXY ") - 1; + src_addr = strsep_char(protocol, end, ' '); + dst_addr = strsep_char(src_addr, end, ' '); + src_port = strsep_char(dst_addr, end, ' '); + dst_port = strsep_char(src_port, end, ' '); + + if (UNLIKELY(!dst_port)) + return NULL; + + STRING_SWITCH(protocol) { + case STR4_INT('T', 'C', 'P', '4'): { + struct sockaddr_in *from = &proxy->from.ipv4; + struct sockaddr_in *to = &proxy->to.ipv4; + + from->sin_family = to->sin_family = AF_INET; + + if (UNLIKELY(inet_pton(AF_INET, src_addr, &from->sin_addr) <= 0)) + return NULL; + if (UNLIKELY(inet_pton(AF_INET, dst_addr, &to->sin_addr) <= 0)) + return NULL; + if (UNLIKELY(!parse_ascii_port(src_port, &from->sin_port))) + return NULL; + if (UNLIKELY(!parse_ascii_port(dst_port, &to->sin_port))) + return NULL; + + break; + } + case STR4_INT('T', 'C', 'P', '6'): { + struct sockaddr_in6 *from = &proxy->from.ipv6; + struct sockaddr_in6 *to = &proxy->to.ipv6; + + from->sin6_family = to->sin6_family = AF_INET6; + + if (UNLIKELY(inet_pton(AF_INET6, src_addr, &from->sin6_addr) <= 0)) + return NULL; + if (UNLIKELY(inet_pton(AF_INET6, dst_addr, &to->sin6_addr) <= 0)) + return NULL; + if (UNLIKELY(!parse_ascii_port(src_port, &from->sin6_port))) + return NULL; + if (UNLIKELY(!parse_ascii_port(dst_port, &to->sin6_port))) + return NULL; + + break; + } + default: + return NULL; + } + + request->flags |= REQUEST_PROXIED; + return buffer + size; +} + +LWAN_ACCESS_PARAM(read_only, 2) +static char *parse_proxy_protocol_v2(struct lwan_request *request, char *buffer) +{ + struct proxy_header_v2 *hdr = (struct proxy_header_v2 *)buffer; + struct lwan_request_parser_helper *helper = request->helper; + const unsigned int proto_signature_length = 16; + unsigned int size; + struct lwan_proxy *const proxy = request->proxy; + + enum { LOCAL = 0x20, PROXY = 0x21, TCP4 = 0x11, TCP6 = 0x21 }; + + size = proto_signature_length + (unsigned int)ntohs(hdr->len); + if (UNLIKELY(size > (unsigned int)sizeof(*hdr))) + return NULL; + if (UNLIKELY(size >= helper->buffer->len)) + return NULL; + + if (LIKELY(hdr->cmd_ver == PROXY)) { + if (hdr->fam == TCP4) { + struct sockaddr_in *from = &proxy->from.ipv4; + struct sockaddr_in *to = &proxy->to.ipv4; + + to->sin_family = from->sin_family = AF_INET; + + from->sin_addr.s_addr = hdr->addr.ip4.src_addr; + from->sin_port = hdr->addr.ip4.src_port; + + to->sin_addr.s_addr = hdr->addr.ip4.dst_addr; + to->sin_port = hdr->addr.ip4.dst_port; + } else if (hdr->fam == TCP6) { + struct sockaddr_in6 *from = &proxy->from.ipv6; + struct sockaddr_in6 *to = &proxy->to.ipv6; + + from->sin6_family = to->sin6_family = AF_INET6; + + from->sin6_addr = hdr->addr.ip6.src_addr; + from->sin6_port = hdr->addr.ip6.src_port; + + to->sin6_addr = hdr->addr.ip6.dst_addr; + to->sin6_port = hdr->addr.ip6.dst_port; + } else { + return NULL; + } + } else if (hdr->cmd_ver == LOCAL) { + struct sockaddr_in *from = &proxy->from.ipv4; + struct sockaddr_in *to = &proxy->to.ipv4; + + from->sin_family = to->sin_family = AF_UNSPEC; + } else { + return NULL; + } + + request->flags |= REQUEST_PROXIED; + return buffer + size; +} + +#if !defined(LWAN_HAVE_BUILTIN_EXPECT_PROBABILITY) +#define __builtin_expect_with_probability(value1, value2, probability) \ + __builtin_expect(value1, value2) +#endif + +LWAN_ACCESS_PARAM(read_only, 2) +static ALWAYS_INLINE char *identify_http_method(struct lwan_request *request, + char *buffer) +{ + const uint32_t first_four = string_as_uint32(buffer); + +#define GENERATE_IF(upper, lower, mask, constant, probability) \ + if (__builtin_expect_with_probability(first_four == (constant), 1, \ + probability)) { \ + request->flags |= (mask); \ + return buffer + sizeof(#upper); \ + } + + FOR_EACH_REQUEST_METHOD(GENERATE_IF) + +#undef GENERATE_IF + + return NULL; +} + +LWAN_ACCESS_PARAM(read_write, 1) +__attribute__((nonnull(1))) static ssize_t url_decode(char *str) +{ + static const unsigned char tbl1[256] = { + [0 ... 255] = 255, ['0'] = 0 << 4, ['1'] = 1 << 4, ['2'] = 2 << 4, + ['3'] = 3 << 4, ['4'] = 4 << 4, ['5'] = 5 << 4, ['6'] = 6 << 4, + ['7'] = 7 << 4, ['8'] = 8 << 4, ['9'] = 9 << 4, ['a'] = 10 << 4, + ['b'] = 11 << 4, ['c'] = 12 << 4, ['d'] = 13 << 4, ['e'] = 14 << 4, + ['f'] = 15 << 4, ['A'] = 10 << 4, ['B'] = 11 << 4, ['C'] = 12 << 4, + ['D'] = 13 << 4, ['E'] = 14 << 4, ['F'] = 15 << 4, + }; + static const char tbl2[256] = { + [0 ... 255] = -1, ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, + ['4'] = 4, ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, + ['9'] = 9, ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, + ['e'] = 14, ['f'] = 15, ['A'] = 10, ['B'] = 11, ['C'] = 12, + ['D'] = 13, ['E'] = 14, ['F'] = 15, + }; + const char *inptr = str; + char *outptr = str; + + for (char *p = strpbrk(inptr, "%+"); p; p = strpbrk(inptr, "%+")) { + const ptrdiff_t diff = p - inptr; + if (diff) + outptr = mempmove(outptr, inptr, (size_t)diff); + + if (*p == '+') { + *outptr++ = ' '; + inptr = p + 1; + continue; + } + + const char first = (char)tbl1[(unsigned char)p[1]]; + const char second = tbl2[(unsigned char)p[2]]; + const char decoded = first | second; + if (UNLIKELY(decoded <= 0)) { + /* This shouldn't happen in normal circumstances, but if %00 is + * found in the encoded string, bail here. */ + if (decoded == '\0') + return -1; + + /* OR-ing both lookups will yield a negative number if either + * encoded character is not a valid hex digit; check it here so + * that other valid-but-negative bytes (e.g. 0xff) are still + * written to outptr. */ + if (first == -1) { + /* tbl1 is shifted so a valid nibble might be negative; + * check for all the bits here instead. */ + return -1; + } + if (second < 0) { + /* tbl2 isn't shifted so we can check for the sign bit only. */ + return -1; + } + } + + *outptr++ = decoded; + inptr = p + 3; + } + + if (inptr > outptr) { + outptr = stpcpy(outptr, inptr); + return (ssize_t)(outptr - str); + } + + return (ssize_t)strlen(str); +} + +LWAN_ACCESS_PARAM(read_only, 1) +LWAN_ACCESS_PARAM(read_only, 2) +static int key_value_compare(const void *a, const void *b) +{ + return strcmp(((const struct lwan_key_value *)a)->key, + ((const struct lwan_key_value *)b)->key); +} + +static void +reset_key_value_array(void *data) +{ + struct lwan_key_value_array *array = data; + + lwan_key_value_array_reset(array); +} + +static void parse_key_values(struct lwan_request *request, + struct lwan_value *helper_value, + struct lwan_key_value_array *array, + ssize_t (*decode_value)(char *value), + const char separator) +{ + struct lwan_key_value *kv; + char *ptr = helper_value->value; + const char *end = helper_value->value + helper_value->len; + coro_deferred reset_defer; + + if (!helper_value->len) + return; + + lwan_key_value_array_init(array); + reset_defer = coro_defer(request->conn->coro, reset_key_value_array, array); + + do { + char *key, *value; + + while (*ptr == ' ' || *ptr == separator) + ptr++; + if (UNLIKELY(*ptr == '\0')) + break; + + key = ptr; + ptr = strsep_char(key, end, separator); + + value = strsep_char(key, end, '='); + if (UNLIKELY(!value)) { + value = ""; + } else if (UNLIKELY(decode_value(value) < 0)) { + /* Disallow values that failed decoding, but allow empty values */ + goto error; + } + + if (UNLIKELY(decode_value(key) <= 0)) { + /* Disallow keys that failed decoding, or empty keys */ + goto error; + } + + kv = lwan_key_value_array_append(array); + if (UNLIKELY(!kv)) + goto error; + + kv->key = key; + kv->value = value; + } while (ptr); + + lwan_key_value_array_sort(array, key_value_compare); + + return; + +error: + coro_defer_fire_and_disarm(request->conn->coro, reset_defer); +} + +static ssize_t +identity_decode(char *input __attribute__((unused))) +{ + return 1; +} + +static void parse_cookies(struct lwan_request *request) +{ + const char *cookies = lwan_request_get_header(request, "Cookie"); + + if (!cookies) + return; + + struct lwan_value header = {.value = (char *)cookies, + .len = strlen(cookies)}; + parse_key_values(request, &header, &request->helper->cookies, + identity_decode, ';'); +} + +static void parse_query_string(struct lwan_request *request) +{ + struct lwan_request_parser_helper *helper = request->helper; + + parse_key_values(request, &helper->query_string, &helper->query_params, + url_decode, '&'); +} + +static void parse_form_data(struct lwan_request *request) +{ + struct lwan_request_parser_helper *helper = request->helper; + static const char content_type[] = "application/x-www-form-urlencoded"; + + if (helper->content_type.len < sizeof(content_type) - 1) + return; + if (UNLIKELY(strncmp(helper->content_type.value, content_type, + sizeof(content_type) - 1))) + return; + + parse_key_values(request, &helper->body_data, &helper->post_params, + url_decode, '&'); +} + +static void find_query_string(struct lwan_request *request, const char *space) +{ + struct lwan_request_parser_helper *helper = request->helper; + + char *query_string = memchr(request->url.value, '?', request->url.len); + if (query_string) { + *query_string = '\0'; + helper->query_string.value = query_string + 1; + helper->query_string.len = (size_t)(space - query_string - 1); + request->url.len -= helper->query_string.len + 1; + request->flags |= REQUEST_HAS_QUERY_STRING; + } +} + +static char * +identify_http_path(struct lwan_request *request, char *buffer) +{ + struct lwan_request_parser_helper *helper = request->helper; + static const size_t minimal_request_line_len = sizeof("/ HTTP/1.0") - 1; + char *space, *end_of_line; + ptrdiff_t end_len; + + if (UNLIKELY(*buffer != '/')) + return NULL; + + end_len = buffer - helper->buffer->value; + if (UNLIKELY((size_t)end_len >= helper->buffer->len)) + return NULL; + + end_of_line = memchr(buffer, '\r', helper->buffer->len - (size_t)end_len); + if (UNLIKELY(!end_of_line)) + return NULL; + if (UNLIKELY((size_t)(end_of_line - buffer) < minimal_request_line_len)) + return NULL; + *end_of_line = '\0'; + + space = end_of_line - sizeof("HTTP/X.X"); + + request->url.value = buffer; + request->url.len = (size_t)(space - buffer); + find_query_string(request, space); + request->original_url = request->url; + + *space++ = '\0'; + + STRING_SWITCH_LARGE(space) { + case STR8_INT('H','T','T','P','/','1','.','0'): + request->flags |= REQUEST_IS_HTTP_1_0; + break; + case STR8_INT('H','T','T','P','/','1','.','1'): + break; + default: + return NULL; + } + + return end_of_line + 1; +} + +__attribute__((noinline)) static void set_header_value( + struct lwan_value *header, char *end, char *p, size_t header_len) +{ + p += header_len; + + if (LIKELY(string_as_uint16(p) == STR2_INT(':', ' '))) { + *end = '\0'; + char *value = p + sizeof(": ") - 1; + + header->value = value; + header->len = (size_t)(end - value); + } +} + +#define HEADER_LENGTH(hdr) \ + ({ \ + if (UNLIKELY(end - sizeof(hdr) + 1 < p)) \ + continue; \ + sizeof(hdr) - 1; \ + }) + +#define SET_HEADER_VALUE(dest, hdr) \ + do { \ + const size_t header_len = HEADER_LENGTH(hdr); \ + set_header_value(&(helper->dest), end, p, header_len); \ + } while (0) + +static ALWAYS_INLINE ssize_t find_headers(char **header_start, + struct lwan_value *request_buffer, + char **next_request) +{ + char *buffer = request_buffer->value; + char *buffer_end = buffer + request_buffer->len; + ssize_t n_headers = 0; + char *next_header; + + for (char *next_chr = buffer + 1;;) { + next_header = memchr(next_chr, '\r', (size_t)(buffer_end - next_chr)); + + if (UNLIKELY(!next_header)) + return -1; + + if (next_chr == next_header) { + ptrdiff_t remaining = buffer_end - next_chr; + + if (UNLIKELY(remaining < (ptrdiff_t)HEADER_TERMINATOR_LEN)) + return -1; + + /* next_chr[0] is guaranteed to be '\r'. See comment below regarding + * request smuggling. */ + if (UNLIKELY(next_chr[1] != '\n')) + return -1; + + if (remaining > (ptrdiff_t)HEADER_TERMINATOR_LEN) + *next_request = next_header + HEADER_TERMINATOR_LEN; + + goto out; + } + + /* Is there at least a space for a minimal (H)eader and a (V)alue? */ + if (LIKELY(next_header - next_chr >= (ptrdiff_t)(sizeof("H: V") - 1))) { + header_start[n_headers++] = next_chr; + + if (UNLIKELY(n_headers >= N_HEADER_START - 1)) + return -1; + } else { + /* Better to abort early if there's no space. */ + return -1; + } + + next_chr = next_header + HEADER_TERMINATOR_LEN; + if (UNLIKELY(next_chr >= buffer_end)) + return -1; + + /* Avoid request smuggling. More info: https://github.com/lpereira/lwan/issues/368 */ + if (UNLIKELY(next_header[1] != '\n')) + return -1; + } + +out: + header_start[n_headers] = next_header; + return n_headers; +} + +static bool parse_headers(struct lwan_request_parser_helper *helper, + char *buffer) +{ + char **header_start = helper->header_start; + ssize_t n_headers = 0; + + /* FIXME: is there a better way to do this? */ + struct lwan_value header_start_buffer = { + .value = buffer, + .len = helper->buffer->len - (size_t)(buffer - helper->buffer->value) + }; + n_headers = find_headers(header_start, &header_start_buffer, + &helper->next_request); + if (UNLIKELY(n_headers < 0)) + return false; + + for (ssize_t i = 0; i < n_headers; i++) { + char *p = header_start[i]; + char *end = header_start[i + 1] - HEADER_TERMINATOR_LEN; + + STRING_SWITCH_L (p) { + case STR4_INT_L('A', 'c', 'c', 'e'): + p += HEADER_LENGTH("Accept"); + + STRING_SWITCH_L (p) { + case STR4_INT_L('-', 'E', 'n', 'c'): + SET_HEADER_VALUE(accept_encoding, "-Encoding"); + break; + } + break; + case STR4_INT_L('C', 'o', 'n', 'n'): + SET_HEADER_VALUE(connection, "Connection"); + break; + case STR4_INT_L('C', 'o', 'n', 't'): + p += HEADER_LENGTH("Content"); + + STRING_SWITCH_L (p) { + case STR4_INT_L('-', 'T', 'y', 'p'): + SET_HEADER_VALUE(content_type, "-Type"); + break; + case STR4_INT_L('-', 'L', 'e', 'n'): + SET_HEADER_VALUE(content_length, "-Length"); + break; + } + break; + case STR4_INT_L('I', 'f', '-', 'M'): + SET_HEADER_VALUE(if_modified_since.raw, "If-Modified-Since"); + break; + case STR4_INT_L('H', 'o', 's', 't'): + SET_HEADER_VALUE(host, "Host"); + break; + case STR4_INT_L('R', 'a', 'n', 'g'): + SET_HEADER_VALUE(range.raw, "Range"); + break; + } + } + + helper->n_header_start = (size_t)n_headers; + return true; +} +#undef HEADER_LENGTH +#undef SET_HEADER_VALUE + +ssize_t lwan_find_headers(char **header_start, struct lwan_value *buffer, + char **next_request) +{ + return find_headers(header_start, buffer, next_request); +} + +static void parse_if_modified_since(struct lwan_request_parser_helper *helper) +{ + static const size_t header_len = + sizeof("Wed, 17 Apr 2019 13:59:27 GMT") - 1; + time_t parsed; + + if (UNLIKELY(helper->if_modified_since.raw.len != header_len)) + return; + + if (UNLIKELY(lwan_parse_rfc_time(helper->if_modified_since.raw.value, + &parsed) < 0)) + return; + + helper->if_modified_since.parsed = parsed; +} + +static bool +parse_off_without_sign(const char *ptr, char **end, off_t *off) +{ + unsigned long long val; + + static_assert(sizeof(val) >= sizeof(off_t), + "off_t fits in a long long"); + + errno = 0; + + val = strtoull(ptr, end, 10); + if (UNLIKELY(val == 0 && *end == ptr)) + return false; + if (UNLIKELY(errno != 0)) + return false; + if (UNLIKELY(val > OFF_MAX)) + return false; + + *off = (off_t)val; + return true; +} + +static void +parse_range(struct lwan_request_parser_helper *helper) +{ + if (UNLIKELY(helper->range.raw.len <= (sizeof("bytes=") - 1))) + return; + + char *range = helper->range.raw.value; + if (UNLIKELY(strncmp(range, "bytes=", sizeof("bytes=") - 1))) + return; + + range += sizeof("bytes=") - 1; + + off_t from, to; + char *end; + + if (*range == '-') { + from = 0; + + if (!parse_off_without_sign(range + 1, &end, &to)) + goto invalid_range; + if (*end != '\0') + goto invalid_range; + } else if (lwan_char_isdigit(*range)) { + if (!parse_off_without_sign(range, &end, &from)) + goto invalid_range; + if (*end != '-') + goto invalid_range; + + range = end + 1; + if (*range == '\0') { + to = -1; + } else { + if (!parse_off_without_sign(range, &end, &to)) + goto invalid_range; + if (*end != '\0') + goto invalid_range; + } + } else { +invalid_range: + to = from = -1; + } + + helper->range.from = from; + helper->range.to = to; +} + +static void +parse_accept_encoding(struct lwan_request *request) +{ + struct lwan_request_parser_helper *helper = request->helper; + + if (!helper->accept_encoding.len) + return; + + for (const char *p = helper->accept_encoding.value; *p; p++) { + STRING_SWITCH(p) { + case STR4_INT('d','e','f','l'): + case STR4_INT(' ','d','e','f'): + request->flags |= REQUEST_ACCEPT_DEFLATE; + break; + case STR4_INT('g','z','i','p'): + case STR4_INT(' ','g','z','i'): + request->flags |= REQUEST_ACCEPT_GZIP; + break; +#if defined(LWAN_HAVE_ZSTD) + case STR4_INT('z','s','t','d'): + case STR4_INT(' ','z','s','t'): + request->flags |= REQUEST_ACCEPT_ZSTD; + break; +#endif +#if defined(LWAN_HAVE_BROTLI) + default: + while (lwan_char_isspace(*p)) + p++; + + STRING_SWITCH_SMALL(p) { + case STR2_INT('b', 'r'): + request->flags |= REQUEST_ACCEPT_BROTLI; + break; + } +#endif + } + + if (!(p = strchr(p, ','))) + break; + } +} + +static ALWAYS_INLINE void parse_connection_header(struct lwan_request *request) +{ + struct lwan_request_parser_helper *helper = request->helper; + struct lwan_connection *conn = request->conn; + bool has_keep_alive = false; + bool has_close = false; + + if (!helper->connection.len) + goto out; + + for (const char *p = helper->connection.value; *p; p++) { + STRING_SWITCH_L(p) { + case STR4_INT_L('k','e','e','p'): + case STR4_INT_L(' ', 'k','e','e'): + has_keep_alive = true; + break; + case STR4_INT_L('c','l','o','s'): + case STR4_INT_L(' ', 'c','l','o'): + has_close = true; + break; + case STR4_INT_L('u','p','g','r'): + case STR4_INT_L(' ', 'u','p','g'): + conn->flags |= CONN_IS_UPGRADE; + break; + } + + if (!(p = strchr(p, ','))) + break; + } + +out: + if (LIKELY(!(request->flags & REQUEST_IS_HTTP_1_0))) { + has_keep_alive = !has_close; + if (has_keep_alive) + conn->flags |= CONN_SENT_CONNECTION_HEADER; + } + + if (has_keep_alive) { + conn->flags |= CONN_IS_KEEP_ALIVE; + } else { + conn->flags &= ~(CONN_IS_KEEP_ALIVE | CONN_SENT_CONNECTION_HEADER); + } +} + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) +static void save_to_corpus_for_fuzzing(struct lwan_value buffer) +{ + struct lwan_value buffer_copy; + char corpus_name[PATH_MAX]; + const char *crlfcrlf; + int fd; + + if (!(crlfcrlf = memmem(buffer.value, buffer.len, "\r\n\r\n", 4))) + return; + buffer.len = (size_t)(crlfcrlf - buffer.value + 4); + +try_another_file_name: + buffer_copy = buffer; + + snprintf(corpus_name, sizeof(corpus_name), "corpus-request-%d", rand()); + + fd = open(corpus_name, O_WRONLY | O_CLOEXEC | O_CREAT | O_EXCL, 0644); + if (fd < 0) + goto try_another_file_name; + + while (buffer_copy.len) { + ssize_t r = write(fd, buffer_copy.value, buffer_copy.len); + + if (r < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + + close(fd); + unlink(corpus_name); + goto try_another_file_name; + } + + buffer_copy.value += r; + buffer_copy.len -= (size_t)r; + } + + close(fd); + lwan_status_debug("Request saved to %s", corpus_name); +} +#endif + +static enum lwan_http_status client_read( + struct lwan_request *request, + struct lwan_value *buffer, + const size_t want_to_read, + enum lwan_read_finalizer (*finalizer)(const struct lwan_value *buffer, + size_t want_to_read, + const struct lwan_request *request, + int n_packets)) +{ + struct lwan_request_parser_helper *helper = request->helper; + int n_packets = 0; + + if (helper->next_request) { + const size_t next_request_len = + (size_t)(helper->next_request - buffer->value); + size_t new_len; + + if (__builtin_sub_overflow(buffer->len, next_request_len, &new_len)) { + helper->next_request = NULL; + } else if (new_len) { + /* FIXME: This memmove() could be eventually removed if a better + * stucture (maybe a ringbuffer, reading with readv(), and each + * pointer is coro_strdup() if they wrap around?) were used for + * the request buffer. */ + buffer->len = new_len; + memmove(buffer->value, helper->next_request, new_len); + goto try_to_finalize; + } + } + + for (buffer->len = 0;; n_packets++) { + size_t to_read = (size_t)(want_to_read - buffer->len); + + if (UNLIKELY(to_read == 0)) + return HTTP_TOO_LARGE; + + ssize_t n = recv(request->fd, buffer->value + buffer->len, to_read, 0); + if (UNLIKELY(n <= 0)) { + if (n < 0) { + switch (errno) { + case EINTR: + case EAGAIN: +yield_and_read_again: + coro_yield(request->conn->coro, CONN_CORO_WANT_READ); + continue; + } + + /* Unexpected error before reading anything */ + if (UNLIKELY(!buffer->len)) + return HTTP_BAD_REQUEST; + } + + /* Client shut down orderly (n = 0), or unrecoverable error (n < 0); + * shut down coro. */ + break; + } + + buffer->len += (size_t)n; + +try_to_finalize: + switch (finalizer(buffer, want_to_read, request, n_packets)) { + case FINALIZER_DONE: + buffer->value[buffer->len] = '\0'; +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + save_to_corpus_for_fuzzing(*buffer); +#endif + return HTTP_OK; + + case FINALIZER_TRY_AGAIN: + goto yield_and_read_again; + + case FINALIZER_TIMEOUT: + return HTTP_TIMEOUT; + } + } + + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + return HTTP_INTERNAL_ERROR; +} + +static enum lwan_read_finalizer +read_request_finalizer_from_helper(const struct lwan_value *buffer, + struct lwan_request_parser_helper *helper, + int n_packets, + bool allow_proxy_reqs) +{ + static const size_t min_proxied_request_size = + MIN_REQUEST_SIZE + sizeof(struct proxy_header_v2); + + if (LIKELY(buffer->len >= MIN_REQUEST_SIZE)) { + STRING_SWITCH (buffer->value + buffer->len - 4) { + case STR4_INT('\r', '\n', '\r', '\n'): + return FINALIZER_DONE; + } + } + + char *crlfcrlf = memmem(buffer->value, buffer->len, "\r\n\r\n", 4); + if (LIKELY(crlfcrlf)) { + if (LIKELY(helper->next_request)) { + helper->next_request = NULL; + return FINALIZER_DONE; + } + + const size_t crlfcrlf_to_base = (size_t)(crlfcrlf - buffer->value); + if (crlfcrlf_to_base >= MIN_REQUEST_SIZE - 4) + return FINALIZER_DONE; + + if (buffer->len > min_proxied_request_size && allow_proxy_reqs) { + /* FIXME: Checking for PROXYv2 protocol header here is a layering + * violation. */ + STRING_SWITCH_LARGE (crlfcrlf + 4) { + case STR8_INT(0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a): + return FINALIZER_DONE; + } + } + } + + /* Yield a timeout error to avoid clients being intentionally slow and + * hogging the server. (Clients can't only connect and do nothing, they + * need to send data, otherwise the timeout queue timer will kick in and + * close the connection. Limit the number of packets to avoid them sending + * just a byte at a time.) See lwan_calculate_n_packets() to see how this is + * calculated. */ + if (UNLIKELY(n_packets > helper->error_when_n_packets)) + return FINALIZER_TIMEOUT; + + return FINALIZER_TRY_AGAIN; +} + +static inline enum lwan_read_finalizer +read_request_finalizer(const struct lwan_value *buffer, + size_t want_to_read __attribute__((unused)), + const struct lwan_request *request, + int n_packets) +{ + return read_request_finalizer_from_helper( + buffer, request->helper, n_packets, + request->flags & REQUEST_ALLOW_PROXY_REQS); +} + +static ALWAYS_INLINE enum lwan_http_status +read_request(struct lwan_request *request) +{ + return client_read(request, request->helper->buffer, + DEFAULT_BUFFER_SIZE - 1 /* -1 for NUL byte */, + read_request_finalizer); +} + +static enum lwan_read_finalizer +body_data_finalizer(const struct lwan_value *buffer, + size_t want_to_read, + const struct lwan_request *request, + int n_packets) +{ + const struct lwan_request_parser_helper *helper = request->helper; + + if (want_to_read == buffer->len) + return FINALIZER_DONE; + + /* For POST requests, the body can be larger, and due to small MTUs on + * most ethernet connections, responding with a timeout solely based on + * number of packets doesn't work. Use keepalive timeout instead. */ + if (UNLIKELY(time(NULL) > helper->error_when_time)) + return FINALIZER_TIMEOUT; + + /* In addition to time, also estimate the number of packets based on an + * usual MTU value and the request body size. */ + if (UNLIKELY(n_packets > helper->error_when_n_packets)) + return FINALIZER_TIMEOUT; + + return FINALIZER_TRY_AGAIN; +} + +__attribute__((cold)) +static const char *is_dir_good_for_tmp(const char *v) +{ + struct stat st; + + if (!v) + return NULL; + + if (*v != '/') + return NULL; + + if (stat(v, &st) < 0) + return NULL; + + if (!S_ISDIR(st.st_mode)) + return NULL; + + if (!(st.st_mode & S_ISVTX)) { + lwan_status_warning( + "Using %s as temporary directory, but it doesn't have " + "the sticky bit set.", + v); + } + +#ifdef LWAN_HAVE_STATFS_F_TYPE + struct statfs sb; + if (!statfs(v, &sb) && sb.f_type == TMPFS_MAGIC) { + lwan_status_warning("%s is a tmpfs filesystem, " + "not considering it", v); + return NULL; + } +#endif + + return v; +} + +static const size_t body_buffer_temp_file_thresh = 1<<20; + +LWAN_LAZY_GLOBAL(const char *, get_temp_dir) +{ + const char *tmpdir; + + tmpdir = is_dir_good_for_tmp(secure_getenv("TMPDIR")); + if (tmpdir) + return tmpdir; + + tmpdir = is_dir_good_for_tmp(secure_getenv("TMP")); + if (tmpdir) + return tmpdir; + + tmpdir = is_dir_good_for_tmp(secure_getenv("TEMP")); + if (tmpdir) + return tmpdir; + + tmpdir = is_dir_good_for_tmp("/var/tmp"); + if (tmpdir) + return tmpdir; + + tmpdir = is_dir_good_for_tmp(P_tmpdir); + if (tmpdir) + return tmpdir; + + lwan_status_warning("Temporary directory could not be determined. POST " + "or PUT requests over %zu bytes bytes will fail.", + body_buffer_temp_file_thresh); + return NULL; +} + +static int create_temp_file(void) +{ + char template[PATH_MAX]; + mode_t prev_mask; + int ret; + + if (UNLIKELY(!get_temp_dir())) + return -ENOENT; + +#if defined(O_TMPFILE) + int fd = open(get_temp_dir(), + O_TMPFILE | O_CREAT | O_RDWR | O_EXCL | O_CLOEXEC | + O_NOFOLLOW | O_NOATIME, + S_IRUSR | S_IWUSR); + if (LIKELY(fd >= 0)) + return fd; +#endif + + ret = snprintf(template, sizeof(template), "%s/lwanXXXXXX", get_temp_dir()); + if (UNLIKELY(ret < 0 || ret >= (int)sizeof(template))) + return -EOVERFLOW; + + prev_mask = umask_for_tmpfile(S_IRUSR | S_IWUSR); + ret = mkostemp(template, O_CLOEXEC); + umask_for_tmpfile(prev_mask); + + if (LIKELY(ret >= 0)) + unlink(template); + + return ret; +} + +struct file_backed_buffer { + void *ptr; + size_t size; +}; + +static void +free_body_buffer(void *data) +{ + struct file_backed_buffer *buf = data; + + munmap(buf->ptr, buf->size); + free(buf); +} + +static void* +alloc_body_buffer(struct coro *coro, size_t size, bool allow_file) +{ + struct file_backed_buffer *buf; + void *ptr = (void *)MAP_FAILED; + int fd; + + if (LIKELY(size < body_buffer_temp_file_thresh)) { + ptr = coro_malloc(coro, size); + + if (LIKELY(ptr)) + return ptr; + } + + if (UNLIKELY(!allow_file)) + return NULL; + + fd = create_temp_file(); + if (UNLIKELY(fd < 0)) + return NULL; + + if (UNLIKELY(ftruncate(fd, (off_t)size) < 0)) { + close(fd); + return NULL; + } + + if (MAP_HUGETLB) { + ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_HUGETLB, fd, 0); + } + if (UNLIKELY(ptr == MAP_FAILED)) + ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + if (UNLIKELY(ptr == MAP_FAILED)) + return NULL; + + buf = coro_malloc_full(coro, sizeof(*buf), free_body_buffer); + if (UNLIKELY(!buf)) { + munmap(ptr, size); + return NULL; + } + + buf->ptr = ptr; + buf->size = size; + return ptr; +} + +static enum lwan_http_status +get_remaining_body_data_length(struct lwan_request *request, + const size_t max_size, + size_t *total, + size_t *have) +{ + struct lwan_request_parser_helper *helper = request->helper; + long long parsed_size; + + if (UNLIKELY(!helper->content_length.value)) + return HTTP_BAD_REQUEST; + + parsed_size = parse_long_long(helper->content_length.value, -1); + if (UNLIKELY(parsed_size < 0)) + return HTTP_BAD_REQUEST; + if (UNLIKELY((size_t)parsed_size >= max_size)) + return HTTP_TOO_LARGE; + if (UNLIKELY(!parsed_size)) { + helper->body_data.value = helper->next_request = NULL; + *total = *have = 0; + } else { + *total = (size_t)parsed_size; + + if (!helper->next_request) { + *have = 0; + return HTTP_PARTIAL_CONTENT; + } + + char *buffer_end = helper->buffer->value + helper->buffer->len; + + *have = (size_t)(buffer_end - helper->next_request); + + if (*have < *total) + return HTTP_PARTIAL_CONTENT; + + helper->body_data.value = helper->next_request; + helper->next_request += *total; + } + + helper->body_data.len = *total; + return HTTP_OK; +} + +static int read_body_data(struct lwan_request *request) +{ + /* Holy indirection, Batman! */ + const struct lwan_config *config = &request->conn->thread->lwan->config; + struct lwan_request_parser_helper *helper = request->helper; + enum lwan_http_status status; + size_t total, have, max_data_size; + bool allow_temp_file; + char *new_buffer; + + switch (lwan_request_get_method(request)) { + case REQUEST_METHOD_POST: + allow_temp_file = config->allow_post_temp_file; + max_data_size = config->max_post_data_size; + break; + case REQUEST_METHOD_PUT: + allow_temp_file = config->allow_put_temp_file; + max_data_size = config->max_put_data_size; + break; + default: + return -HTTP_NOT_ALLOWED; + } + + status = + get_remaining_body_data_length(request, max_data_size, &total, &have); + if (status == HTTP_OK) + return HTTP_OK; + if (status != HTTP_PARTIAL_CONTENT) + return -(int)status; + + new_buffer = + alloc_body_buffer(request->conn->coro, total + 1, allow_temp_file); + if (UNLIKELY(!new_buffer)) + return -HTTP_INTERNAL_ERROR; + + if (!(request->flags & REQUEST_IS_HTTP_1_0)) { + /* §8.2.3 https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html */ + const char *expect = lwan_request_get_header(request, "Expect"); + + if (expect && strncmp(expect, "100-", 4) == 0) { + static const char continue_header[] = "HTTP/1.1 100 Continue\r\n\r\n"; + + lwan_send(request, continue_header, sizeof(continue_header) - 1, 0); + } + } + + helper->body_data.value = new_buffer; + helper->body_data.len = total; + if (have) { + new_buffer = mempcpy(new_buffer, helper->next_request, have); + total -= have; + } + helper->next_request = NULL; + + helper->error_when_time = time(NULL) + config->keep_alive_timeout; + helper->error_when_n_packets = lwan_calculate_n_packets(total); + + struct lwan_value buffer = {.value = new_buffer, .len = total}; + return (int)client_read(request, &buffer, total, body_data_finalizer); +} + +static char * +parse_proxy_protocol(struct lwan_request *request, char *buffer) +{ + STRING_SWITCH(buffer) { + case STR4_INT('P','R','O','X'): + return parse_proxy_protocol_v1(request, buffer); + case STR4_INT('\x0D','\x0A','\x0D','\x0A'): + return parse_proxy_protocol_v2(request, buffer); + } + + return buffer; +} + +static enum lwan_http_status parse_http_request(struct lwan_request *request) +{ + struct lwan_request_parser_helper *helper = request->helper; + char *buffer = helper->buffer->value; + + if (request->flags & REQUEST_ALLOW_PROXY_REQS) { + /* REQUEST_ALLOW_PROXY_REQS will be cleared in lwan_process_request() */ + + buffer = parse_proxy_protocol(request, buffer); + if (UNLIKELY(!buffer)) + return HTTP_BAD_REQUEST; + } + + if (UNLIKELY(buffer > helper->buffer->value + helper->buffer->len - + MIN_REQUEST_SIZE)) + return HTTP_BAD_REQUEST; + + char *path = identify_http_method(request, buffer); + if (UNLIKELY(!path)) + return HTTP_NOT_ALLOWED; + + buffer = identify_http_path(request, path); + if (UNLIKELY(!buffer)) + return HTTP_BAD_REQUEST; + + if (UNLIKELY(!parse_headers(helper, buffer))) + return HTTP_BAD_REQUEST; + + ssize_t decoded_len = url_decode(request->url.value); + if (UNLIKELY(decoded_len < 0)) + return HTTP_BAD_REQUEST; + request->original_url.len = request->url.len = (size_t)decoded_len; + + parse_connection_header(request); + + return HTTP_OK; +} + +static enum lwan_http_status +prepare_websocket_handshake(struct lwan_request *request, char **encoded) +{ + static const unsigned char websocket_uuid[] = + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + unsigned char digest[20]; + sha1_context ctx; + + if (UNLIKELY(request->flags & RESPONSE_SENT_HEADERS)) + return HTTP_INTERNAL_ERROR; + + if (UNLIKELY(!(request->conn->flags & CONN_IS_UPGRADE))) + return HTTP_BAD_REQUEST; + + const char *upgrade = lwan_request_get_header(request, "Upgrade"); + if (UNLIKELY(!upgrade)) { + /* Ugh. Some older versions of Safari do not send an Upgrade header + * when establishing websocket connections. I don't want to ignore + * this header in the name of being as strict as possible, but if + * the handshake doesn't work in those browsers, this whole thing is + * useless. Sniffing the user agent here for compatibility reasons + * makes me want to puke in my mouth a little bit, but seems to be + * the best way to workaround this, while being more strict for those + * user agents that are actually compliant with the RFC. */ + const char *user_agent = lwan_request_get_header(request, "User-Agent"); + if (!user_agent) + return HTTP_BAD_REQUEST; + + /* A bunch of browsers include the substring "Safari" in their user + * agent, so look for them first. If we find "Chrome" or "Android" + * then it's not Safari. Hopefully this catches other Chrome-based + * browsers too, where the handshake is correctly performed with the + * Upgrade header. */ + if (strstr(user_agent, "Chrome") || strstr(user_agent, "Android")) + return HTTP_BAD_REQUEST; + + /* If we find a "Safari" string in the user agent at this point, + * then we're almost certain it's a Safari browser, and we can + * ignore the absence of an Upgrade header. If it's not there, then + * they should've sent the Upgrade header. Blergh. */ + if (!strstr(user_agent, "Safari")) + return HTTP_BAD_REQUEST; + } else if (UNLIKELY(!streq(upgrade, "websocket"))) { + return HTTP_BAD_REQUEST; + } + + const char *sec_websocket_key = + lwan_request_get_header(request, "Sec-WebSocket-Key"); + if (UNLIKELY(!sec_websocket_key)) + return HTTP_BAD_REQUEST; + + const size_t sec_websocket_key_len = strlen(sec_websocket_key); + if (base64_encoded_len(16) != sec_websocket_key_len) + return HTTP_BAD_REQUEST; + if (UNLIKELY(!base64_validate((void *)sec_websocket_key, sec_websocket_key_len))) + return HTTP_BAD_REQUEST; + + sha1_init(&ctx); + sha1_update(&ctx, (void *)sec_websocket_key, sec_websocket_key_len); + sha1_update(&ctx, websocket_uuid, sizeof(websocket_uuid) - 1); + sha1_finalize(&ctx, digest); + + *encoded = (char *)base64_encode(digest, sizeof(digest), NULL); + return LIKELY(*encoded) ? HTTP_SWITCHING_PROTOCOLS : HTTP_INTERNAL_ERROR; +} + +enum lwan_http_status +lwan_request_websocket_upgrade(struct lwan_request *request) +{ + char header_buf[DEFAULT_HEADERS_SIZE]; + size_t header_buf_len; + char *encoded; + + enum lwan_http_status r = prepare_websocket_handshake(request, &encoded); + if (r != HTTP_SWITCHING_PROTOCOLS) + return r; + + request->flags |= RESPONSE_NO_CONTENT_LENGTH; + header_buf_len = lwan_prepare_response_header_full( + request, HTTP_SWITCHING_PROTOCOLS, header_buf, sizeof(header_buf), + (struct lwan_key_value[]){ + /* Connection: Upgrade is implicit if conn->flags & CONN_IS_UPGRADE */ + {.key = "Sec-WebSocket-Accept", .value = encoded}, + {.key = "Upgrade", .value = "websocket"}, + {}, + }); + free(encoded); + if (UNLIKELY(!header_buf_len)) + return HTTP_INTERNAL_ERROR; + + request->conn->flags |= CONN_IS_WEBSOCKET; + lwan_send(request, header_buf, header_buf_len, 0); + + return HTTP_SWITCHING_PROTOCOLS; +} + +static inline bool request_has_body(const struct lwan_request *request) +{ + /* 3rd bit set in method: request method has body. See lwan.h, + * definition of FOR_EACH_REQUEST_METHOD() for more info. */ + return lwan_request_get_method(request) & (1 << 3); +} + +static enum lwan_http_status +maybe_read_body_data(const struct lwan_url_map *url_map, + struct lwan_request *request) +{ + int status = 0; + + if (url_map->flags & HANDLER_EXPECTS_BODY_DATA) { + status = read_body_data(request); + if (status > 0) + return (enum lwan_http_status)status; + } + + /* Instead of trying to read the body here, which will require + * us to allocate and read potentially a lot of bytes, force + * this connection to be closed as soon as we send a "not allowed" + * response. */ + request->conn->flags &= ~CONN_IS_KEEP_ALIVE; + + if (status < 0) { + status = -status; + return (enum lwan_http_status)status; + } + + return HTTP_NOT_ALLOWED; +} + +static enum lwan_http_status prepare_for_response(const struct lwan_url_map *url_map, + struct lwan_request *request) +{ + request->url.value += url_map->prefix_len; + request->url.len -= url_map->prefix_len; + while (*request->url.value == '/' && request->url.len > 0) { + request->url.value++; + request->url.len--; + } + + if (UNLIKELY(url_map->flags & HANDLER_MUST_AUTHORIZE)) { + if (!lwan_http_authorize_urlmap(request, url_map)) + return HTTP_NOT_AUTHORIZED; + } + + if (UNLIKELY(request_has_body(request))) + return maybe_read_body_data(url_map, request); + + return HTTP_OK; +} + +static bool handle_rewrite(struct lwan_request *request) +{ + struct lwan_request_parser_helper *helper = request->helper; + + request->flags &= ~RESPONSE_URL_REWRITTEN; + + find_query_string(request, request->url.value + request->url.len); + + helper->urls_rewritten++; + if (UNLIKELY(helper->urls_rewritten > 4)) { + lwan_default_response(request, HTTP_INTERNAL_ERROR); + return false; + } + + return true; +} + +const char *lwan_request_get_method_str(const struct lwan_request *request) +{ +#define GENERATE_CASE_STMT(upper, lower, mask, constant, probability) \ + case REQUEST_METHOD_##upper: \ + return #upper; + + switch (lwan_request_get_method(request)) { + FOR_EACH_REQUEST_METHOD(GENERATE_CASE_STMT) + default: + return "UNKNOWN"; + } +#undef GENERATE_CASE_STMT +} + +#ifndef NDEBUG +static void log_request(struct lwan_request *request, + enum lwan_http_status status, + double time_to_read_request, + double time_to_process_request) +{ + char ip_buffer[INET6_ADDRSTRLEN]; + + lwan_status_debug( + "%s [%s] %016lx \"%s %s HTTP/%s\" %d %s (r:%.3fms p:%.3fms)", + lwan_request_get_remote_address(request, ip_buffer), + request->conn->thread->date.date, lwan_request_get_id(request), + lwan_request_get_method_str(request), request->original_url.value, + request->flags & REQUEST_IS_HTTP_1_0 ? "1.0" : "1.1", status, + request->response.mime_type, time_to_read_request, + time_to_process_request); +} +#else +#define log_request(...) +#endif + +#ifndef NDEBUG +static struct timespec current_precise_monotonic_timespec(void) +{ + struct timespec now; + + if (UNLIKELY(clock_gettime(CLOCK_MONOTONIC, &now) < 0)) { + lwan_status_perror("clock_gettime"); + return (struct timespec){}; + } + + return now; +} + +static double elapsed_time_ms(const struct timespec then) +{ + const struct timespec now = current_precise_monotonic_timespec(); + struct timespec diff = { + .tv_sec = now.tv_sec - then.tv_sec, + .tv_nsec = now.tv_nsec - then.tv_nsec, + }; + + if (diff.tv_nsec < 0) { + diff.tv_sec--; + diff.tv_nsec += 1000000000l; + } + + return (double)diff.tv_sec / 1000.0 + (double)diff.tv_nsec / 1000000.0; +} +#endif + +void lwan_process_request(struct lwan *l, struct lwan_request *request) +{ + enum lwan_http_status status; + struct lwan_url_map *url_map; + +#ifndef NDEBUG + struct timespec request_read_begin_time = current_precise_monotonic_timespec(); +#endif + status = read_request(request); + +#ifndef NDEBUG + double time_to_read_request = elapsed_time_ms(request_read_begin_time); + + struct timespec request_begin_time = current_precise_monotonic_timespec(); +#endif + if (UNLIKELY(status != HTTP_OK)) { + /* If read_request() returns any error at this point, it's probably + * better to just send an error response and abort the coroutine and + * let the client handle the error instead: we don't have + * information to even log the request because it has not been + * parsed yet at this stage. Even if there are other requests waiting + * in the pipeline, this seems like the safer thing to do. */ + request->conn->flags &= ~CONN_IS_KEEP_ALIVE; + lwan_default_response(request, status); + /* Let process_request_coro() gracefully close the connection. */ + return; + } + + status = parse_http_request(request); + if (UNLIKELY(status != HTTP_OK)) + goto log_and_return; + +lookup_again: + url_map = lwan_trie_lookup_prefix(&l->url_map_trie, request->url.value); + if (UNLIKELY(!url_map)) { + status = HTTP_NOT_FOUND; + goto log_and_return; + } + + status = prepare_for_response(url_map, request); + if (UNLIKELY(status != HTTP_OK)) + goto log_and_return; + + status = url_map->handler(request, &request->response, url_map->data); + if (UNLIKELY(url_map->flags & HANDLER_CAN_REWRITE_URL)) { + if (request->flags & RESPONSE_URL_REWRITTEN) { + if (LIKELY(handle_rewrite(request))) + goto lookup_again; + return; + } + } + +log_and_return: + lwan_response(request, status); + + log_request(request, status, time_to_read_request, elapsed_time_ms(request_begin_time)); +} + +static inline const char *value_lookup(const struct lwan_key_value_array *array, + const char *key) +{ + const struct lwan_array *la = (const struct lwan_array *)array; + + if (LIKELY(la->elements)) { + struct lwan_key_value k = { .key = (char *)key }; + struct lwan_key_value *entry; + + entry = bsearch(&k, la->base, la->elements, sizeof(k), key_value_compare); + if (LIKELY(entry)) + return entry->value; + } + + return NULL; +} + +const char *lwan_request_get_query_param(struct lwan_request *request, + const char *key) +{ + return value_lookup(lwan_request_get_query_params(request), key); +} + +const char *lwan_request_get_post_param(struct lwan_request *request, + const char *key) +{ + return value_lookup(lwan_request_get_post_params(request), key); +} + +const char *lwan_request_get_cookie(struct lwan_request *request, + const char *key) +{ + return value_lookup(lwan_request_get_cookies(request), key); +} + +const char *lwan_request_get_header(struct lwan_request *request, + const char *header) +{ + const struct lwan_request_parser_helper *helper = request->helper; + const size_t header_len = strlen(header); + const size_t header_len_with_separator = + header_len + HEADER_VALUE_SEPARATOR_LEN; + + assert(strchr(header, ':') == NULL); + + for (size_t i = 0; i < helper->n_header_start; i++) { + const char *start = helper->header_start[i]; + char *end = helper->header_start[i + 1] - HEADER_TERMINATOR_LEN; + + if (UNLIKELY((size_t)(end - start) < header_len_with_separator)) + continue; + STRING_SWITCH_SMALL (start + header_len) { + case STR2_INT(':', ' '): + if (strcaseequal_neutral_len(start, header, header_len)) { + *end = '\0'; + return start + header_len_with_separator; + } + } + } + + return NULL; +} + +const char *lwan_request_get_host(struct lwan_request *request) +{ + const struct lwan_request_parser_helper *helper = request->helper; + + return helper->host.len ? helper->host.value : NULL; +} + +const char * +lwan_request_get_remote_address_and_port(const struct lwan_request *request, + char buffer[static INET6_ADDRSTRLEN], + uint16_t *port) +{ + struct sockaddr_storage non_proxied_addr = {.ss_family = AF_UNSPEC}; + struct sockaddr_storage *sock_addr; + + *port = 0; + + if (request->flags & REQUEST_PROXIED) { + sock_addr = (struct sockaddr_storage *)&request->proxy->from; + + if (UNLIKELY(sock_addr->ss_family == AF_UNSPEC)) { + static const char unspecified[] = "*unspecified*"; + + static_assert(sizeof(unspecified) <= INET6_ADDRSTRLEN, + "Enough space for unspecified address family"); + return memcpy(buffer, unspecified, sizeof(unspecified)); + } + } else { + socklen_t sock_len = sizeof(non_proxied_addr); + + sock_addr = &non_proxied_addr; + + if (UNLIKELY(getpeername(request->fd, (struct sockaddr *)sock_addr, + &sock_len) < 0)) { + return NULL; + } + } + + if (sock_addr->ss_family == AF_INET) { + struct sockaddr_in *sin = (struct sockaddr_in *)sock_addr; + *port = ntohs(sin->sin_port); + return inet_ntop(AF_INET, &sin->sin_addr, buffer, INET6_ADDRSTRLEN); + } + + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sock_addr; + *port = ntohs(sin6->sin6_port); + return inet_ntop(AF_INET6, &sin6->sin6_addr, buffer, INET6_ADDRSTRLEN); +} + +const char * +lwan_request_get_remote_address(const struct lwan_request *request, + char buffer[static INET6_ADDRSTRLEN]) +{ + uint16_t port; + return lwan_request_get_remote_address_and_port(request, buffer, &port); +} + +static void remove_sleep(void *data1, void *data2) +{ + static const enum lwan_connection_flags suspended_sleep = + CONN_SUSPENDED_MASK | CONN_HAS_REMOVE_SLEEP_DEFER; + struct timeouts *wheel = data1; + struct timeout *timeout = data2; + struct lwan_request *request = + container_of(timeout, struct lwan_request, timeout); + + if ((request->conn->flags & suspended_sleep) == suspended_sleep) + timeouts_del(wheel, timeout); + + request->conn->flags &= ~CONN_HAS_REMOVE_SLEEP_DEFER; +} + +void lwan_request_sleep(struct lwan_request *request, uint64_t ms) +{ + struct lwan_connection *conn = request->conn; + struct timeouts *wheel = conn->thread->wheel; + struct timespec now; + coro_deferred defer = -1; + + /* We need to update the timer wheel right now because + * a request might have requested to sleep a long time + * before it was being serviced -- causing the timeout + * to essentially be a no-op. */ + if (UNLIKELY(clock_gettime(monotonic_clock_id, &now) < 0)) + lwan_status_critical("Could not get monotonic time"); + timeouts_update(wheel, (timeout_t)(now.tv_sec * 1000 + now.tv_nsec / 1000000)); + + request->timeout = (struct timeout) {}; + timeouts_add(wheel, &request->timeout, ms); + + if (!(conn->flags & CONN_HAS_REMOVE_SLEEP_DEFER)) { + defer = coro_defer2(conn->coro, remove_sleep, wheel, &request->timeout); + conn->flags |= CONN_HAS_REMOVE_SLEEP_DEFER; + } + + coro_yield(conn->coro, CONN_CORO_SUSPEND); + + if (defer > 0) + coro_defer_fire_and_disarm(conn->coro, defer); +} + +ALWAYS_INLINE int +lwan_request_get_range(struct lwan_request *request, off_t *from, off_t *to) +{ + struct lwan_request_parser_helper *helper = request->helper; + + if (!(request->flags & REQUEST_PARSED_RANGE)) { + parse_range(helper); + request->flags |= REQUEST_PARSED_RANGE; + } + + if (LIKELY(helper->range.raw.len)) { + *from = helper->range.from; + *to = helper->range.to; + return 0; + } + + return -ENOENT; +} + +ALWAYS_INLINE int +lwan_request_get_if_modified_since(struct lwan_request *request, time_t *value) +{ + struct lwan_request_parser_helper *helper = request->helper; + + if (!(request->flags & REQUEST_PARSED_IF_MODIFIED_SINCE)) { + parse_if_modified_since(helper); + request->flags |= REQUEST_PARSED_IF_MODIFIED_SINCE; + } + + if (LIKELY(helper->if_modified_since.raw.len)) { + *value = helper->if_modified_since.parsed; + return 0; + } + + return -ENOENT; +} + +ALWAYS_INLINE const struct lwan_value * +lwan_request_get_request_body(struct lwan_request *request) +{ + return &request->helper->body_data; +} + +ALWAYS_INLINE const struct lwan_value * +lwan_request_get_content_type(struct lwan_request *request) +{ + return &request->helper->content_type; +} + +ALWAYS_INLINE const struct lwan_key_value_array * +lwan_request_get_cookies(struct lwan_request *request) +{ + if (!(request->flags & REQUEST_PARSED_COOKIES)) { + parse_cookies(request); + request->flags |= REQUEST_PARSED_COOKIES; + } + + return &request->helper->cookies; +} + +ALWAYS_INLINE const struct lwan_key_value_array * +lwan_request_get_query_params(struct lwan_request *request) +{ + if (!(request->flags & REQUEST_PARSED_QUERY_STRING)) { + parse_query_string(request); + request->flags |= REQUEST_PARSED_QUERY_STRING; + } + + return &request->helper->query_params; +} + +ALWAYS_INLINE const struct lwan_key_value_array * +lwan_request_get_post_params(struct lwan_request *request) +{ + if (!(request->flags & REQUEST_PARSED_FORM_DATA)) { + parse_form_data(request); + request->flags |= REQUEST_PARSED_FORM_DATA; + } + + return &request->helper->post_params; +} + +ALWAYS_INLINE enum lwan_request_flags +lwan_request_get_accept_encoding(struct lwan_request *request) +{ + if (!(request->flags & REQUEST_PARSED_ACCEPT_ENCODING)) { + parse_accept_encoding(request); + request->flags |= REQUEST_PARSED_ACCEPT_ENCODING; + } + + return request->flags & REQUEST_ACCEPT_MASK; +} + +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +static int useless_coro_for_fuzzing(struct coro *c __attribute__((unused)), + void *data __attribute__((unused))) +{ + return 0; +} + +static bool request_seems_complete(struct lwan_request_parser_helper *helper) +{ + return read_request_finalizer_from_helper(helper->buffer, helper, 1, + false) == FINALIZER_DONE; +} + +__attribute__((used)) int fuzz_parse_http_request(const uint8_t *data, + size_t length) +{ + static struct coro_switcher switcher; + static struct coro *coro; + static char *header_start[N_HEADER_START]; + static char data_copy[32767] = {0}; + + if (length > sizeof(data_copy)) + length = sizeof(data_copy); + memcpy(data_copy, data, length); + + if (!coro) { + coro = coro_new(&switcher, useless_coro_for_fuzzing, NULL); + + lwan_job_thread_init(); + lwan_http_authorize_init(); + } + + struct lwan_request_parser_helper helper = { + .buffer = &(struct lwan_value){.value = data_copy, .len = length}, + .header_start = header_start, + .error_when_n_packets = 2, + }; + struct lwan_connection conn = {.coro = coro}; + struct lwan_proxy proxy = {}; + struct lwan_request request = { + .helper = &helper, + .conn = &conn, + .flags = REQUEST_ALLOW_PROXY_REQS, + .proxy = &proxy, + }; + + /* If the finalizer isn't happy with a request, there's no point in + * going any further with parsing it. */ + if (!request_seems_complete(&helper)) + return 0; + + /* client_read() NUL-terminates the string */ + data_copy[length - 1] = '\0'; + + if (parse_http_request(&request) != HTTP_OK) + return 0; + + off_t trash1; + time_t trash2; + char *trash3; + size_t gen = coro_deferred_get_generation(coro); + + /* Only pointers were set in helper struct; actually parse them here. */ + parse_accept_encoding(&request); + + /* Requesting these items will force them to be parsed, and also + * exercise the lookup function. */ + LWAN_NO_DISCARD(lwan_request_get_header(&request, "Non-Existing-Header")); + + /* Usually existing short header */ + LWAN_NO_DISCARD(lwan_request_get_header(&request, "Host")); + + LWAN_NO_DISCARD(lwan_request_get_cookie(&request, "Non-Existing-Cookie")); + /* Set by some tests */ + LWAN_NO_DISCARD(lwan_request_get_cookie(&request, "FOO")); + + LWAN_NO_DISCARD( + lwan_request_get_query_param(&request, "Non-Existing-Query-Param")); + + LWAN_NO_DISCARD( + lwan_request_get_post_param(&request, "Non-Existing-Post-Param")); + + lwan_request_get_range(&request, &trash1, &trash1); + LWAN_NO_DISCARD(trash1); + + lwan_request_get_if_modified_since(&request, &trash2); + LWAN_NO_DISCARD(trash2); + + enum lwan_http_status handshake = + prepare_websocket_handshake(&request, &trash3); + LWAN_NO_DISCARD(trash3); + if (handshake == HTTP_SWITCHING_PROTOCOLS) + free(trash3); + + LWAN_NO_DISCARD(lwan_http_authorize(&request, "Fuzzy Realm", "/dev/null")); + + coro_deferred_run(coro, gen); + + return 0; +} +#endif + +void lwan_request_foreach_header_for_cgi(struct lwan_request *request, + void (*cb)(const char *header_name, + size_t header_len, + const char *value, + size_t value_len, + void *user_data), + void *user_data) +{ + struct lwan_request_parser_helper *helper = request->helper; + char **header_start = helper->header_start; + size_t n_header_start = helper->n_header_start; + + for (size_t i = 0; i < n_header_start; i++) { + const char *header = header_start[i]; + const char *next_header = header_start[i + 1]; + const char *colon = memchr(header, ':', 127 - sizeof("HTTP_: ") - 1); + char header_name[128]; + int r; + + if (!colon) + continue; + + const ptrdiff_t header_len = colon - header; + const ptrdiff_t value_len = next_header - colon - 4; + + if (header_len < 0 || value_len < 0) + continue; + + r = snprintf(header_name, sizeof(header_name), "HTTP_%.*s", + (int)header_len, header); + if (r < 0 || r >= (int)sizeof(header_name)) + continue; + + for (char *p = header_name; *p; p++) { + if (lwan_char_isalpha(*p)) + *p &= ~0x20; + else if (!lwan_char_iscgiheader(*p)) + *p = '_'; + } + + if (streq(header_name, "HTTP_PROXY")) { + /* Mitigation for https://httpoxy.org */ + continue; + } + + cb(header_name, (size_t)header_len + sizeof("HTTP_") - 1, colon + 2, + (size_t)value_len, user_data); + } +} diff --git a/src/lib/lwan-response.c b/src/lib/lwan-response.c new file mode 100644 index 000000000..5d72ea282 --- /dev/null +++ b/src/lib/lwan-response.c @@ -0,0 +1,507 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "int-to-str.h" +#include "lwan-io-wrappers.h" +#include "lwan-template.h" + +#include "response-data.h" + +static struct lwan_tpl *error_template = NULL; + +struct error_template { + const char *short_message; + const char *long_message; +}; + +void lwan_response_init(struct lwan *l) +{ +#undef TPL_STRUCT +#define TPL_STRUCT struct error_template + static const struct lwan_var_descriptor error_descriptor[] = { + TPL_VAR_STR(short_message), TPL_VAR_STR(long_message), + TPL_VAR_SENTINEL}; + + assert(!error_template); + + lwan_status_debug("Initializing default response"); + + if (l->config.error_template) { + error_template = + lwan_tpl_compile_file(l->config.error_template, error_descriptor); + } else { + error_template = lwan_tpl_compile_value_full( + response_template_value, error_descriptor, LWAN_TPL_FLAG_CONST_TEMPLATE); + } + + if (UNLIKELY(!error_template)) + lwan_status_critical_perror("lwan_tpl_compile_string"); +} + +void lwan_response_shutdown(struct lwan *l __attribute__((unused))) +{ + lwan_status_debug("Shutting down response"); + assert(error_template); + lwan_tpl_free(error_template); +} + +static inline bool has_response_body(enum lwan_request_flags method, + enum lwan_http_status status) +{ + /* See FOR_EACH_REQUEST_METHOD() in lwan.h */ + return (method & 1 << 0) || status != HTTP_NOT_MODIFIED; +} + +void lwan_response(struct lwan_request *request, enum lwan_http_status status) +{ + const struct lwan_response *response = &request->response; + char headers[DEFAULT_HEADERS_SIZE]; + + if (UNLIKELY(request->flags & RESPONSE_CHUNKED_ENCODING)) { + /* Send last, 0-sized chunk */ + lwan_strbuf_reset(response->buffer); + lwan_response_send_chunk(request); + return; + } + + if (UNLIKELY(request->flags & RESPONSE_SENT_HEADERS)) { + lwan_status_debug("Headers already sent, ignoring call"); + return; + } + + if (UNLIKELY(!response->mime_type)) { + /* Requests without a MIME Type are errors from handlers that should + just be handled by lwan_default_response(). */ + return lwan_default_response(request, status); + } + + if (request->flags & RESPONSE_STREAM) { + if (LIKELY(response->stream.callback)) { + status = response->stream.callback(request, response->stream.data); + } else { + status = HTTP_INTERNAL_ERROR; + } + + if (UNLIKELY(status >= HTTP_CLASS__CLIENT_ERROR)) { + request->flags &= ~RESPONSE_STREAM; + lwan_default_response(request, status); + } + + return; + } + + size_t header_len = + lwan_prepare_response_header(request, status, headers, sizeof(headers)); + if (UNLIKELY(!header_len)) + return lwan_default_response(request, HTTP_INTERNAL_ERROR); + + if (!has_response_body(lwan_request_get_method(request), status)) + return (void)lwan_send(request, headers, header_len, 0); + + char *resp_buf = lwan_strbuf_get_buffer(response->buffer); + const size_t resp_len = lwan_strbuf_get_length(response->buffer); + if (sizeof(headers) - header_len > resp_len) { + /* writev() has to allocate, copy, and validate the response vector, + * so use send() for responses small enough to fit the headers + * buffer. On Linux, this is ~10% faster. */ + memcpy(headers + header_len, resp_buf, resp_len); + return (void)lwan_send(request, headers, header_len + resp_len, 0); + } + + struct iovec response_vec[] = { + {.iov_base = headers, .iov_len = header_len}, + {.iov_base = resp_buf, .iov_len = resp_len}, + }; + + return (void)lwan_writev(request, response_vec, N_ELEMENTS(response_vec)); +} + +void lwan_fill_default_response(struct lwan_strbuf *buffer, + enum lwan_http_status status) +{ + lwan_tpl_apply_with_buffer( + error_template, buffer, + &(struct error_template){ + .short_message = lwan_http_status_as_string(status), + .long_message = lwan_http_status_as_descriptive_string(status), + }); +} + +void lwan_default_response(struct lwan_request *request, + enum lwan_http_status status) +{ + request->response.mime_type = "text/html"; + + lwan_fill_default_response(request->response.buffer, status); + lwan_response(request, status); +} + +#define RETURN_0_ON_OVERFLOW(len_) \ + if (UNLIKELY(p_headers + (len_) >= p_headers_end)) \ + return 0 + +#define APPEND_STRING_LEN(const_str_, len_) \ + do { \ + RETURN_0_ON_OVERFLOW(len_); \ + p_headers = mempcpy(p_headers, (const_str_), (len_)); \ + } while (0) + +#define APPEND_STRING(str_) \ + do { \ + size_t len = strlen(str_); \ + APPEND_STRING_LEN((str_), len); \ + } while (0) + +#define APPEND_CHAR(value_) \ + do { \ + RETURN_0_ON_OVERFLOW(1); \ + *p_headers++ = (value_); \ + } while (0) + +#define APPEND_CHAR_NOCHECK(value_) *p_headers++ = (value_) + +#define APPEND_UINT(value_) \ + do { \ + size_t len; \ + char *tmp = uint_to_string((value_), buffer, &len); \ + RETURN_0_ON_OVERFLOW(len); \ + APPEND_STRING_LEN(tmp, len); \ + } while (0) + +#define APPEND_CONSTANT(const_str_) \ + APPEND_STRING_LEN((const_str_), sizeof(const_str_) - 1) + +static ALWAYS_INLINE __attribute__((const)) bool +flags_has_content_length(enum lwan_request_flags v) +{ + return !(v & (RESPONSE_NO_CONTENT_LENGTH | RESPONSE_STREAM | + RESPONSE_CHUNKED_ENCODING)); +} + +static ALWAYS_INLINE __attribute__((const)) bool +has_uncommon_response_headers(enum lwan_request_flags v) +{ + return v & (RESPONSE_INCLUDE_REQUEST_ID | REQUEST_ALLOW_CORS | + RESPONSE_CHUNKED_ENCODING | REQUEST_WANTS_HSTS_HEADER); +} + +size_t lwan_prepare_response_header_full( + struct lwan_request *request, + enum lwan_http_status status, + char headers[], + size_t headers_buf_size, + const struct lwan_key_value *additional_headers) +{ + /* NOTE: If new response headers are added here, update + * can_override_header() in lwan.c */ + + char *p_headers; + char *p_headers_end = headers + headers_buf_size; + char buffer[INT_TO_STR_BUFFER_SIZE]; + const enum lwan_request_flags request_flags = request->flags; + const enum lwan_connection_flags conn_flags = request->conn->flags; + bool expires_override = !!(request->flags & (RESPONSE_NO_EXPIRES | REQUEST_HAS_QUERY_STRING)); + + assert(request->global_response_headers); + + p_headers = headers; + + if (UNLIKELY(request_flags & REQUEST_IS_HTTP_1_0)) + APPEND_CONSTANT("HTTP/1.0 "); + else + APPEND_CONSTANT("HTTP/1.1 "); + APPEND_STRING(lwan_http_status_as_string_with_code(status)); + + if (LIKELY(!additional_headers)) + goto skip_additional_headers; + + if (LIKELY((status < HTTP_CLASS__CLIENT_ERROR))) { + const struct lwan_key_value *header; + bool date_override = false; + + for (header = additional_headers; header->key; header++) { + STRING_SWITCH_L (header->key) { + case STR4_INT_L('S', 'e', 'r', 'v'): + if (LIKELY(streq(header->key + 4, "er"))) + continue; + break; + case STR4_INT_L('D', 'a', 't', 'e'): + if (LIKELY(*(header->key + 4) == '\0')) + date_override = true; + break; + case STR4_INT_L('E', 'x', 'p', 'i'): + if (LIKELY(streq(header->key + 4, "res"))) + expires_override = true; + break; + } + + RETURN_0_ON_OVERFLOW(4); + APPEND_CHAR_NOCHECK('\r'); + APPEND_CHAR_NOCHECK('\n'); + APPEND_STRING(header->key); + APPEND_CHAR_NOCHECK(':'); + APPEND_CHAR_NOCHECK(' '); + APPEND_STRING(header->value); + } + + if (date_override) + goto skip_date_header; + } else if (UNLIKELY(status == HTTP_NOT_AUTHORIZED)) { + const struct lwan_key_value *header; + + for (header = additional_headers; header->key; header++) { + if (streq(header->key, "WWW-Authenticate")) { + APPEND_CONSTANT("\r\nWWW-Authenticate: "); + APPEND_STRING(header->value); + break; + } + } + } + +skip_additional_headers: + APPEND_CONSTANT("\r\nDate: "); + APPEND_STRING_LEN(request->conn->thread->date.date, 29); + +skip_date_header: + if (UNLIKELY(conn_flags & CONN_IS_UPGRADE)) { + APPEND_CONSTANT("\r\nConnection: Upgrade"); + } else { + if (!(conn_flags & CONN_SENT_CONNECTION_HEADER)) { + if (LIKELY(conn_flags & CONN_IS_KEEP_ALIVE)) + APPEND_CONSTANT("\r\nConnection: keep-alive"); + else + APPEND_CONSTANT("\r\nConnection: close"); + request->conn->flags |= CONN_SENT_CONNECTION_HEADER; + } + + if (LIKELY(request->response.mime_type)) { + APPEND_CONSTANT("\r\nContent-Type: "); + APPEND_STRING(request->response.mime_type); + } + + if (!expires_override) { + APPEND_CONSTANT("\r\nExpires: "); + APPEND_STRING_LEN(request->conn->thread->date.expires, 29); + } + } + + const bool has_content_length = flags_has_content_length(request_flags); + if (LIKELY(has_content_length)) { + APPEND_CONSTANT("\r\nContent-Length: "); + APPEND_UINT(lwan_strbuf_get_length(request->response.buffer)); + } + if (UNLIKELY(has_uncommon_response_headers(request_flags))) { + if (request_flags & REQUEST_ALLOW_CORS) { + APPEND_CONSTANT( + "\r\nAccess-Control-Allow-Origin: *" + "\r\nAccess-Control-Allow-Methods: GET, POST, PUT, OPTIONS" + "\r\nAccess-Control-Allow-Credentials: true" + "\r\nAccess-Control-Allow-Headers: Origin, Accept, " + "Content-Type"); + } + if (!has_content_length && (request_flags & RESPONSE_CHUNKED_ENCODING)) { + APPEND_CONSTANT("\r\nTransfer-Encoding: chunked"); + } + if (request_flags & RESPONSE_INCLUDE_REQUEST_ID) { + APPEND_CONSTANT("\r\nX-Request-Id: "); + RETURN_0_ON_OVERFLOW(16); + uint64_t id = lwan_request_get_id(request); + for (int i = 60; i >= 0; i -= 4) + APPEND_CHAR_NOCHECK("0123456789abcdef"[(id >> i) & 0xf]); + } + if (request_flags & REQUEST_WANTS_HSTS_HEADER) { + APPEND_CONSTANT("\r\nStrict-Transport-Security: " + "max-age=31536000; includeSubdomains"); + } + } + + APPEND_STRING_LEN(request->global_response_headers->value, + request->global_response_headers->len); + + return (size_t)(p_headers - headers); +} + +#undef APPEND_CHAR +#undef APPEND_CHAR_NOCHECK +#undef APPEND_CONSTANT +#undef APPEND_STRING +#undef APPEND_STRING_LEN +#undef APPEND_UINT +#undef RETURN_0_ON_OVERFLOW + +ALWAYS_INLINE size_t lwan_prepare_response_header(struct lwan_request *request, + enum lwan_http_status status, + char headers[], + size_t headers_buf_size) +{ + return lwan_prepare_response_header_full( + request, status, headers, headers_buf_size, request->response.headers); +} + +bool lwan_response_set_chunked_full(struct lwan_request *request, + enum lwan_http_status status, + const struct lwan_key_value *additional_headers) +{ + char buffer[DEFAULT_BUFFER_SIZE]; + size_t buffer_len; + + if (request->flags & RESPONSE_SENT_HEADERS) + return false; + + request->flags |= RESPONSE_CHUNKED_ENCODING; + buffer_len = lwan_prepare_response_header_full(request, status, buffer, + DEFAULT_BUFFER_SIZE, + additional_headers); + if (UNLIKELY(!buffer_len)) + return false; + + request->flags |= RESPONSE_SENT_HEADERS; + lwan_send(request, buffer, buffer_len, MSG_MORE); + + return true; +} + +inline bool lwan_response_set_chunked(struct lwan_request *request, + enum lwan_http_status status) +{ + return lwan_response_set_chunked_full(request, status, + request->response.headers); +} + +void lwan_response_send_chunk_full(struct lwan_request *request, + struct lwan_strbuf *strbuf) +{ + if (!(request->flags & RESPONSE_SENT_HEADERS)) { + if (UNLIKELY(!lwan_response_set_chunked(request, HTTP_OK))) + return; + } + + size_t buffer_len = lwan_strbuf_get_length(strbuf); + if (UNLIKELY(!buffer_len)) { + static const char last_chunk[] = "0\r\n\r\n"; + lwan_send(request, last_chunk, sizeof(last_chunk) - 1, 0); + return; + } + + char chunk_size[3 * sizeof(size_t) + 2]; + int converted_len = + snprintf(chunk_size, sizeof(chunk_size), "%zx\r\n", buffer_len); + if (UNLIKELY(converted_len < 0 || + (size_t)converted_len >= sizeof(chunk_size))) { + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + size_t chunk_size_len = (size_t)converted_len; + + struct iovec chunk_vec[] = { + {.iov_base = chunk_size, .iov_len = chunk_size_len}, + {.iov_base = lwan_strbuf_get_buffer(strbuf), .iov_len = buffer_len}, + {.iov_base = "\r\n", .iov_len = 2}, + }; + + lwan_writev(request, chunk_vec, N_ELEMENTS(chunk_vec)); + + lwan_strbuf_reset(strbuf); +} + +void lwan_response_send_chunk(struct lwan_request *request) +{ + return lwan_response_send_chunk_full(request, request->response.buffer); +} + +bool lwan_response_set_event_stream(struct lwan_request *request, + enum lwan_http_status status) +{ + char buffer[DEFAULT_BUFFER_SIZE]; + size_t buffer_len; + + if (request->flags & RESPONSE_SENT_HEADERS) + return false; + + request->response.mime_type = "text/event-stream"; + request->flags |= RESPONSE_NO_CONTENT_LENGTH; + buffer_len = lwan_prepare_response_header(request, status, buffer, + DEFAULT_BUFFER_SIZE); + if (UNLIKELY(!buffer_len)) + return false; + + request->flags |= RESPONSE_SENT_HEADERS; + lwan_send(request, buffer, buffer_len, MSG_MORE); + + return true; +} + +void lwan_response_send_event(struct lwan_request *request, const char *event) +{ + struct iovec vec[6]; + int last = 0; + + if (!(request->flags & RESPONSE_SENT_HEADERS)) { + if (UNLIKELY(!lwan_response_set_event_stream(request, HTTP_OK))) + return; + } + + if (event) { + vec[last++] = (struct iovec){ + .iov_base = "event: ", + .iov_len = sizeof("event: ") - 1, + }; + vec[last++] = (struct iovec){ + .iov_base = (char *)event, + .iov_len = strlen(event), + }; + vec[last++] = (struct iovec){ + .iov_base = "\r\n", + .iov_len = 2, + }; + } + + size_t buffer_len = lwan_strbuf_get_length(request->response.buffer); + if (buffer_len) { + vec[last++] = (struct iovec){ + .iov_base = "data: ", + .iov_len = sizeof("data: ") - 1, + }; + vec[last++] = (struct iovec){ + .iov_base = lwan_strbuf_get_buffer(request->response.buffer), + .iov_len = buffer_len, + }; + } + + vec[last++] = (struct iovec){ + .iov_base = "\r\n\r\n", + .iov_len = 4, + }; + + lwan_writev(request, vec, last); + + lwan_strbuf_reset(request->response.buffer); + coro_yield(request->conn->coro, CONN_CORO_WANT_WRITE); +} diff --git a/src/lib/lwan-socket.c b/src/lib/lwan-socket.c new file mode 100644 index 000000000..213bbeec2 --- /dev/null +++ b/src/lib/lwan-socket.c @@ -0,0 +1,376 @@ +/* + * lwan - web server + * Copyright (c) 2012, 2013 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "int-to-str.h" +#include "sd-daemon.h" + +#ifdef __linux__ +LWAN_LAZY_GLOBAL(bool, is_reno_supported) +{ + FILE *allowed; + bool supported = false; + + allowed = fopen("/proc/sys/net/ipv4/tcp_allowed_congestion_control", "re"); + if (allowed) { + char line[4096]; + if (fgets(line, sizeof(line), allowed)) { + if (strstr(line, "reno")) + supported = true; + } + fclose(allowed); + } + + return supported; +} +#endif + +LWAN_LAZY_GLOBAL(int, get_backlog_size) +{ + int backlog_size = SOMAXCONN; + +#ifdef __linux__ + FILE *somaxconn = fopen("/proc/sys/net/core/somaxconn", "re"); + + if (somaxconn) { + int tmp; + if (fscanf(somaxconn, "%d", &tmp) == 1) + backlog_size = tmp; + fclose(somaxconn); + } +#elif defined(KIPC_SOMAXCONN) || defined(KERN_SOMAXCONN) + +#if defined(KERN_SOMAXCONN) /* OpenBSD */ + int mib[] = {CTL_KERN, KERN_SOMAXCONN, -1}; +#else /* FreeBSD, macOS */ + int mib[] = {CTL_KERN, KERN_IPC, KIPC_SOMAXCONN, -1}; +#endif + int tmp = 0; + + if (!sysctl(mib, N_ELEMENTS(mib), NULL, NULL, &tmp, sizeof(tmp))) + backlog_size = tmp; +#endif + + return backlog_size; +} + +static int set_socket_flags(int fd) +{ + int flags = fcntl(fd, F_GETFD); + if (flags < 0) + lwan_status_critical_perror("Could not obtain socket flags"); + if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) + lwan_status_critical_perror("Could not set socket flags"); + + flags = fcntl(fd, F_GETFL); + if (flags < 0) + lwan_status_critical_perror("Could not obtain socket flags"); + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + lwan_status_critical_perror("Could not set socket flags"); + + return fd; +} + +static sa_family_t parse_listener_ipv4(char *listener, char **node, char **port) +{ + char *colon = strrchr(listener, ':'); + if (!colon) { + *port = "8080"; + if (!strchr(listener, '.')) { + /* 8080 */ + *node = "0.0.0.0"; + } else { + /* 127.0.0.1 */ + *node = listener; + } + } else { + /* + * 127.0.0.1:8080 + * localhost:8080 + */ + *colon = '\0'; + *node = listener; + *port = colon + 1; + + if (streq(*node, "*")) { + /* *:8080 */ + *node = "0.0.0.0"; + + return AF_UNSPEC; /* IPv4 or IPv6 */ + } + } + + return AF_INET; +} + +static sa_family_t parse_listener_ipv6(char *listener, char **node, char **port) +{ + char *last_colon = strrchr(listener, ':'); + if (!last_colon) + return AF_MAX; + + if (*(last_colon - 1) == ']') { + /* [::]:8080 */ + *(last_colon - 1) = '\0'; + *node = listener + 1; + *port = last_colon + 1; + } else { + /* [::1] */ + listener[strlen(listener) - 1] = '\0'; + *node = listener + 1; + *port = "8080"; + } + + return AF_INET6; +} + +sa_family_t lwan_socket_parse_address(char *listener, char **node, char **port) +{ + if (*listener == '[') + return parse_listener_ipv6(listener, node, port); + + return parse_listener_ipv4(listener, node, port); +} + +static int listen_addrinfo(int fd, + const struct addrinfo *addr, + bool print_listening_msg, + bool is_https) +{ + if (listen(fd, get_backlog_size()) < 0) + lwan_status_critical_perror("listen"); + + if (print_listening_msg) { + const char *s_if_https = is_https ? "s" : ""; + char host_buf[NI_MAXHOST], serv_buf[NI_MAXSERV]; + int ret = getnameinfo(addr->ai_addr, addr->ai_addrlen, host_buf, + sizeof(host_buf), serv_buf, sizeof(serv_buf), + NI_NUMERICHOST | NI_NUMERICSERV); + if (ret) + lwan_status_critical("getnameinfo: %s", gai_strerror(ret)); + + if (addr->ai_family == AF_INET6) + lwan_status_info("Listening on http%s://[%s]:%s", s_if_https, + host_buf, serv_buf); + else + lwan_status_info("Listening on http%s://%s:%s", s_if_https, + host_buf, serv_buf); + } + + return set_socket_flags(fd); +} + +#define SET_SOCKET_OPTION(_domain, _option, _param) \ + do { \ + const socklen_t _param_size_ = (socklen_t)sizeof(*(_param)); \ + if (setsockopt(fd, (_domain), (_option), (_param), _param_size_) < 0) \ + lwan_status_critical_perror("setsockopt"); \ + } while (0) + +#define SET_SOCKET_OPTION_MAY_FAIL(_domain, _option, _param) \ + do { \ + const socklen_t _param_size_ = (socklen_t)sizeof(*(_param)); \ + if (setsockopt(fd, (_domain), (_option), (_param), _param_size_) < 0) \ + lwan_status_perror("%s not supported by the kernel", #_option); \ + } while (0) + +static int bind_and_listen_addrinfos(const struct addrinfo *addrs, + bool print_listening_msg, + bool is_https) +{ + const struct addrinfo *addr; + + /* Try each address until we bind one successfully. */ + for (addr = addrs; addr; addr = addr->ai_next) { + int fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); + if (fd < 0) + continue; + + SET_SOCKET_OPTION(SOL_SOCKET, SO_REUSEADDR, (int[]){1}); +#ifdef SO_REUSEPORT + SET_SOCKET_OPTION(SOL_SOCKET, SO_REUSEPORT, (int[]){1}); +#else + lwan_status_critical("SO_REUSEPORT not supported by the OS"); +#endif + + if (!bind(fd, addr->ai_addr, addr->ai_addrlen)) + return listen_addrinfo(fd, addr, print_listening_msg, is_https); + + close(fd); + } + + lwan_status_critical("Could not bind socket"); +} + +static int set_socket_options(const struct lwan *l, int fd) +{ + SET_SOCKET_OPTION(SOL_SOCKET, SO_LINGER, + (&(struct linger){.l_onoff = 1, .l_linger = 1})); + +#ifdef __linux__ + +#ifndef TCP_FASTOPEN +#define TCP_FASTOPEN 23 +#endif + + SET_SOCKET_OPTION_MAY_FAIL(SOL_SOCKET, SO_REUSEADDR, (int[]){1}); + SET_SOCKET_OPTION_MAY_FAIL(SOL_TCP, TCP_NODELAY, (int[]){1}); + SET_SOCKET_OPTION_MAY_FAIL(SOL_TCP, TCP_FASTOPEN, (int[]){5}); + SET_SOCKET_OPTION_MAY_FAIL(SOL_TCP, TCP_QUICKACK, (int[]){0}); + SET_SOCKET_OPTION_MAY_FAIL(SOL_TCP, TCP_DEFER_ACCEPT, + (int[]){(int)l->config.keep_alive_timeout}); + + if (is_reno_supported()) + setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "reno", 4); +#else + (void)l; +#endif + + return fd; +} + +static int setup_socket_normally(const struct lwan *l, + bool print_listening_msg, + bool is_https, + const char *listener_from_config) +{ + char *node, *port; + char *listener = strdupa(listener_from_config); + sa_family_t family = lwan_socket_parse_address(listener, &node, &port); + + if (family == AF_MAX) { + lwan_status_critical("Could not parse listener: %s", + l->config.listener); + } + + struct addrinfo *addrs; + struct addrinfo hints = {.ai_family = family, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_PASSIVE}; + + int ret = getaddrinfo(node, port, &hints, &addrs); + if (ret) + lwan_status_critical("getaddrinfo: %s", gai_strerror(ret)); + + int fd = bind_and_listen_addrinfos(addrs, print_listening_msg, is_https); + freeaddrinfo(addrs); + return set_socket_options(l, fd); +} + +static int from_systemd_socket(const struct lwan *l, int fd) +{ + if (sd_is_socket_inet(fd, AF_INET, SOCK_STREAM, 1, 0) == 1 || + sd_is_socket_inet(fd, AF_INET6, SOCK_STREAM, 1, 0) == 1) { + return set_socket_options(l, set_socket_flags(fd)); + } + + lwan_status_critical("Passed file descriptor is not a " + "listening TCP socket"); +} + +int lwan_create_listen_socket(const struct lwan *l, + bool print_listening_msg, + bool is_https) +{ + const char *listener = + is_https ? l->config.tls_listener : l->config.listener; + + if (!strncmp(listener, "systemd:", sizeof("systemd:") - 1)) { + char **names = NULL; + int n = sd_listen_fds_with_names(false, &names); + int fd = -1; + + listener += sizeof("systemd:") - 1; + + if (n < 0) { + errno = -n; + lwan_status_perror( + "Could not parse socket activation data from systemd"); + return n; + } + + if (n == 0) { + lwan_status_critical("Not invoked from systemd " + "(expecting socket name %s)", + listener); + } + + for (int i = 0; i < n; i++) { + if (!strcmp(names[i], listener)) { + fd = SD_LISTEN_FDS_START + i; + break; + } + } + + strv_free(names); + + if (fd < 0) { + lwan_status_critical( + "No socket named `%s' has been passed from systemd", listener); + } + + return from_systemd_socket(l, fd); + } + + if (streq(listener, "systemd")) { + int n = sd_listen_fds(false); + + if (n < 0) { + errno = -n; + lwan_status_perror("Could not obtain sockets passed from systemd"); + return n; + } + + if (n == 0) { + lwan_status_critical( + "Not invoked from systemd (expecting 1 socket)"); + } + + if (n != 1) { + lwan_status_critical( + "%d listeners passed from systemd. Must specify listeners with " + "systemd:listener-name syntax", + n); + } + + return from_systemd_socket(l, SD_LISTEN_FDS_START); + } + + return setup_socket_normally(l, print_listening_msg, is_https, listener); +} + +#undef SET_SOCKET_OPTION +#undef SET_SOCKET_OPTION_MAY_FAIL diff --git a/src/lib/lwan-status.c b/src/lib/lwan-status.c new file mode 100644 index 000000000..464f6ae25 --- /dev/null +++ b/src/lib/lwan-status.c @@ -0,0 +1,335 @@ +/* + * lwan - web server + * Copyright (c) 2021 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" +#include "lwan-status.h" + +enum lwan_status_type { + STATUS_INFO = 0, + STATUS_WARNING = 1, + STATUS_ERROR = 2, + STATUS_DEBUG = 3, + STATUS_PERROR = 4, + STATUS_NONE = 5, + /* [6,7] are unused so that CRITICAL can be ORed with previous items */ + STATUS_CRITICAL = 8, +}; + +static bool can_use_colors(void); +static bool can_use_emojis(void); + +static volatile bool quiet = false; +static bool use_colors; +static bool use_emojis; + +void lwan_status_init(struct lwan *l) +{ +#ifdef NDEBUG + quiet = l->config.quiet; +#else + quiet = false; + (void)l; +#endif + use_colors = can_use_colors(); + use_emojis = can_use_emojis(); +} + +void lwan_status_shutdown(struct lwan *l __attribute__((unused))) {} + +static bool can_use_colors(void) +{ + const char *term; + const char *no_color; + + if (!isatty(fileno(stdout))) + return false; + + /* From https://no-color.org: "Command-line software which adds ANSI + * color to its output by default should check for a NO_COLOR + * environment variable that, when present and not an empty string + * (regardless of its value), prevents the addition of ANSI color." */ + no_color = secure_getenv("NO_COLOR"); + if (no_color && no_color[0]) + return false; + + term = secure_getenv("TERM"); + if (term && streq(term, "dumb")) + return false; + + return true; +} + +static bool can_use_emojis(void) +{ + if (!can_use_colors()) + return false; + + const char *lang = secure_getenv("LANG"); + if (!lang) + return false; + + if (!strstr(lang, ".UTF-8")) + return false; + + return true; +} + +static int status_index(enum lwan_status_type type) +{ + return use_colors ? (int)type : STATUS_NONE; +} + +#define V(c) { .value = c, .len = sizeof(c) - 1 } +static const struct lwan_value start_colors[] = { + [STATUS_INFO] = V("\033[36m"), + [STATUS_WARNING] = V("\033[33m"), + [STATUS_DEBUG] = V("\033[37m"), + [STATUS_PERROR] = V("\033[35m"), + [STATUS_CRITICAL] = V("\033[31;1m"), + [STATUS_NONE] = V(""), + [STATUS_ERROR] = V("\033[35m"), + [STATUS_CRITICAL | STATUS_PERROR] = V("\033[31;1m"), +}; + +static inline struct lwan_value start_color(enum lwan_status_type type) +{ + return start_colors[status_index(type)]; +} + +static inline struct lwan_value end_color(void) +{ + return use_colors ? (struct lwan_value)V("\033[0m\n") + : (struct lwan_value)V("\n"); +} +#undef V + +static inline char *strerror_thunk_r(int error_number, char *buffer, size_t len) +{ +#if defined(__GLIBC__) && defined(_GNU_SOURCE) + return strerror_r(error_number, buffer, len); +#else /* XSI-compliant strerror_r() */ + if (!strerror_r(error_number, buffer, len)) + return buffer; + return "Unknown"; +#endif +} + +#ifndef NDEBUG + +LWAN_LAZY_THREAD_LOCAL(long, gettid_cached) +{ + return gettid(); +} + +LWAN_LAZY_THREAD_LOCAL(const char *, get_thread_emoji) +{ + static const char *emojis[] = { + "🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", + "🐷", "🐽", "🐸", "🐵", "🐔", "🐧", "🐦", "🐤", "🦆", "🦉", "🦇", "🐺", + "🐗", "🐴", "🦄", "🐝", "🪱", "🐛", "🦋", "🐌", "🐞", "🐜", "🪰", "🪲", + "🪳", "🦟", "🦗", "🦂", "🐢", "🐍", "🦎", "🦖", "🦕", "🐙", "🦑", "🦐", + "🦞", "🦀", "🐡", "🐠", "🐟", "🐬", "🐳", "🐋", "🦈", "🦭", "🐊", "🐅", + "🐆", "🦓", "🦍", "🦧", "🦣", "🐘", "🦛", "🦏", "🐪", "🐫", "🦒", "🦘", + "🦬", "🐃", "🐂", "🐄", "🐎", "🐖", "🐏", "🐑", "🦙", "🐐", "🦌", "🐕", + "🐩", "🐈", "🐓", "🦃", "🦤", "🦚", "🦜", "🦢", "🦩", "🕊", "🐇", "🦝", + "🦨", "🦡", "🦫", "🦦", "🦥", "🐁", "🐀", "🐿", "🦔", "🐉", "🐲", + }; + static unsigned int last_emoji_id; + return emojis[ATOMIC_INC(last_emoji_id) % (int)N_ELEMENTS(emojis)]; +} +#endif + +#define FORMAT_WITH_COLOR(fmt, color) "\033[" color "m" fmt "\033[0m" + +#ifdef LWAN_HAVE_SYSLOG + +#include +#include + +static int status_to_syslog_prio[] = { + [STATUS_CRITICAL | STATUS_PERROR] = LOG_CRIT, + [STATUS_CRITICAL] = LOG_CRIT, + [STATUS_ERROR] = LOG_ERR, + [STATUS_WARNING] = LOG_WARNING, + [STATUS_INFO] = LOG_INFO, + [STATUS_DEBUG] = LOG_DEBUG, +}; + +void lwan_syslog_status_out( +#ifndef NDEBUG + const char *file, + const int line, + const char *func, + const long tid, +#endif + enum lwan_status_type type, + int saved_errno, + const char *fmt, + va_list values) +{ + char syslog_buffer[256]; + va_list copied_values; + struct lwan_strbuf buf; + + lwan_strbuf_init_with_fixed_buffer(&buf, syslog_buffer, + sizeof(syslog_buffer)); + +#ifndef NDEBUG + if (!lwan_strbuf_append_printf(&buf, "%ld %s:%d %s() ", tid, + basename(strdupa(file)), line, func)) + goto out; +#endif + + va_copy(copied_values, values); + if (!lwan_strbuf_append_vprintf(&buf, fmt, copied_values)) + goto out; + + if (type & STATUS_PERROR) { + char errbuf[128]; + + if (!lwan_strbuf_append_strz( + &buf, + strerror_thunk_r(saved_errno, errbuf, sizeof(errbuf) - 1))) + goto out; + } + + syslog(status_to_syslog_prio[type], "%.*s", + (int)lwan_strbuf_get_length(&buf), lwan_strbuf_get_buffer(&buf)); + +out: + lwan_strbuf_free(&buf); +} + +LWAN_CONSTRUCTOR(register_lwan_to_syslog, 0) +{ + openlog("lwan", LOG_NDELAY | LOG_PID | LOG_CONS, LOG_USER); +} +#else +#define lwan_syslog_status_out(...) +#endif + +static void status_out( +#ifndef NDEBUG + const char *file, + const int line, + const char *func, +#endif + enum lwan_status_type type, + const char *fmt, + va_list values) +{ + struct lwan_value start = start_color(type); + struct lwan_value end = end_color(); + int saved_errno = errno; + +#ifndef NDEBUG + lwan_syslog_status_out(file, line, func, gettid_cached(), type, saved_errno, + fmt, values); +#else + lwan_syslog_status_out(type, saved_errno, fmt, values); +#endif + + flockfile(stdout); + +#ifndef NDEBUG + char *base_name = basename(strdupa(file)); + if (LIKELY(use_colors)) { + if (LIKELY(use_emojis)) + printf("%s ", get_thread_emoji()); + else + printf(FORMAT_WITH_COLOR("%ld ", "32;1"), gettid_cached()); + printf(FORMAT_WITH_COLOR("%s:%d ", "3"), base_name, line); + printf(FORMAT_WITH_COLOR("%s() ", "33"), func); + } else { + printf("%ld %s:%d %s() ", gettid_cached(), base_name, line, func); + } +#endif + + fwrite_unlocked(start.value, start.len, 1, stdout); + vprintf(fmt, values); + + if (UNLIKELY(type & STATUS_PERROR)) { + char errbuf[64]; + char *errmsg = + strerror_thunk_r(saved_errno, errbuf, sizeof(errbuf) - 1); + + printf(": %s (error number %d)", errmsg, saved_errno); + } + + fwrite_unlocked(end.value, end.len, 1, stdout); + + funlockfile(stdout); + + errno = saved_errno; +} + +#undef FORMAT_WITH_COLOR + +#ifdef NDEBUG +#define IMPLEMENT_FUNCTION(fn_name_, type_) \ + void lwan_status_##fn_name_(const char *fmt, ...) \ + { \ + if (LIKELY(!quiet)) { \ + va_list values; \ + va_start(values, fmt); \ + status_out(type_, fmt, values); \ + va_end(values); \ + } \ + if (UNLIKELY((type_)&STATUS_CRITICAL)) \ + exit(1); \ + } +#else +#define IMPLEMENT_FUNCTION(fn_name_, type_) \ + void lwan_status_##fn_name_##_debug(const char *file, const int line, \ + const char *func, const char *fmt, \ + ...) \ + { \ + if (LIKELY(!quiet)) { \ + va_list values; \ + va_start(values, fmt); \ + status_out(file, line, func, type_, fmt, values); \ + va_end(values); \ + } \ + if (UNLIKELY((type_)&STATUS_CRITICAL)) \ + abort(); \ + } + +IMPLEMENT_FUNCTION(debug, STATUS_DEBUG) +#endif + +IMPLEMENT_FUNCTION(info, STATUS_INFO) +IMPLEMENT_FUNCTION(warning, STATUS_WARNING) +IMPLEMENT_FUNCTION(error, STATUS_ERROR) +IMPLEMENT_FUNCTION(perror, STATUS_PERROR) + +IMPLEMENT_FUNCTION(critical, STATUS_CRITICAL) +IMPLEMENT_FUNCTION(critical_perror, STATUS_CRITICAL | STATUS_PERROR) + +#undef IMPLEMENT_FUNCTION diff --git a/common/lwan-status.h b/src/lib/lwan-status.h similarity index 90% rename from common/lwan-status.h rename to src/lib/lwan-status.h index 67c0b2bb4..a3ca800e5 100644 --- a/common/lwan-status.h +++ b/src/lib/lwan-status.h @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2013 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2013 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -23,14 +23,16 @@ #define DECLARE_STATUS_PROTO(type_, ...) \ void lwan_status_##type_(const char *fmt, ...) \ __attribute__((format(printf, 1, 2))) \ + __attribute__((noinline,cold)) \ __VA_ARGS__; -#define lwan_status_debug(fmt, ...) +#define lwan_status_debug(fmt, ...) do {} while(0) #else #define DECLARE_STATUS_PROTO(type_, ...) \ void lwan_status_##type_##_debug(const char *file, const int line, \ const char *func, const char *fmt, ...) \ __attribute__((format(printf, 4, 5))) \ + __attribute__((noinline,cold)) \ __VA_ARGS__; #define lwan_status_info(fmt, ...) \ diff --git a/src/lib/lwan-straitjacket.c b/src/lib/lwan-straitjacket.c new file mode 100644 index 000000000..cffa5c964 --- /dev/null +++ b/src/lib/lwan-straitjacket.c @@ -0,0 +1,273 @@ +/* + * lwan - web server + * Copyright (c) 2015 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "lwan-config.h" +#include "lwan-status.h" + +static bool get_user_uid_gid(const char *user, uid_t *uid, gid_t *gid) +{ + struct passwd pwd = {}; + struct passwd *result; + char *buf; + long pw_size_max = sysconf(_SC_GETPW_R_SIZE_MAX); + int r; + + if (pw_size_max < 0) { + /* This constant is returned for sysconf(_SC_GETPW_R_SIZE_MAX) in glibc, + * and it seems to be a reasonable size (1024). Use it as a fallback in + * the (very unlikely) case where sysconf() fails. */ + pw_size_max = NSS_BUFLEN_PASSWD; + } + + buf = malloc((size_t)pw_size_max); + if (!buf) { + lwan_status_error("Could not allocate buffer for passwd struct"); + return false; + } + + r = getpwnam_r(user, &pwd, buf, (size_t)pw_size_max, &result); + *uid = pwd.pw_uid; + *gid = pwd.pw_gid; + free(buf); + + if (result) + return true; + + if (!r) { + lwan_status_error("Username not found: %s", user); + } else { + errno = r; + lwan_status_perror("Could not obtain uid/gid for user %s", user); + } + + return false; +} + +static bool switch_to_user(uid_t uid, gid_t gid, const char *username) +{ + uid_t ruid, euid, suid; + gid_t rgid, egid, sgid; + + lwan_status_info("Dropping privileges to UID %d, GID %d (%s)", uid, gid, + username); + + if (setresgid(gid, gid, gid) < 0) + return false; +#if defined(__APPLE__) + if (initgroups(username, (int)gid) < 0) + return false; +#else + if (initgroups(username, gid) < 0) + return false; +#endif + if (setresuid(uid, uid, uid) < 0) + return false; + + if (getresuid(&ruid, &euid, &suid) < 0) + return false; + if (ruid != euid || euid != suid || suid != uid) + return false; + + if (getresgid(&rgid, &egid, &sgid) < 0) + return false; + if (rgid != egid || egid != sgid || sgid != gid) + return false; + + return true; +} + +#ifdef __linux__ +static void abort_on_open_directories(void) +{ + /* This is racy, but is a way to detect misconfiguration. Since it's + * called just once during boot time, before threads are created, this + * should be fine (maybe not if Lwan is used as a library.) + */ + DIR *dir = opendir("/proc/self/fd"); + struct dirent *ent; + char own_fd[3 * sizeof(int)]; + int ret; + + if (!dir) { + lwan_status_critical_perror( + "Could not determine if there are open directory fds"); + } + + ret = snprintf(own_fd, sizeof(own_fd), "%d", dirfd(dir)); + if (ret < 0 || ret >= (int)sizeof(own_fd)) { + lwan_status_critical("Could not get descriptor of /proc/self/fd"); + } + + while ((ent = readdir(dir))) { + char path[PATH_MAX]; + struct stat st; + ssize_t len; + + if (streq(ent->d_name, own_fd)) + continue; + if (streq(ent->d_name, ".") || streq(ent->d_name, "..")) + continue; + + len = readlinkat(dirfd(dir), ent->d_name, path, sizeof(path)); + if (len < 0) { + lwan_status_critical_perror("Could not get information about fd %s", + ent->d_name); + } + path[len] = '\0'; + + if (path[0] != '/') { + /* readlink() there will point to the realpath() of a file, so + * if it's on a filesystem, it starts with '/'. Sockets, for + * instance, begin with "socket:" instead... so no need for + * stat(). */ + continue; + } + + if (stat(path, &st) < 0) { + lwan_status_critical_perror( + "Could not get information about open file: %s", path); + } + + if (S_ISDIR(st.st_mode)) { + closedir(dir); + + lwan_status_critical( + "The directory '%s' is open (fd %s), can't chroot", path, + ent->d_name); + return; + } + } + + closedir(dir); +} +#else +static void abort_on_open_directories(void) {} +#endif + +void lwan_straitjacket_enforce(const struct lwan_straitjacket *sj) +{ + uid_t uid = 0; + gid_t gid = 0; + bool got_uid_gid = false; + + if (!sj->user_name && !sj->chroot_path) + goto out; + + if (geteuid() != 0) + lwan_status_critical("Straitjacket requires root privileges"); + + if (sj->user_name && *sj->user_name) { + if (!get_user_uid_gid(sj->user_name, &uid, &gid)) + lwan_status_critical("Unknown user: %s", sj->user_name); + got_uid_gid = true; + } + + if (sj->chroot_path) { + abort_on_open_directories(); + + if (chroot(sj->chroot_path) < 0) { + lwan_status_critical_perror("Could not chroot() to %s", + sj->chroot_path); + } + + if (chdir("/") < 0) + lwan_status_critical_perror("Could not chdir() to /"); + + lwan_status_info("Jailed to %s", sj->chroot_path); + } + + if (got_uid_gid && !switch_to_user(uid, gid, sj->user_name)) { + lwan_status_critical("Could not drop privileges to %s, aborting", + sj->user_name); + } + +out: + if (sj->drop_capabilities) { + struct __user_cap_header_struct header = { + .version = _LINUX_CAPABILITY_VERSION_1, + }; + struct __user_cap_data_struct data = {}; + + if (capset(&header, &data) < 0) + lwan_status_critical_perror("Could not drop capabilities"); + } +} + +void lwan_straitjacket_enforce_from_config(struct config *c) +{ + const struct config_line *l; + char *user_name = NULL; + char *chroot_path = NULL; + bool drop_capabilities = true; + + while ((l = config_read_line(c))) { + switch (l->type) { + case CONFIG_LINE_TYPE_LINE: + /* TODO: limit_syscalls */ + if (streq(l->key, "user")) { + if (user_name) { + config_error(c, "`user' already specified"); + return; + } + user_name = strdupa(l->value); + } else if (streq(l->key, "chroot")) { + if (chroot_path) { + config_error(c, "`chroot' already specified"); + return; + } + chroot_path = strdupa(l->value); + } else if (streq(l->key, "drop_capabilities")) { + drop_capabilities = parse_bool(l->value, true); + } else { + config_error(c, "Invalid key: %s", l->key); + return; + } + break; + case CONFIG_LINE_TYPE_SECTION: + config_error(c, "Straitjacket accepts no sections"); + return; + case CONFIG_LINE_TYPE_SECTION_END: + lwan_straitjacket_enforce(&(struct lwan_straitjacket){ + .user_name = user_name, + .chroot_path = chroot_path, + .drop_capabilities = drop_capabilities, + }); + + return; + } + } + + config_error(c, "Expecting section end while parsing straitjacket"); +} diff --git a/src/lib/lwan-strbuf.c b/src/lib/lwan-strbuf.c new file mode 100644 index 000000000..9b8fc71ab --- /dev/null +++ b/src/lib/lwan-strbuf.c @@ -0,0 +1,459 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +static const unsigned int BUFFER_MALLOCD = 1 << 0; +static const unsigned int STRBUF_MALLOCD = 1 << 1; +static const unsigned int BUFFER_FIXED = 1 << 2; +static const unsigned int GROW_BUFFER_FAILED = 1 << 3; + +bool lwan_strbuf_has_grow_buffer_failed_flag(const struct lwan_strbuf *s) +{ + return s->flags & GROW_BUFFER_FAILED; +} + +static inline size_t align_size(size_t unaligned_size) +{ + const size_t aligned_size = lwan_nextpow2(unaligned_size); + + if (UNLIKELY(unaligned_size >= aligned_size)) + return 0; + + return aligned_size; +} + +static ALWAYS_INLINE +bool grow_buffer_if_needed_internal(struct lwan_strbuf *s, size_t size) +{ + if (s->flags & BUFFER_FIXED) + return size < s->capacity; + + /* Ensure we always have space for the NUL character! */ + if (UNLIKELY(__builtin_add_overflow(size, 1, &size))) + return false; + + if (!(s->flags & BUFFER_MALLOCD)) { + const size_t aligned_size = align_size(LWAN_MAX(size, s->used)); + if (UNLIKELY(!aligned_size)) + return false; + + char *buffer = malloc(aligned_size); + if (UNLIKELY(!buffer)) + return false; + + memcpy(buffer, s->buffer, s->used); + buffer[s->used + 1] = '\0'; + + s->flags |= BUFFER_MALLOCD; + s->buffer = buffer; + s->capacity = aligned_size; + + return true; + } + + if (UNLIKELY(s->capacity < size)) { + char *buffer; + const size_t aligned_size = align_size(size); + + if (UNLIKELY(!aligned_size)) + return false; + + if (s->used == 0) { + /* Avoid memcpy() inside realloc() if we were not using the + * allocated buffer at this point. */ + buffer = malloc(aligned_size); + + if (UNLIKELY(!buffer)) + return false; + + free(s->buffer); + buffer[0] = '\0'; + } else { + buffer = realloc(s->buffer, aligned_size); + + if (UNLIKELY(!buffer)) + return false; + } + + s->buffer = buffer; + s->capacity = aligned_size; + } + + return true; +} + +static bool grow_buffer_if_needed(struct lwan_strbuf *s, size_t size) +{ + if (UNLIKELY(!grow_buffer_if_needed_internal(s, size))) { + s->flags |= GROW_BUFFER_FAILED; + return false; + } + + return true; +} + +bool lwan_strbuf_init_with_size(struct lwan_strbuf *s, size_t size) +{ + if (UNLIKELY(!s)) + return false; + + *s = LWAN_STRBUF_STATIC_INIT; + + if (size) { + if (UNLIKELY(!grow_buffer_if_needed(s, size))) + return false; + + s->buffer[0] = '\0'; + } + + return true; +} + +bool lwan_strbuf_init_with_fixed_buffer(struct lwan_strbuf *s, + void *buffer, + size_t size) +{ + if (UNLIKELY(!s)) + return false; + + *s = (struct lwan_strbuf) { + .capacity = size, + .used = 0, + .buffer = buffer, + .flags = BUFFER_FIXED, + }; + + return true; +} + +ALWAYS_INLINE bool lwan_strbuf_init(struct lwan_strbuf *s) +{ + return lwan_strbuf_init_with_size(s, 0); +} + +struct lwan_strbuf *lwan_strbuf_new_with_size(size_t size) +{ + struct lwan_strbuf *s = malloc(sizeof(*s)); + + if (UNLIKELY(!lwan_strbuf_init_with_size(s, size))) { + free(s); + + return NULL; + } + + s->flags |= STRBUF_MALLOCD; + + return s; +} + +struct lwan_strbuf *lwan_strbuf_new_with_fixed_buffer(size_t size) +{ + struct lwan_strbuf *s; + size_t alloc_size; + + if (UNLIKELY(__builtin_add_overflow(sizeof(*s) + 1, size, &alloc_size))) + return NULL; + + s = malloc(alloc_size); + if (UNLIKELY(!lwan_strbuf_init_with_fixed_buffer(s, s + 1, size))) { + free(s); + return NULL; + } + + s->flags |= STRBUF_MALLOCD; + + return s; +} + +ALWAYS_INLINE struct lwan_strbuf *lwan_strbuf_new(void) +{ + return lwan_strbuf_new_with_size(0); +} + +ALWAYS_INLINE struct lwan_strbuf *lwan_strbuf_new_static(const char *str, + size_t size) +{ + struct lwan_strbuf *s = malloc(sizeof(*s)); + + if (UNLIKELY(!s)) + return NULL; + + *s = (struct lwan_strbuf) { + .flags = STRBUF_MALLOCD, + .buffer = (char *)str, + .used = size, + .capacity = size, + }; + + return s; +} + +void lwan_strbuf_free(struct lwan_strbuf *s) +{ + if (UNLIKELY(!s)) + return; + if (s->flags & BUFFER_MALLOCD) { + assert(!(s->flags & BUFFER_FIXED)); + free(s->buffer); + } + if (s->flags & STRBUF_MALLOCD) + free(s); +} + +bool lwan_strbuf_append_char(struct lwan_strbuf *s, const char c) +{ + size_t grow_size; + if (UNLIKELY(__builtin_add_overflow(s->used, 1, &grow_size))) + return false; + if (UNLIKELY(!grow_buffer_if_needed(s, grow_size))) + return false; + + s->buffer[s->used++] = c; + s->buffer[s->used] = '\0'; + + return true; +} + +bool lwan_strbuf_append_str(struct lwan_strbuf *s1, const char *s2, size_t sz) +{ + size_t grow_size; + if (UNLIKELY(__builtin_add_overflow(s1->used, sz, &grow_size))) + return false; + if (UNLIKELY(!grow_buffer_if_needed(s1, grow_size))) + return false; + + memcpy(s1->buffer + s1->used, s2, sz); + s1->used += sz; + s1->buffer[s1->used] = '\0'; + + return true; +} + +bool lwan_strbuf_set_static(struct lwan_strbuf *s1, const char *s2, size_t sz) +{ + if (s1->flags & BUFFER_MALLOCD) + free(s1->buffer); + + s1->buffer = (char *)s2; + s1->used = s1->capacity = sz; + s1->flags &= ~(BUFFER_MALLOCD | BUFFER_FIXED); + + return true; +} + +bool lwan_strbuf_set(struct lwan_strbuf *s1, const char *s2, size_t sz) +{ + if (UNLIKELY(!grow_buffer_if_needed(s1, sz))) + return false; + + memcpy(s1->buffer, s2, sz); + s1->used = sz; + s1->buffer[sz] = '\0'; + + return true; +} + +static ALWAYS_INLINE bool +internal_printf(struct lwan_strbuf *s1, + bool (*save_str)(struct lwan_strbuf *, const char *, size_t), + const char *fmt, + va_list values) +{ + char *s2; + int len; + + if (UNLIKELY((len = vasprintf(&s2, fmt, values)) < 0)) + return false; + + bool success = save_str(s1, s2, (size_t)len); + free(s2); + + return success; +} + +bool lwan_strbuf_vprintf(struct lwan_strbuf *s, const char *fmt, va_list ap) +{ + return internal_printf(s, lwan_strbuf_set, fmt, ap); +} + +bool lwan_strbuf_printf(struct lwan_strbuf *s, const char *fmt, ...) +{ + bool could_printf; + va_list values; + + va_start(values, fmt); + could_printf = lwan_strbuf_vprintf(s, fmt, values); + va_end(values); + + return could_printf; +} + +bool lwan_strbuf_append_vprintf(struct lwan_strbuf *s, const char *fmt, va_list ap) +{ + return internal_printf(s, lwan_strbuf_append_str, fmt, ap); +} + +bool lwan_strbuf_append_printf(struct lwan_strbuf *s, const char *fmt, ...) +{ + bool could_printf; + va_list values; + + va_start(values, fmt); + could_printf = lwan_strbuf_append_vprintf(s, fmt, values); + va_end(values); + + return could_printf; +} + +bool lwan_strbuf_grow_to(struct lwan_strbuf *s, size_t new_size) +{ + return grow_buffer_if_needed(s, new_size); +} + +bool lwan_strbuf_grow_by(struct lwan_strbuf *s, size_t offset) +{ + size_t new_size; + + if (UNLIKELY(__builtin_add_overflow(offset, s->used, &new_size))) + return false; + + return lwan_strbuf_grow_to(s, new_size); +} + +void lwan_strbuf_reset(struct lwan_strbuf *s) +{ + if (s->flags & BUFFER_MALLOCD) { + s->buffer[0] = '\0'; + } else { + s->buffer = ""; + s->capacity = 0; + } + + s->used = 0; +} + +void lwan_strbuf_reset_trim(struct lwan_strbuf *s, size_t trim_thresh) +{ + if (s->flags & BUFFER_MALLOCD && s->capacity > trim_thresh) { + free(s->buffer); + s->flags &= ~BUFFER_MALLOCD; + } + + return lwan_strbuf_reset(s); +} + +/* This function is quite dangerous, so the prototype is only in lwan-private.h */ +char *lwan_strbuf_extend_unsafe(struct lwan_strbuf *s, size_t by) +{ + if (!lwan_strbuf_grow_by(s, by)) + return NULL; + + size_t prev_used = s->used; + s->used += by; + + return s->buffer + prev_used; +} + +bool lwan_strbuf_init_from_file(struct lwan_strbuf *s, const char *path) +{ + int fd = open(path, O_RDONLY | O_CLOEXEC); + struct stat st; + + if (UNLIKELY(fd < 0)) + return false; + + if (UNLIKELY(fstat(fd, &st) < 0)) + goto error_close; + + if (UNLIKELY(!lwan_strbuf_init_with_size(s, (size_t)st.st_size))) + goto error_close; + + s->used = (size_t)st.st_size; + + for (char *buffer = s->buffer; st.st_size; ) { + ssize_t n_read = read(fd, buffer, (size_t)st.st_size); + + if (UNLIKELY(n_read < 0)) { + if (errno == EINTR) + continue; + goto error; + } + + buffer += n_read; + *buffer = '\0'; + st.st_size -= (off_t)n_read; + } + + close(fd); + return true; + +error: + lwan_strbuf_free(s); +error_close: + close(fd); + return false; +} + +struct lwan_strbuf *lwan_strbuf_new_from_file(const char *path) +{ + struct lwan_strbuf *strbuf = malloc(sizeof(*strbuf)); + + if (!strbuf) + return NULL; + + if (lwan_strbuf_init_from_file(strbuf, path)) { + strbuf->flags |= STRBUF_MALLOCD; + return strbuf; + } + + free(strbuf); + return NULL; +} + +struct lwan_value lwan_strbuf_to_value(const struct lwan_strbuf *s) +{ + return (struct lwan_value){.value = lwan_strbuf_get_buffer(s), + .len = lwan_strbuf_get_length(s)}; +} + +struct iovec lwan_strbuf_to_iovec(const struct lwan_strbuf *s) +{ + return (struct iovec){.iov_base = lwan_strbuf_get_buffer(s), + .iov_len = lwan_strbuf_get_length(s)}; +} + +bool lwan_strbuf_append_value(struct lwan_strbuf *s1, + const struct lwan_value *s2) +{ + return lwan_strbuf_append_str(s1, s2->value, s2->len); +} diff --git a/src/lib/lwan-strbuf.h b/src/lib/lwan-strbuf.h new file mode 100644 index 000000000..84dd99007 --- /dev/null +++ b/src/lib/lwan-strbuf.h @@ -0,0 +1,111 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include +#include +#include +#include +#include + +struct lwan_strbuf { + char *buffer; + + /* `capacity` used to be derived from `used` by aligning it to the next + * power of two, but this resulted in re-allocations after this strbuf + * been reset between requests. It now always contains the capacity + * allocated by `buffer`; resetting essentially only resets `used` and + * writes `\0` to buffer[0]. */ + size_t capacity, used; + + unsigned int flags; +}; + +#define LWAN_STRBUF_STATIC_INIT \ + (struct lwan_strbuf) { .buffer = "" } + +bool lwan_strbuf_init_with_fixed_buffer(struct lwan_strbuf *buf, + void *buffer, + size_t size); +bool lwan_strbuf_init_with_size(struct lwan_strbuf *buf, size_t size); +bool lwan_strbuf_init(struct lwan_strbuf *buf); +struct lwan_strbuf *lwan_strbuf_new_static(const char *str, size_t size); +struct lwan_strbuf *lwan_strbuf_new_with_size(size_t size); +struct lwan_strbuf *lwan_strbuf_new_with_fixed_buffer(size_t size); +struct lwan_strbuf *lwan_strbuf_new(void); +void lwan_strbuf_free(struct lwan_strbuf *s); + +void lwan_strbuf_reset(struct lwan_strbuf *s); +void lwan_strbuf_reset_trim(struct lwan_strbuf *s, size_t trim_thresh); + +bool lwan_strbuf_append_char(struct lwan_strbuf *s, const char c); + +bool lwan_strbuf_append_str(struct lwan_strbuf *s1, const char *s2, size_t sz); +static inline bool lwan_strbuf_append_strz(struct lwan_strbuf *s1, + const char *s2) +{ + return lwan_strbuf_append_str(s1, s2, strlen(s2)); +} + +struct lwan_value; +bool lwan_strbuf_append_value(struct lwan_strbuf *s1, const struct lwan_value *s2); + +bool lwan_strbuf_set_static(struct lwan_strbuf *s1, const char *s2, size_t sz); +static inline bool lwan_strbuf_set_staticz(struct lwan_strbuf *s1, + const char *s2) +{ + return lwan_strbuf_set_static(s1, s2, strlen(s2)); +} + +bool lwan_strbuf_set(struct lwan_strbuf *s1, const char *s2, size_t sz); +static inline bool lwan_strbuf_setz(struct lwan_strbuf *s1, const char *s2) +{ + return lwan_strbuf_set(s1, s2, strlen(s2)); +} + +bool lwan_strbuf_append_printf(struct lwan_strbuf *s, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +bool lwan_strbuf_append_vprintf(struct lwan_strbuf *s, + const char *fmt, + va_list v); + +bool lwan_strbuf_printf(struct lwan_strbuf *s1, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +bool lwan_strbuf_vprintf(struct lwan_strbuf *s1, const char *fmt, va_list ap); + +bool lwan_strbuf_grow_to(struct lwan_strbuf *s, size_t new_size); +bool lwan_strbuf_grow_by(struct lwan_strbuf *s, size_t offset); + +static inline size_t lwan_strbuf_get_length(const struct lwan_strbuf *s) +{ + return s->used; +} + +static inline char *lwan_strbuf_get_buffer(const struct lwan_strbuf *s) +{ + return s->buffer; +} + +bool lwan_strbuf_init_from_file(struct lwan_strbuf *s, const char *path); +struct lwan_strbuf *lwan_strbuf_new_from_file(const char *path); + +struct iovec lwan_strbuf_to_iovec(const struct lwan_strbuf *s); +struct lwan_value lwan_strbuf_to_value(const struct lwan_strbuf *s); diff --git a/src/lib/lwan-tables.c b/src/lib/lwan-tables.c new file mode 100644 index 000000000..503735ec2 --- /dev/null +++ b/src/lib/lwan-tables.c @@ -0,0 +1,311 @@ +/* + * lwan - web server + * Copyright (c) 2012, 2013 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define _GNU_SOURCE +#include +#include +#include + +#if defined(LWAN_HAVE_BROTLI) +#include +#elif defined(LWAN_HAVE_ZSTD) +#include +#else +#include +#endif + +#include "lwan-private.h" + +#include "mime-types.h" + +static unsigned char uncompressed_mime_entries[MIME_UNCOMPRESSED_LEN]; +static char *mime_types[MIME_ENTRIES]; +static uint64_t *mime_extensions; +static bool mime_entries_initialized = false; + +void lwan_tables_shutdown(void) +{ +} + +void lwan_tables_init(void) +{ + if (mime_entries_initialized) + return; + + lwan_status_debug("Uncompressing MIME type table: %u->%u bytes, %d entries", + MIME_COMPRESSED_LEN, MIME_UNCOMPRESSED_LEN, MIME_ENTRIES); + +#if defined(LWAN_HAVE_BROTLI) + size_t uncompressed_length = MIME_UNCOMPRESSED_LEN; + BrotliDecoderResult ret; + + ret = BrotliDecoderDecompress(MIME_COMPRESSED_LEN, mime_entries_compressed, + &uncompressed_length, + uncompressed_mime_entries); + if (ret != BROTLI_DECODER_RESULT_SUCCESS) + lwan_status_critical("Error while uncompressing table with Brotli"); +#elif defined(LWAN_HAVE_ZSTD) + size_t uncompressed_length = + ZSTD_decompress(uncompressed_mime_entries, MIME_UNCOMPRESSED_LEN, + mime_entries_compressed, MIME_COMPRESSED_LEN); + if (ZSTD_isError(uncompressed_length)) + lwan_status_critical("Error while uncompressing table with Zstd"); +#else + uLongf uncompressed_length = MIME_UNCOMPRESSED_LEN; + int ret = + uncompress((Bytef *)uncompressed_mime_entries, &uncompressed_length, + (const Bytef *)mime_entries_compressed, MIME_COMPRESSED_LEN); + if (ret != Z_OK) { + lwan_status_critical("Error while uncompressing table: zlib error %d", + ret); + } +#endif + + if (uncompressed_length != MIME_UNCOMPRESSED_LEN) { + lwan_status_critical("Expected uncompressed length %d, got %ld", + MIME_UNCOMPRESSED_LEN, uncompressed_length); + } + + unsigned char *ptr = uncompressed_mime_entries + 8 * MIME_ENTRIES; + for (size_t i = 0; i < MIME_ENTRIES; i++) { + mime_types[i] = (char *)ptr; + ptr += strlen((const char *)ptr) + 1; + } + mime_extensions = (uint64_t *)uncompressed_mime_entries; + + mime_entries_initialized = true; + + assert(streq(lwan_determine_mime_type_for_file_name(".mkv"), + "video/x-matroska")); + assert(streq(lwan_determine_mime_type_for_file_name(".xml"), + "application/xml")); + assert(streq(lwan_determine_mime_type_for_file_name(".nosuchext"), + "application/octet-stream")); + assert(streq(lwan_determine_mime_type_for_file_name("nodotinfilename"), + "application/octet-stream")); + assert(streq(lwan_determine_mime_type_for_file_name(""), + "application/octet-stream")); + assert(streq(lwan_determine_mime_type_for_file_name(".gif"), "image/gif")); + assert(streq(lwan_determine_mime_type_for_file_name(".JS"), + "text/javascript")); + assert(streq(lwan_determine_mime_type_for_file_name(".BZ2"), + "application/x-bzip2")); + assert(streq(lwan_determine_mime_type_for_file_name(".z1"), + "application/x-zmachine")); + assert(streq(lwan_determine_mime_type_for_file_name(".asm"), + "text/x-asm")); +} + +LWAN_SELF_TEST(status_codes) +{ +#define ASSERT_STATUS(id, code, short, long) \ + do { \ + assert(!strncmp(lwan_http_status_as_string_with_code(HTTP_##id), \ + #code, 3)); \ + assert(!strcmp(lwan_http_status_as_string(HTTP_##id), short)); \ + assert( \ + !strcmp(lwan_http_status_as_descriptive_string(HTTP_##id), long)); \ + } while (0); + FOR_EACH_HTTP_STATUS(ASSERT_STATUS) +#undef ASSERT_STATUS +} + +static ALWAYS_INLINE const char *bsearch_mime_type(uint64_t ext) +{ + /* Based on https://orlp.net/blog/bitwise-binary-search/ */ + int64_t b = ext > mime_extensions[MIME_ENTRIES / 2] + ? MIME_ENTRIES - MIME_ENTRIES_FLOOR + : -1; + for (uint64_t bit = MIME_ENTRIES_FLOOR >> 1; bit != 0; bit >>= 1) { + if (ext > mime_extensions[b + (int64_t)bit]) + b += (int64_t)bit; + } + return mime_types[mime_extensions[b + 1] == ext ? b + 1 + : MIME_ENTRY_FALLBACK]; +} + +const char *lwan_determine_mime_type_for_file_name(const char *file_name) +{ + const char *last_dot = strrchr(file_name, '.') ?: MIME_EXT_FALLBACK; + uint64_t key = 0; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-truncation" + /* Data is stored with NULs on strings up to 7 chars, and + * no NULs for 8-char strings, because that's implicit. + * So truncation is intentional here: comparisons in + * bsearch_mime_type() always loads keys as uint64_ts. */ + strncpy((char *)&key, last_dot + 1, 8); +#pragma GCC diagnostic pop + + return bsearch_mime_type(htobe64(key & ~0x2020202020202020ull)); +} + +#include "lookup-http-status.h" /* genrated by statuslookupgen */ + +ALWAYS_INLINE const char * +lwan_http_status_as_string_with_code(const enum lwan_http_status status) +{ + return lwan_lookup_http_status_impl(status); +} + +const char * +lwan_http_status_as_string(const enum lwan_http_status status) +{ + return lwan_http_status_as_string_with_code(status) + 4; +} + +const char *lwan_http_status_as_descriptive_string(const enum lwan_http_status status) +{ + const char *str = lwan_lookup_http_status_impl(status); + return str + strlen(str) + 1; +} + +enum { + CHAR_PROP_SPACE = 1 << 0, + CHAR_PROP_HEX = 1 << 1, + CHAR_PROP_DIG = 1 << 2, + CHAR_PROP_ALPHA = 1 << 3, + CHAR_PROP_CGI_HEADER = 1 << 4, +}; + +static const uint8_t char_prop_tbl[256] = { + [' '] = CHAR_PROP_SPACE, + ['\t'] = CHAR_PROP_SPACE, + ['\n'] = CHAR_PROP_SPACE, + ['\r'] = CHAR_PROP_SPACE, + ['0'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['1'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['2'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['3'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['4'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['5'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['6'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['7'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['8'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['9'] = CHAR_PROP_HEX | CHAR_PROP_DIG | CHAR_PROP_CGI_HEADER, + ['a'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['b'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['c'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['d'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['e'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['f'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['g'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['h'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['i'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['j'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['k'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['l'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['m'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['n'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['o'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['p'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['q'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['r'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['s'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['t'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['u'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['v'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['w'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['x'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['y'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['z'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['A'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['B'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['C'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['D'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['E'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['F'] = CHAR_PROP_HEX | CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['G'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['H'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['I'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['J'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['K'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['L'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['M'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['N'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['O'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['P'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['Q'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['R'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['S'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['T'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['U'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['V'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['W'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['X'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['Y'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['Z'] = CHAR_PROP_ALPHA | CHAR_PROP_CGI_HEADER, + ['!'] = CHAR_PROP_CGI_HEADER, + ['#'] = CHAR_PROP_CGI_HEADER, + ['$'] = CHAR_PROP_CGI_HEADER, + ['%'] = CHAR_PROP_CGI_HEADER, + ['&'] = CHAR_PROP_CGI_HEADER, + ['\''] = CHAR_PROP_CGI_HEADER, + ['*'] = CHAR_PROP_CGI_HEADER, + ['+'] = CHAR_PROP_CGI_HEADER, + ['.'] = CHAR_PROP_CGI_HEADER, + ['^'] = CHAR_PROP_CGI_HEADER, + ['_'] = CHAR_PROP_CGI_HEADER, + ['`'] = CHAR_PROP_CGI_HEADER, + ['|'] = CHAR_PROP_CGI_HEADER, + ['~'] = CHAR_PROP_CGI_HEADER, +}; + +ALWAYS_INLINE uint8_t lwan_char_isspace(char ch) +{ + return char_prop_tbl[(unsigned char)ch] & CHAR_PROP_SPACE; +} + +ALWAYS_INLINE uint8_t lwan_char_iscgiheader(char ch) +{ + return char_prop_tbl[(unsigned char)ch] & CHAR_PROP_CGI_HEADER; +} + +ALWAYS_INLINE uint8_t lwan_char_isxdigit(char ch) +{ + return char_prop_tbl[(unsigned char)ch] & CHAR_PROP_HEX; +} + +ALWAYS_INLINE uint8_t lwan_char_isdigit(char ch) +{ + return char_prop_tbl[(unsigned char)ch] & CHAR_PROP_DIG; +} + +ALWAYS_INLINE uint8_t lwan_char_isalpha(char ch) +{ + return char_prop_tbl[(unsigned char)ch] & CHAR_PROP_ALPHA; +} + +ALWAYS_INLINE uint8_t lwan_char_isalnum(char ch) +{ + return char_prop_tbl[(unsigned char)ch] & (CHAR_PROP_ALPHA | CHAR_PROP_DIG); +} + +#include +LWAN_SELF_TEST(compare_with_ctype) +{ + for (int i = 0; i < 256; i++) { + assert(!!isxdigit((char)i) == !!lwan_char_isxdigit((char)i)); + assert(!!isdigit((char)i) == !!lwan_char_isdigit((char)i)); + assert(!!isalpha((char)i) == !!lwan_char_isalpha((char)i)); + assert(!!isalnum((char)i) == !!lwan_char_isalnum((char)i)); + assert(!!memchr(" \t\n\r", i, 4) == !!lwan_char_isspace((char)i)); + } +} diff --git a/src/lib/lwan-template.c b/src/lib/lwan-template.c new file mode 100644 index 000000000..a55a5d6cc --- /dev/null +++ b/src/lib/lwan-template.c @@ -0,0 +1,1788 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ +/* + * Ideas from Mustache logic-less templates: http://mustache.github.com/ + * Lexer+parser implemented using ideas from Rob Pike's talk "Lexical Scanning + * in Go" (https://www.youtube.com/watch?v=HxaD_trXwRE). + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "hash.h" +#include "int-to-str.h" +#include "list.h" +#include "ringbuffer.h" +#include "lwan-array.h" +#include "lwan-strbuf.h" +#include "lwan-template.h" + +/* Define this and build a debug version to have the template + * chunks printed out after compilation. */ +#undef TEMPLATE_DEBUG + +#define LEXEME_MAX_LEN 64 + +enum action { + ACTION_APPEND, + ACTION_APPEND_SMALL, + ACTION_VARIABLE, + ACTION_VARIABLE_STR, + ACTION_VARIABLE_STR_ESCAPE, + ACTION_START_ITER, + ACTION_END_ITER, + ACTION_IF_VARIABLE_NOT_EMPTY, + ACTION_END_IF_VARIABLE_NOT_EMPTY, + ACTION_APPLY_TPL, + ACTION_LAMBDA, + ACTION_LAST +}; + +enum flags { + FLAGS_ALL = -1, + FLAGS_NEGATE = 1 << 0, + FLAGS_QUOTE = 1 << 1, + FLAGS_NO_FREE = 1 << 2, + FLAGS_LAMBDA = 1 << 3, +}; + +#define FOR_EACH_LEXEME(X) \ + X(ERROR) X(EOF) X(IDENTIFIER) X(LEFT_META) X(HASH) X(RIGHT_META) X(TEXT) \ + X(SLASH) X(QUESTION_MARK) X(HAT) X(GREATER_THAN) X(OPEN_CURLY_BRACE) \ + X(CLOSE_CURLY_BRACE) + +#define GENERATE_ENUM(id) LEXEME_##id, +#define GENERATE_ARRAY_ITEM(id) [LEXEME_##id] = #id, + +enum lexeme_type { + FOR_EACH_LEXEME(GENERATE_ENUM) + TOTAL_LEXEMES +}; + +static const char *lexeme_type_str[TOTAL_LEXEMES] = { + FOR_EACH_LEXEME(GENERATE_ARRAY_ITEM) +}; + +#undef GENERATE_ENUM +#undef GENERATE_ARRAY_ITEM + +struct chunk { + union { + /* `instruction` is filled when baking computed goto address + * from `action`. This needs to be "unbaked" when freeing + * a template. */ + const void *instruction; + enum action action; + }; + void *data; + enum flags flags; +}; + +DEFINE_ARRAY_TYPE(chunk_array, struct chunk) + +struct lwan_tpl { + struct chunk_array chunks; + size_t minimum_size; + const void *const *dispatch_table; +}; + +struct symtab { + struct hash *hash; + struct symtab *next; +}; + +struct lexeme { + enum lexeme_type type; + struct { + const char *value; + size_t len; + } value; +}; + +DEFINE_RING_BUFFER_TYPE(lexeme_ring_buffer, struct lexeme, 4) + +struct lexer { + void *(*state)(struct lexer *); + const char *start, *pos, *end; + + struct lexeme_ring_buffer ring_buffer; +}; + +struct parser { + struct lwan_tpl *tpl; + const struct lwan_var_descriptor *descriptor; + struct symtab *symtab; + struct lexer lexer; + enum flags flags; + struct list_head stack; + struct chunk_array chunks; + enum lwan_tpl_flag template_flags; +}; + +struct stacked_lexeme { + struct list_node stack; + struct lexeme lexeme; +}; + +struct chunk_descriptor { + struct chunk *chunk; + struct lwan_var_descriptor *descriptor; +}; + +static const char left_meta[] = "{{"; +static const char right_meta[] = "}}"; +static_assert(sizeof(left_meta) == sizeof(right_meta), + "right_meta and left_meta are the same length"); + +static void *lex_inside_action(struct lexer *lexer); +static void *lex_identifier(struct lexer *lexer); +static void *lex_left_meta(struct lexer *lexer); +static void *lex_right_meta(struct lexer *lexer); +static void *lex_text(struct lexer *lexer); + +static void *parser_end_iter(struct parser *parser, struct lexeme *lexeme); +static void *parser_end_var_not_empty(struct parser *parser, + struct lexeme *lexeme); +static void *parser_iter(struct parser *parser, struct lexeme *lexeme); +static void *parser_meta(struct parser *parser, struct lexeme *lexeme); +static void *parser_negate(struct parser *parser, struct lexeme *lexeme); +static void *parser_identifier(struct parser *parser, struct lexeme *lexeme); +static void *parser_slash(struct parser *parser, struct lexeme *lexeme); +static void *parser_text(struct parser *parser, struct lexeme *lexeme); + +static void error_vlexeme(struct lexeme *lexeme, const char *msg, va_list ap) + __attribute__((format(printf, 2, 0))); +static void *error_lexeme(struct lexeme *lexeme, const char *msg, ...) + __attribute__((format(printf, 2, 3))); +static void *lex_error(struct lexer *lexer, const char *msg, ...) + __attribute__((format(printf, 2, 3))); + +static struct lwan_var_descriptor *symtab_lookup(struct parser *parser, + const char *var_name) +{ + for (struct symtab *tab = parser->symtab; tab; tab = tab->next) { + struct lwan_var_descriptor *var = hash_find(tab->hash, var_name); + if (var) + return var; + } + + return NULL; +} + +static __attribute__((noinline)) struct lwan_var_descriptor * +symtab_lookup_lexeme(struct parser *parser, struct lexeme *lexeme) +{ + if (lexeme->value.len > LEXEME_MAX_LEN) { + lwan_status_error("Lexeme exceeds %d characters", LEXEME_MAX_LEN); + return NULL; + } + + return symtab_lookup(parser, + strndupa(lexeme->value.value, lexeme->value.len)); +} + +static int symtab_push(struct parser *parser, + const struct lwan_var_descriptor *descriptor) +{ + struct symtab *tab; + int r; + + if (!descriptor) + return -ENODEV; + + tab = malloc(sizeof(*tab)); + if (!tab) + return -errno; + + tab->hash = hash_str_new(NULL, NULL); + if (!tab->hash) { + r = -ENOMEM; + goto hash_new_err; + } + + tab->next = parser->symtab; + parser->symtab = tab; + + for (; descriptor->name; descriptor++) { + r = hash_add(parser->symtab->hash, descriptor->name, descriptor); + + if (r < 0) + goto hash_add_err; + } + + return 0; + +hash_add_err: + hash_unref(tab->hash); +hash_new_err: + free(tab); + + return r; +} + +static void symtab_pop(struct parser *parser) +{ + struct symtab *tab = parser->symtab; + + assert(tab); + + hash_unref(tab->hash); + parser->symtab = tab->next; + free(tab); +} + +static void emit_lexeme(struct lexer *lexer, struct lexeme *lexeme) +{ + lexeme_ring_buffer_put(&lexer->ring_buffer, lexeme); + lexer->start = lexer->pos; +} + +static void emit(struct lexer *lexer, enum lexeme_type lexeme_type) +{ + struct lexeme lexeme = { + .type = lexeme_type, + .value = { + .value = lexer->start, + .len = (size_t)(lexer->pos - lexer->start) + } + }; + emit_lexeme(lexer, &lexeme); +} + +static int next(struct lexer *lexer) +{ + if (lexer->pos >= lexer->end) + return EOF; + int r = *lexer->pos; + lexer->pos++; + return r; +} + +static size_t remaining(struct lexer *lexer) +{ + return (size_t)(lexer->end - lexer->pos); +} + +static bool lex_streq(struct lexer *lexer, const char *str, size_t s) +{ + if (remaining(lexer) < s) + return false; + + return !strncmp(lexer->pos, str, s); +} + +static void ignore(struct lexer *lexer) { lexer->start = lexer->pos; } + +static void backup(struct lexer *lexer) { lexer->pos--; } + +static void error_vlexeme(struct lexeme *lexeme, const char *msg, va_list ap) +{ + char *formatted; + size_t formatted_len; + int r; + + *lexeme = (struct lexeme){.type = LEXEME_ERROR}; + + r = vasprintf(&formatted, msg, ap); + if (r < 0) { + lexeme->value.value = strdup(strerror(errno)); + if (!lexeme->value.value) + return; + + formatted_len = strlen(lexeme->value.value); + } else { + formatted_len = (size_t)r; + } + + lwan_status_error("Error while parsing template: %.*s", (int)formatted_len, formatted); + free(formatted); +} + +static void *error_lexeme(struct lexeme *lexeme, const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + error_vlexeme(lexeme, msg, ap); + va_end(ap); + + return NULL; +} + +static void *lex_error(struct lexer *lexer, const char *msg, ...) +{ + struct lexeme lexeme; + va_list ap; + + va_start(ap, msg); + error_vlexeme(&lexeme, msg, ap); + va_end(ap); + + emit_lexeme(lexer, &lexeme); + return NULL; +} + +static bool is_ident(int ch) +{ + return isalnum(ch) || ch == '_' || ch == '.' || ch == '/'; +} + +static void *lex_identifier(struct lexer *lexer) +{ + while (is_ident(next(lexer))) + ; + backup(lexer); + emit(lexer, LEXEME_IDENTIFIER); + return lex_inside_action; +} + +static void *lex_partial(struct lexer *lexer) +{ + while (true) { + int r = next(lexer); + + if (r == EOF) + return lex_error(lexer, "unexpected EOF while scanning action"); + if (r == '\n') + return lex_error(lexer, "actions cannot span multiple lines"); + if (isspace(r)) { + ignore(lexer); + continue; + } + if (is_ident(r)) { + backup(lexer); + return lex_identifier; + } + return lex_error(lexer, "unexpected character: %c", r); + } +} + +static void *lex_quoted_identifier(struct lexer *lexer) +{ + int r; + + emit(lexer, LEXEME_OPEN_CURLY_BRACE); + lex_identifier(lexer); + + r = next(lexer); + if (r != '}') + return lex_error(lexer, "expecting `}', found `%c'", r); + + emit(lexer, LEXEME_CLOSE_CURLY_BRACE); + return lex_inside_action; +} + +static void *lex_comment(struct lexer *lexer) +{ + size_t brackets = strlen(left_meta); + + do { + int r = next(lexer); + if (r == '{') + brackets++; + else if (r == '}') + brackets--; + else if (r == EOF) + return lex_error(lexer, + "unexpected EOF while scanning comment end"); + } while (brackets); + + ignore(lexer); + return lex_text; +} + +static void *lex_inside_action(struct lexer *lexer) +{ + while (true) { + int r; + + if (lex_streq(lexer, right_meta, strlen(right_meta))) + return lex_right_meta; + + r = next(lexer); + switch (r) { + case EOF: + return lex_error(lexer, "unexpected EOF while scanning action"); + case '\n': + return lex_error(lexer, "actions cannot span multiple lines"); + case '#': + emit(lexer, LEXEME_HASH); + break; + case '?': + emit(lexer, LEXEME_QUESTION_MARK); + break; + case '^': + emit(lexer, LEXEME_HAT); + break; + case '>': + emit(lexer, LEXEME_GREATER_THAN); + return lex_partial; + case '{': + return lex_quoted_identifier; + case '/': + emit(lexer, LEXEME_SLASH); + break; + default: + if (isspace(r)) { + ignore(lexer); + continue; + } + if (is_ident(r)) { + backup(lexer); + return lex_identifier; + } + + return lex_error(lexer, "unexpected character: %c", r); + } + + return lex_inside_action; + } +} + +static void *lex_left_meta(struct lexer *lexer) +{ + lexer->pos += strlen(left_meta); + int r = next(lexer); + if (r == '!') + return lex_comment; + backup(lexer); + + emit(lexer, LEXEME_LEFT_META); + return lex_inside_action; +} + +static void *lex_right_meta(struct lexer *lexer) +{ + lexer->pos += strlen(right_meta); + emit(lexer, LEXEME_RIGHT_META); + return lex_text; +} + +static void *lex_text(struct lexer *lexer) +{ + do { + if (lex_streq(lexer, left_meta, strlen(left_meta))) { + if (lexer->pos > lexer->start) + emit(lexer, LEXEME_TEXT); + return lex_left_meta; + } + + if (lex_streq(lexer, right_meta, strlen(right_meta))) + return lex_error(lexer, "unexpected action close sequence"); + } while (next(lexer) != EOF); + if (lexer->pos > lexer->start) + emit(lexer, LEXEME_TEXT); + emit(lexer, LEXEME_EOF); + return NULL; +} + +static struct lexeme *lex_next_fsm_loop(struct lexer *lexer) +{ + struct lexeme *lexeme; + + while (lexer->state) { + if ((lexeme = lexeme_ring_buffer_get_ptr_or_null(&lexer->ring_buffer))) + return lexeme; + + lexer->state = lexer->state(lexer); + } + + return lexeme_ring_buffer_get_ptr_or_null(&lexer->ring_buffer); +} + +static struct lexeme *lex_next(struct lexer *lexer) +{ + struct lexeme *lexeme = lex_next_fsm_loop(lexer); + + if (lexeme && lexeme->type == LEXEME_ERROR) + return NULL; + + return lexeme; +} + +static void lex_init(struct lexer *lexer, struct lwan_value input) +{ + lexer->state = lex_text; + lexer->pos = lexer->start = input.value; + lexer->end = input.value + input.len; + lexeme_ring_buffer_init(&lexer->ring_buffer); +} + +static void *unexpected_lexeme(struct lexeme *lexeme) +{ + if (lexeme->type == LEXEME_ERROR) + return NULL; + + return error_lexeme(lexeme, "unexpected lexeme: %s [%.*s]", + lexeme_type_str[lexeme->type], (int)lexeme->value.len, + lexeme->value.value); +} + +static void parser_push_lexeme(struct parser *parser, struct lexeme *lexeme) +{ + struct stacked_lexeme *stacked_lexeme = malloc(sizeof(*stacked_lexeme)); + if (!stacked_lexeme) + lwan_status_critical_perror("Could not push parser lexeme"); + + stacked_lexeme->lexeme = *lexeme; + list_add(&parser->stack, &stacked_lexeme->stack); +} + +static void emit_chunk(struct parser *parser, + enum action action, + enum flags flags, + void *data) +{ + struct chunk *chunk; + + chunk = chunk_array_append(&parser->chunks); + if (!chunk) + lwan_status_critical_perror("Could not emit template chunk"); + + chunk->action = action; + chunk->flags = flags; + chunk->data = data; +} + +static bool parser_stack_top_matches(struct parser *parser, + struct lexeme *lexeme, + enum lexeme_type type) +{ + if (list_empty(&parser->stack)) { + error_lexeme(lexeme, "unexpected {{/%.*s}}", (int)lexeme->value.len, + lexeme->value.value); + return false; + } + + struct stacked_lexeme *stacked_lexeme = + (struct stacked_lexeme *)parser->stack.n.next; + bool matches = (stacked_lexeme->lexeme.type == type && + lexeme->value.len == stacked_lexeme->lexeme.value.len && + !memcmp(stacked_lexeme->lexeme.value.value, + lexeme->value.value, lexeme->value.len)); + if (matches) { + list_del(&stacked_lexeme->stack); + free(stacked_lexeme); + return true; + } + + error_lexeme(lexeme, "expecting %s `%.*s' but found `%.*s'", + lexeme_type_str[stacked_lexeme->lexeme.type], + (int)stacked_lexeme->lexeme.value.len, + stacked_lexeme->lexeme.value.value, (int)lexeme->value.len, + lexeme->value.value); + return false; +} + +static void *parser_right_meta(struct parser *parser __attribute__((unused)), + struct lexeme *lexeme) +{ + if (lexeme->type != LEXEME_RIGHT_META) + return unexpected_lexeme(lexeme); + return parser_text; +} + +static void *parser_end_iter(struct parser *parser, struct lexeme *lexeme) +{ + struct chunk *iter; + struct lwan_var_descriptor *symbol; + + if (!parser_stack_top_matches(parser, lexeme, LEXEME_IDENTIFIER)) + return NULL; + + symbol = symtab_lookup_lexeme(parser, lexeme); + if (!symbol) { + return error_lexeme(lexeme, "Unknown variable: %.*s", + (int)lexeme->value.len, lexeme->value.value); + } + + LWAN_ARRAY_FOREACH_REVERSE(&parser->chunks, iter) { + if (iter->action != ACTION_START_ITER) + continue; + if (iter->data == symbol) { + size_t index = chunk_array_get_elem_index(&parser->chunks, iter); + + emit_chunk(parser, ACTION_END_ITER, 0, (void *)index); + symtab_pop(parser); + + return parser_text; + } + } + + return error_lexeme(lexeme, "Could not find {{#%.*s}}", + (int)lexeme->value.len, lexeme->value.value); +} + +static void *parser_end_var_not_empty(struct parser *parser, + struct lexeme *lexeme) +{ + struct chunk *iter; + struct lwan_var_descriptor *symbol; + + if (!parser_stack_top_matches(parser, lexeme, LEXEME_IDENTIFIER)) + return NULL; + + symbol = symtab_lookup_lexeme(parser, lexeme); + if (!symbol) { + return error_lexeme(lexeme, "Unknown variable: %.*s", + (int)lexeme->value.len, lexeme->value.value); + } + + if (!parser->chunks.base.elements) + return error_lexeme( + lexeme, + "No chunks were emitted but parsing end variable not empty"); + + LWAN_ARRAY_FOREACH_REVERSE(&parser->chunks, iter) { + if (iter->action != ACTION_IF_VARIABLE_NOT_EMPTY) + continue; + if (iter->data == symbol) { + emit_chunk(parser, ACTION_END_IF_VARIABLE_NOT_EMPTY, 0, symbol); + return parser_right_meta; + } + } + + return error_lexeme(lexeme, "Could not find {{%.*s?}}", + (int)lexeme->value.len, lexeme->value.value); +} + +static void *parser_slash(struct parser *parser, struct lexeme *lexeme) +{ + if (lexeme->type == LEXEME_IDENTIFIER) { + struct lexeme *next; + + if ((next = lex_next(&parser->lexer))) { + if (next->type == LEXEME_RIGHT_META) + return parser_end_iter(parser, lexeme); + + if (next->type == LEXEME_QUESTION_MARK) + return parser_end_var_not_empty(parser, lexeme); + + return unexpected_lexeme(next); + } + } + + return unexpected_lexeme(lexeme); +} + +static void *parser_iter(struct parser *parser, struct lexeme *lexeme) +{ + if (lexeme->type == LEXEME_IDENTIFIER) { + enum flags negate = parser->flags & FLAGS_NEGATE; + struct lwan_var_descriptor *symbol = + symtab_lookup_lexeme(parser, lexeme); + if (!symbol) { + return error_lexeme(lexeme, "Unknown variable: %.*s", + (int)lexeme->value.len, lexeme->value.value); + } + + int r = symtab_push(parser, symbol->list_desc); + if (r < 0) { + if (r == -ENODEV) { + return error_lexeme( + lexeme, "Couldn't find descriptor for variable `%.*s'", + (int)lexeme->value.len, lexeme->value.value); + } + return error_lexeme(lexeme, + "Could not push symbol table (out of memory)"); + } + + emit_chunk(parser, ACTION_START_ITER, negate | FLAGS_NO_FREE, symbol); + + parser_push_lexeme(parser, lexeme); + parser->flags &= ~FLAGS_NEGATE; + return parser_right_meta; + } + + return unexpected_lexeme(lexeme); +} + +static void *parser_negate(struct parser *parser, struct lexeme *lexeme) +{ + switch (lexeme->type) { + default: + return unexpected_lexeme(lexeme); + + case LEXEME_HASH: + parser->flags ^= FLAGS_NEGATE; + return parser_iter; + + case LEXEME_IDENTIFIER: + parser->flags ^= FLAGS_NEGATE; + return parser_identifier(parser, lexeme); + } +} + +static void *parser_identifier(struct parser *parser, struct lexeme *lexeme) +{ + struct lexeme *next; + + if (!(next = lex_next(&parser->lexer))) + return NULL; + + if (parser->flags & FLAGS_QUOTE) { + if (next->type != LEXEME_CLOSE_CURLY_BRACE) + return error_lexeme(lexeme, "Expecting closing brace"); + if (!(next = lex_next(&parser->lexer))) + return unexpected_lexeme(lexeme); + } + + if (next->type == LEXEME_RIGHT_META) { + struct lwan_var_descriptor *symbol = + symtab_lookup_lexeme(parser, lexeme); + if (!symbol) { + return error_lexeme(lexeme, "Unknown variable: %.*s", + (int)lexeme->value.len, lexeme->value.value); + } + + if (symbol->offset == 0x1aabdacb) { + emit_chunk(parser, ACTION_LAMBDA, parser->flags, symbol); + assert(!symbol->get_is_empty); + assert(!symbol->list_desc); + } else { + emit_chunk(parser, ACTION_VARIABLE, parser->flags, symbol); + } + + parser->flags &= ~FLAGS_QUOTE; + parser->tpl->minimum_size += lexeme->value.len + 1; + + return parser_text; + } + + if (next->type == LEXEME_QUESTION_MARK) { + struct lwan_var_descriptor *symbol = + symtab_lookup_lexeme(parser, lexeme); + if (!symbol) { + return error_lexeme(lexeme, "Unknown variable: %.*s", + (int)lexeme->value.len, lexeme->value.value); + } + + enum flags flags = FLAGS_NO_FREE | (parser->flags & FLAGS_NEGATE); + + if (symbol->offset == 0x1aabdacb) + flags |= FLAGS_LAMBDA; + + emit_chunk(parser, ACTION_IF_VARIABLE_NOT_EMPTY, flags, symbol); + parser_push_lexeme(parser, lexeme); + + parser->flags &= ~FLAGS_NEGATE; + + return parser_right_meta; + } + + return unexpected_lexeme(next); +} + +static void *parser_partial(struct parser *parser, struct lexeme *lexeme) +{ + struct lwan_tpl *tpl; + char *filename = strndupa(lexeme->value.value, lexeme->value.len); + + if (lexeme->type != LEXEME_IDENTIFIER) + return unexpected_lexeme(lexeme); + + tpl = lwan_tpl_compile_file(filename, parser->descriptor); + if (tpl) { + emit_chunk(parser, ACTION_APPLY_TPL, 0, tpl); + return parser_right_meta; + } + + return error_lexeme(lexeme, "Could not compile template ``%s''", filename); +} + +static void *parser_meta(struct parser *parser, struct lexeme *lexeme) +{ + switch (lexeme->type) { + default: + return unexpected_lexeme(lexeme); + + case LEXEME_OPEN_CURLY_BRACE: + if (parser->flags & FLAGS_QUOTE) + return unexpected_lexeme(lexeme); + + parser->flags |= FLAGS_QUOTE; + return parser_meta; + + case LEXEME_IDENTIFIER: + return parser_identifier(parser, lexeme); + + case LEXEME_GREATER_THAN: + return parser_partial; + + case LEXEME_HASH: + return parser_iter; + + case LEXEME_HAT: + return parser_negate; + + case LEXEME_SLASH: + return parser_slash; + } +} + +static struct lwan_strbuf *lwan_strbuf_from_lexeme(struct parser *parser, + struct lexeme *lexeme) +{ + if (parser->template_flags & LWAN_TPL_FLAG_CONST_TEMPLATE) + return lwan_strbuf_new_static(lexeme->value.value, lexeme->value.len); + + struct lwan_strbuf *buf = lwan_strbuf_new_with_size(lexeme->value.len); + if (buf) + lwan_strbuf_set(buf, lexeme->value.value, lexeme->value.len); + + return buf; +} + +static void *parser_text(struct parser *parser, struct lexeme *lexeme) +{ + if (lexeme->type == LEXEME_LEFT_META) + return parser_meta; + + if (lexeme->type == LEXEME_TEXT) { + if (lexeme->value.len > 4 * sizeof(void *)) { + struct lwan_strbuf *buf = lwan_strbuf_from_lexeme(parser, lexeme); + if (!buf) + return error_lexeme(lexeme, "Out of memory"); + + emit_chunk(parser, ACTION_APPEND, 0, buf); + } else { + struct lexeme copy = *lexeme; + + while (copy.value.len) { + const size_t len = LWAN_MIN(sizeof(void *), copy.value.len); + uintptr_t tmp = 0; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-truncation" + /* strnlen(..., sizeof(void*)) is used for APPEND_SMALL, + * so it's fine if no NUL is generated for strings with + * sizeof(void *) characters. */ + strncpy((char *)&tmp, copy.value.value, len); +#pragma GCC diagnostic pop + + emit_chunk(parser, ACTION_APPEND_SMALL, 0, (void *)tmp); + + copy.value.len -= len; + copy.value.value += len; + } + } + parser->tpl->minimum_size += lexeme->value.len; + return parser_text; + } + + if (lexeme->type == LEXEME_EOF) { + emit_chunk(parser, ACTION_LAST, 0, NULL); + return NULL; + } + + return unexpected_lexeme(lexeme); +} + +void lwan_append_int_to_strbuf(struct lwan_strbuf *buf, void *ptr) +{ + char convertbuf[INT_TO_STR_BUFFER_SIZE]; + size_t len; + char *converted; + + converted = int_to_string(*(int *)ptr, convertbuf, &len); + lwan_strbuf_append_str(buf, converted, len); +} + +bool lwan_tpl_int_is_empty(void *ptr) { return (*(int *)ptr) == 0; } + +void lwan_append_double_to_strbuf(struct lwan_strbuf *buf, void *ptr) +{ + lwan_strbuf_append_printf(buf, "%f", *(double *)ptr); +} + +bool lwan_tpl_double_is_empty(void *ptr) +{ +#if defined(LWAN_HAVE_BUILTIN_FPCLASSIFY) + return __builtin_fpclassify(FP_NAN, FP_INFINITE, FP_NORMAL, FP_SUBNORMAL, + FP_ZERO, *(double *)ptr); +#else + return fpclassify(*(double *)ptr) == FP_ZERO; +#endif +} + +void lwan_append_str_to_strbuf(struct lwan_strbuf *buf, void *ptr) +{ + const char *str = *(char **)ptr; + + if (LIKELY(str)) + lwan_strbuf_append_strz(buf, str); +} + +#if __x86_64__ +#include +#endif + +static ALWAYS_INLINE int escaped_index(char ch) +{ +#if __x86_64__ + /* FIXME: instead of calling escaped_index() for each byte that needs to be + * escaped, use SIMD to leap through input string until an escapable character + * is found. */ + const __m128i ch_mask = _mm_set1_epi8(ch); + const __m128i escapable = + _mm_set_epi8(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '<', '>', '&', '"', '\'', '/'); + + return __builtin_ffs(_mm_movemask_epi8(_mm_cmpeq_epi8(ch_mask, escapable))); +#else + switch (ch) { + default: + return 0; + case '/': + return 1; + case '\'': + return 2; + case '"': + return 3; + case '&': + return 4; + case '>': + return 5; + case '<': + return 6; + } +#endif +} + +void lwan_append_str_escaped_to_strbuf(struct lwan_strbuf *buf, void *ptr) +{ + static const struct lwan_value escaped[] = { + {}, + { /* / */ "/", 6 }, + { /* ' */ "'", 6 }, + { /* " */ """, 6 }, + { /* & */ "&", 5 }, + { /* > */ ">", 4 }, + { /* < */ "<", 4 }, + }; + + if (UNLIKELY(!ptr)) + return; + + const char *str = *(char **)ptr; + if (UNLIKELY(!str)) + return; + + const char *last, *p; + for (last = p = str; *p; p++) { + int index = escaped_index(*p); + + if (index) { + lwan_strbuf_append_str(buf, last, (size_t)(p - last)); + last = p + 1; + + lwan_strbuf_append_str(buf, escaped[index].value, escaped[index].len); + } + } + + if (last != p) + lwan_strbuf_append_str(buf, last, (size_t)(p - last)); +} + +bool lwan_tpl_str_is_empty(void *ptr) +{ + if (UNLIKELY(!ptr)) + return true; + + const char *str = *(const char **)ptr; + return !str || *str == '\0'; +} + +static void +unbake_direct_addresses(struct lwan_tpl *tpl) +{ + struct chunk *iter; + + if (!tpl->dispatch_table) + return; + + LWAN_ARRAY_FOREACH (&tpl->chunks, iter) { + for (enum action action = ACTION_APPEND; action <= ACTION_LAST; action++) { + if (iter->instruction != tpl->dispatch_table[action]) + continue; + + iter->action = action; + if (action == ACTION_APPLY_TPL) + unbake_direct_addresses(iter->data); + + break; + } + } +} + +static void free_chunk(struct chunk *chunk) +{ + if (!chunk) + return; + if (chunk->flags & FLAGS_NO_FREE) + return; + + switch (chunk->action) { + case ACTION_LAMBDA: + case ACTION_LAST: + case ACTION_APPEND_SMALL: + case ACTION_VARIABLE: + case ACTION_VARIABLE_STR: + case ACTION_VARIABLE_STR_ESCAPE: + case ACTION_END_IF_VARIABLE_NOT_EMPTY: + case ACTION_END_ITER: + /* do nothing */ + break; + case ACTION_IF_VARIABLE_NOT_EMPTY: + case ACTION_START_ITER: + free(chunk->data); + break; + case ACTION_APPEND: + lwan_strbuf_free(chunk->data); + break; + case ACTION_APPLY_TPL: + lwan_tpl_free(chunk->data); + break; + } +} + +static void free_chunk_array(struct chunk_array *array) +{ + struct chunk *iter; + + LWAN_ARRAY_FOREACH(array, iter) + free_chunk(iter); + chunk_array_reset(array); +} + +void lwan_tpl_free(struct lwan_tpl *tpl) +{ + if (tpl) { + unbake_direct_addresses(tpl); + free_chunk_array(&tpl->chunks); + free(tpl); + } +} + +static bool post_process_template(struct parser *parser) +{ + struct chunk *last_chunk = + chunk_array_get_elem(&parser->chunks, chunk_array_len(&parser->chunks)); + struct chunk *prev_chunk; + struct chunk *chunk; + + LWAN_ARRAY_FOREACH (&parser->chunks, chunk) { + if (chunk->action == ACTION_IF_VARIABLE_NOT_EMPTY) { + for (prev_chunk = chunk;; chunk++) { + if (chunk == last_chunk) + goto error; + if (chunk->action == ACTION_LAST) { + lwan_status_error("Internal error: Could not find the end " + "var not empty chunk"); + return false; + } + if (chunk->action == ACTION_END_IF_VARIABLE_NOT_EMPTY && + chunk->data == prev_chunk->data) + break; + } + + assert(prev_chunk->flags & FLAGS_NO_FREE); + + struct chunk_descriptor *cd = malloc(sizeof(*cd)); + if (!cd) + lwan_status_critical_perror("malloc"); + + cd->descriptor = prev_chunk->data; + cd->chunk = chunk; + prev_chunk->data = cd; + prev_chunk->flags &= ~FLAGS_NO_FREE; + + chunk = prev_chunk + 1; + } else if (chunk->action == ACTION_START_ITER) { + enum flags flags = chunk->flags; + + for (prev_chunk = chunk;; chunk++) { + if (chunk == last_chunk) + goto error; + if (chunk->action == ACTION_LAST) { + lwan_status_error( + "Internal error: Could not find the end iter chunk"); + return false; + } + if (chunk->action == ACTION_END_ITER) { + size_t start_index = (size_t)chunk->data; + size_t prev_index = + chunk_array_get_elem_index(&parser->chunks, prev_chunk); + + if (prev_index == start_index) { + chunk->flags |= flags; + chunk->data = + chunk_array_get_elem(&parser->chunks, start_index); + break; + } + } + } + + assert(prev_chunk->flags & FLAGS_NO_FREE); + + struct chunk_descriptor *cd = malloc(sizeof(*cd)); + if (!cd) + lwan_status_critical_perror("malloc"); + + cd->descriptor = prev_chunk->data; + prev_chunk->data = cd; + prev_chunk->flags &= ~FLAGS_NO_FREE; + + if (chunk->action == ACTION_LAST) + cd->chunk = chunk; + else + cd->chunk = chunk + 1; + + chunk = prev_chunk + 1; + } else if (chunk->action == ACTION_VARIABLE) { + struct lwan_var_descriptor *descriptor = chunk->data; + bool escape = chunk->flags & FLAGS_QUOTE; + + if (descriptor->append_to_strbuf == lwan_append_str_to_strbuf) { + if (escape) + chunk->action = ACTION_VARIABLE_STR_ESCAPE; + else + chunk->action = ACTION_VARIABLE_STR; + chunk->data = (void *)(uintptr_t)descriptor->offset; + } else if (escape) { + lwan_status_error("Variable must be string to be escaped"); + return false; + } else if (!descriptor->append_to_strbuf) { + lwan_status_error("Invalid variable descriptor"); + return false; + } + } else if (chunk->action == ACTION_LAST) { + break; + } + } + + parser->tpl->chunks = parser->chunks; + + return true; + +error: + lwan_status_error("Unknown error while parsing template; bug?"); + return false; +} + +static bool parser_init(struct parser *parser, + const struct lwan_var_descriptor *descriptor, + struct lwan_value value) +{ + if (symtab_push(parser, descriptor) < 0) + return false; + + chunk_array_init(&parser->chunks); + parser->tpl->chunks = parser->chunks; + + lex_init(&parser->lexer, value); + list_head_init(&parser->stack); + + return true; +} + +static bool parser_shutdown(struct parser *parser, struct lexeme *lexeme) +{ + bool success = true; + + if (lexeme && lexeme->type == LEXEME_ERROR && lexeme->value.value) { + lwan_status_error("Parser error: %.*s", (int)lexeme->value.len, + lexeme->value.value); + free((char *)lexeme->value.value); + + success = false; + } + + if (!list_empty(&parser->stack)) { + struct stacked_lexeme *stacked, *stacked_next; + + list_for_each_safe (&parser->stack, stacked, stacked_next, stack) { + lwan_status_error( + "Parser error: EOF while looking for matching {{/%.*s}}", + (int)stacked->lexeme.value.len, stacked->lexeme.value.value); + list_del(&stacked->stack); + free(stacked); + } + + success = false; + } + + if (!parser->symtab) { + lwan_status_error( + "Parser error: No symbol table was found when finishing the parser"); + success = false; + } else { + symtab_pop(parser); + if (parser->symtab) { + lwan_status_error( + "Parser error: Symbol table not empty when finishing parser"); + + while (parser->symtab) + symtab_pop(parser); + + success = false; + } + } + + if (parser->flags & FLAGS_NEGATE) { + lwan_status_error("Parser error: unmatched negation"); + success = false; + } + if (parser->flags & FLAGS_QUOTE) { + lwan_status_error("Parser error: unmatched quote"); + success = false; + } + + success = success && post_process_template(parser); + + if (!success) + free_chunk_array(&parser->chunks); + + return success; +} + +static bool parse_value(struct lwan_tpl *tpl, + struct lwan_value value, + const struct lwan_var_descriptor *descriptor, + enum lwan_tpl_flag flags) +{ + struct parser parser = { + .tpl = tpl, + .symtab = NULL, + .descriptor = descriptor, + .template_flags = flags + }; + void *(*state)(struct parser *parser, struct lexeme *lexeme) = parser_text; + struct lexeme *lexeme; + + if (!parser_init(&parser, descriptor, value)) + return false; + + while (state) { + if (!(lexeme = lex_next(&parser.lexer))) + break; + + state = state(&parser, lexeme); + } + + return parser_shutdown(&parser, lexeme); +} + +#if !defined(NDEBUG) && defined(TEMPLATE_DEBUG) +static const char *instr(const char *name, char buf[static 32]) +{ + int ret = snprintf(buf, 32, "\033[33m%s\033[0m", name); + + if (ret < 0 || ret >= 32) + return "?"; + + return buf; +} + +static void dump_program(const struct lwan_tpl *tpl) +{ + struct chunk *iter; + int indent = 0; + + if (!tpl->chunks.base.elements) + return; + + LWAN_ARRAY_FOREACH(&tpl->chunks, iter) { + char instr_buf[32]; + + printf("%8zu ", iter - (struct chunk *)tpl->chunks.base.base); + + switch (iter->action) { + default: + for (int i = 0; i < indent; i++) { + printf(" "); + } + break; + case ACTION_END_ITER: + case ACTION_END_IF_VARIABLE_NOT_EMPTY: + break; + } + + switch (iter->action) { + case ACTION_APPEND: + printf("%s [%.*s]", instr("APPEND", instr_buf), + (int)lwan_strbuf_get_length(iter->data), + lwan_strbuf_get_buffer(iter->data)); + break; + case ACTION_APPEND_SMALL: { + uintptr_t val = (uintptr_t)iter->data; + size_t len = strnlen((char *)&val, sizeof(val)); + + printf("%s (%zu) [%.*s]", instr("APPEND_SMALL", instr_buf), len, (int)len, (char *)&val); + break; + } + case ACTION_LAMBDA: { + struct lwan_var_descriptor *descriptor = iter->data; + + printf("%s %s()", instr("APPEND_LAMBDA", instr_buf), descriptor->name); + break; + } + case ACTION_VARIABLE: { + struct lwan_var_descriptor *descriptor = iter->data; + + printf("%s [%s]", instr("APPEND_VAR", instr_buf), descriptor->name); + break; + } + case ACTION_VARIABLE_STR: + printf("%s", instr("APPEND_VAR_STR", instr_buf)); + break; + case ACTION_VARIABLE_STR_ESCAPE: + printf("%s", instr("APPEND_VAR_STR_ESCAPE", instr_buf)); + break; + case ACTION_START_ITER: { + struct chunk_descriptor *descriptor = iter->data; + + printf("%s [%s]", instr("START_ITER", instr_buf), + descriptor->descriptor->name); + indent++; + break; + } + case ACTION_END_ITER: + printf("%s [%zu]", instr("END_ITER", instr_buf), + (size_t)iter->data); + indent--; + break; + case ACTION_IF_VARIABLE_NOT_EMPTY: { + struct chunk_descriptor *cd = iter->data; + + printf("%s [%s]", + instr((iter->flags & FLAGS_LAMBDA) ? "IF_LAMBDA_NOT_EMPTY" + : "IF_VAR_NOT_EMPTY", + instr_buf), + cd->descriptor->name); + + indent++; + break; + } + case ACTION_END_IF_VARIABLE_NOT_EMPTY: + printf("%s", instr("END_VAR_NOT_EMPTY", instr_buf)); + indent--; + break; + case ACTION_APPLY_TPL: + printf("%s", instr("APPLY_TEMPLATE", instr_buf)); + break; + case ACTION_LAST: + printf("%s", instr("LAST", instr_buf)); + } + + printf("\033[34m"); + if (iter->flags & FLAGS_NEGATE) + printf(" NEG"); + if (iter->flags & FLAGS_QUOTE) + printf(" QUOTE"); + if (iter->flags & FLAGS_NO_FREE) + printf(" NO_FREE"); + printf("\033[0m\n"); + } +} +#endif + +struct lwan_tpl * +lwan_tpl_compile_value_full(struct lwan_value value, + const struct lwan_var_descriptor *descriptor, + enum lwan_tpl_flag flags) +{ + struct lwan_tpl *tpl; + + tpl = calloc(1, sizeof(*tpl)); + if (tpl) { + if (parse_value(tpl, value, descriptor, flags)) { +#if !defined(NDEBUG) && defined(TEMPLATE_DEBUG) + dump_program(tpl); +#endif + + return tpl; + } + + lwan_tpl_free(tpl); + } + + return NULL; +} + +struct lwan_tpl * +lwan_tpl_compile_string(const char *string, + const struct lwan_var_descriptor *descriptor) +{ + return lwan_tpl_compile_string_full(string, descriptor, 0); +} + +struct lwan_tpl * +lwan_tpl_compile_file(const char *filename, + const struct lwan_var_descriptor *descriptor) +{ + int fd; + struct stat st; + char *mapped; + struct lwan_tpl *tpl = NULL; + + fd = open(filename, O_RDONLY | O_CLOEXEC); + if (fd < 0) + goto end; + + if (fstat(fd, &st) < 0) + goto close_file; + + mapped = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (mapped == MAP_FAILED) + goto close_file; + + tpl = lwan_tpl_compile_string(mapped, descriptor); + + if (munmap(mapped, (size_t)st.st_size) < 0) + lwan_status_perror("munmap"); + +close_file: + close(fd); +end: + return tpl; +} + +static void +bake_direct_addresses(struct lwan_tpl *tpl, + const void *const dispatch_table[static ACTION_LAST]) +{ + struct chunk *iter; + + LWAN_ARRAY_FOREACH (&tpl->chunks, iter) { + if (iter->action == ACTION_APPLY_TPL) + bake_direct_addresses(iter->data, dispatch_table); + + iter->instruction = dispatch_table[iter->action]; + } + + tpl->dispatch_table = dispatch_table; +} + + +static const struct chunk *apply(struct lwan_tpl *tpl, + const struct chunk *chunks, + struct lwan_strbuf *buf, + void *variables, + const void *data) +{ + coro_context switcher; + struct coro *coro = NULL; + const struct chunk *chunk = chunks; + + if (UNLIKELY(!chunk)) + return NULL; + + if (!tpl->dispatch_table) { + static const void *const dispatch_table[] = { + [ACTION_APPEND] = &&action_append, + [ACTION_APPEND_SMALL] = &&action_append_small, + [ACTION_VARIABLE] = &&action_variable, + [ACTION_VARIABLE_STR] = &&action_variable_str, + [ACTION_VARIABLE_STR_ESCAPE] = &&action_variable_str_escape, + [ACTION_IF_VARIABLE_NOT_EMPTY] = &&action_if_variable_not_empty, + [ACTION_END_IF_VARIABLE_NOT_EMPTY] = &&action_end_if_variable_not_empty, + [ACTION_APPLY_TPL] = &&action_apply_tpl, + [ACTION_START_ITER] = &&action_start_iter, + [ACTION_END_ITER] = &&action_end_iter, + [ACTION_LAMBDA] = &&action_lambda, + [ACTION_LAST] = &&finalize, + }; + + bake_direct_addresses(tpl, dispatch_table); + } + +#define RETURN_IF_NO_CHUNK(force_) \ + do { \ + if (force_ UNLIKELY(!chunk)) { \ + lwan_status_error("Chunk is NULL while dispatching"); \ + return NULL; \ + } \ + } while (false) + +#define DISPATCH_ACTION(force_check_) \ + do { \ + RETURN_IF_NO_CHUNK(force_check_); \ + goto *chunk->instruction; \ + } while (false) + +#define DISPATCH_NEXT_ACTION(force_check_) \ + do { \ + RETURN_IF_NO_CHUNK(force_check_); \ + \ + chunk++; \ + goto *chunk->instruction; \ + } while (false) + +#define DISPATCH_ACTION_FAST() DISPATCH_ACTION(0 &&) +#define DISPATCH_ACTION_CHECK() DISPATCH_ACTION(1 &&) +#define DISPATCH_NEXT_ACTION_FAST() DISPATCH_NEXT_ACTION(0 &&) +#define DISPATCH_NEXT_ACTION_CHECK() DISPATCH_NEXT_ACTION(1 &&) + + DISPATCH_ACTION_FAST(); + +action_append: + lwan_strbuf_append_str(buf, lwan_strbuf_get_buffer(chunk->data), + lwan_strbuf_get_length(chunk->data)); + DISPATCH_NEXT_ACTION_FAST(); + +action_append_small: { + uintptr_t val = (uintptr_t)chunk->data; + size_t len = strnlen((char *)&val, sizeof(val)); + + lwan_strbuf_append_str(buf, (char*)&val, len); + + DISPATCH_NEXT_ACTION_FAST(); + } + +action_variable: { + struct lwan_var_descriptor *descriptor = chunk->data; + descriptor->append_to_strbuf(buf, (char *)variables + descriptor->offset); + DISPATCH_NEXT_ACTION_FAST(); + } + +action_lambda: { + struct lwan_var_descriptor *descriptor = chunk->data; + descriptor->lambda(buf, variables); + DISPATCH_NEXT_ACTION_FAST(); + } + +action_variable_str: + lwan_append_str_to_strbuf(buf, (char *)variables + (uintptr_t)chunk->data); + DISPATCH_NEXT_ACTION_FAST(); + +action_variable_str_escape: + lwan_append_str_escaped_to_strbuf(buf, (char *)variables + + (uintptr_t)chunk->data); + DISPATCH_NEXT_ACTION_FAST(); + +action_if_variable_not_empty: { + struct chunk_descriptor *cd = chunk->data; + bool empty; + + if (chunk->flags & FLAGS_LAMBDA) { + struct lwan_strbuf temp_buf; + char trash[1]; + + lwan_strbuf_init_with_fixed_buffer(&temp_buf, trash, 0); + + cd->descriptor->lambda(&temp_buf, variables); + empty = lwan_strbuf_has_grow_buffer_failed_flag(&temp_buf); + + lwan_strbuf_free(&temp_buf); + } else { + empty = cd->descriptor->get_is_empty((char *)variables + + cd->descriptor->offset); + } + + if (chunk->flags & FLAGS_NEGATE) + empty = !empty; + if (empty) { + chunk = cd->chunk; + DISPATCH_NEXT_ACTION_FAST(); + } else { + chunk = apply(tpl, chunk + 1, buf, variables, cd->chunk); + DISPATCH_NEXT_ACTION_CHECK(); + } + } + +action_end_if_variable_not_empty: + if (LIKELY(data == chunk)) + goto finalize; + DISPATCH_NEXT_ACTION_FAST(); + +action_apply_tpl: { + struct lwan_tpl *inner_tpl = chunk->data; + + if (LIKELY(lwan_strbuf_grow_by(buf, inner_tpl->minimum_size))) { + if (!apply(inner_tpl, chunk_array_get_array(&inner_tpl->chunks), + buf, variables, NULL)) { + lwan_status_warning("Could not apply subtemplate"); + return NULL; + } + } else { + lwan_status_warning("Could not grow template by %zu bytes", + inner_tpl->minimum_size); + return NULL; + } + + DISPATCH_NEXT_ACTION_FAST(); + } + +action_start_iter: + if (UNLIKELY(coro != NULL)) { + lwan_status_warning("Coroutine is not NULL when starting iteration"); + return NULL; + } + + struct chunk_descriptor *cd = chunk->data; + coro = coro_new(&switcher, cd->descriptor->generator, variables); + + bool resumed = coro_resume_value(coro, 0); + bool negate = chunk->flags & FLAGS_NEGATE; + if (negate) + resumed = !resumed; + if (!resumed) { + chunk = cd->chunk; + + if (negate) + coro_resume_value(coro, 1); + + coro_free(coro); + coro = NULL; + + if (negate) + DISPATCH_ACTION_FAST(); + + DISPATCH_NEXT_ACTION_FAST(); + } + + chunk = apply(tpl, chunk + 1, buf, variables, chunk); + DISPATCH_ACTION_CHECK(); + +action_end_iter: + if (data == chunk->data) + goto finalize; + + if (UNLIKELY(!coro)) { + if (!chunk->flags) { + lwan_status_warning("Coroutine is NULL when finishing iteration"); + return NULL; + } + DISPATCH_NEXT_ACTION_FAST(); + } + + if (!coro_resume_value(coro, 0)) { + coro_free(coro); + coro = NULL; + DISPATCH_NEXT_ACTION_FAST(); + } + + chunk = apply(tpl, ((struct chunk *)chunk->data) + 1, buf, variables, + chunk->data); + DISPATCH_ACTION_CHECK(); + +finalize: + return chunk; +#undef DISPATCH_ACTION +#undef DISPATCH_NEXT_ACTION +#undef DISPATCH_ACTION_CHECK +#undef DISPATCH_NEXT_ACTION_CHECK +#undef DISPATCH_ACTION_FAST +#undef DISPATCH_NEXT_ACTION_FAST +#undef RETURN_IF_NO_CHUNK +} + +bool lwan_tpl_apply_with_buffer(struct lwan_tpl *tpl, + struct lwan_strbuf *buf, + void *variables) +{ + lwan_strbuf_reset(buf); + + if (UNLIKELY(!lwan_strbuf_grow_to(buf, tpl->minimum_size))) + return false; + + if (!apply(tpl, tpl->chunks.base.base, buf, variables, NULL)) + return false; + + return true; +} + +struct lwan_strbuf *lwan_tpl_apply(struct lwan_tpl *tpl, void *variables) +{ + struct lwan_strbuf *buf = lwan_strbuf_new_with_size(tpl->minimum_size); + + if (UNLIKELY(!buf)) + return NULL; + + if (LIKELY(lwan_tpl_apply_with_buffer(tpl, buf, variables))) + return buf; + + lwan_strbuf_free(buf); + return NULL; +} + +#ifdef TEMPLATE_TEST + +struct test_struct { + int some_int; + char *a_string; +}; + +static void hello_lambda(struct lwan_strbuf *b, void *v) +{ + struct test_struct *s = v; + + lwan_strbuf_append_printf("hello from lambda func! appending <%d> from test_struct", s->some_int); +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) { + printf("Usage: %s file.tpl\n", argv[0]); + return 1; + } + + printf("*** Compiling template...\n"); + #undef TPL_STRUCT + #define TPL_STRUCT struct test_struct + struct lwan_var_descriptor desc[] = { + TPL_VAR_INT(some_int), + TPL_VAR_STR(a_string), + TPL_LAMBDA(hello, hello_lambda), + TPL_VAR_SENTINEL + }; + struct lwan_tpl *tpl = lwan_tpl_compile_file(argv[1], desc); + if (!tpl) + return 1; + + printf("*** Applying template 10000000 times...\n"); + for (size_t i = 0; i < 10000000; i++) { + struct lwan_strbuf *applied = lwan_tpl_apply(tpl, &(struct test_struct) { + .some_int = 42, + .a_string = "some string" + }); + lwan_strbuf_free(applied); + } + + lwan_tpl_free(tpl); + return 0; +} + +#endif /* TEMPLATE_TEST */ diff --git a/src/lib/lwan-template.h b/src/lib/lwan-template.h new file mode 100644 index 000000000..59fd96e99 --- /dev/null +++ b/src/lib/lwan-template.h @@ -0,0 +1,120 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ +#pragma once + +#include "lwan-coro.h" +#include "lwan-strbuf.h" +#include + +enum lwan_tpl_flag { LWAN_TPL_FLAG_CONST_TEMPLATE = 1 << 0 }; + +struct lwan_var_descriptor { + const char *name; + const off_t offset; + const struct lwan_var_descriptor *list_desc; + + union { + struct { + void (*append_to_strbuf)(struct lwan_strbuf *buf, void *ptr); + bool (*get_is_empty)(void *ptr); + }; + struct { + coro_function_t generator; + }; + struct { + void (*lambda)(struct lwan_strbuf *buf, void *ptr); + }; + }; +}; + +#define TPL_LAMBDA(var_, lambda_) \ + { \ + .name = #var_, .offset = 0x1aabdacb, .lambda = lambda_, \ + /* 0x1aabdacb = lambda call back */ \ + } + +#define TPL_VAR_SIMPLE(var_, append_to_lwan_strbuf_, get_is_empty_) \ + { \ + .name = #var_, .offset = offsetof(TPL_STRUCT, var_), \ + .append_to_strbuf = append_to_lwan_strbuf_, \ + .get_is_empty = get_is_empty_ \ + } + +#define TPL_VAR_SEQUENCE(var_, generator_, seqitem_desc_) \ + { \ + .name = #var_, .offset = offsetof(TPL_STRUCT, var_.generator), \ + .generator = generator_, .list_desc = seqitem_desc_ \ + } + +#define TPL_VAR_INT(var_) \ + TPL_VAR_SIMPLE(var_, lwan_append_int_to_strbuf, lwan_tpl_int_is_empty) + +#define TPL_VAR_DOUBLE(var_) \ + TPL_VAR_SIMPLE(var_, lwan_append_double_to_strbuf, lwan_tpl_double_is_empty) + +#define TPL_VAR_STR(var_) \ + TPL_VAR_SIMPLE(var_, lwan_append_str_to_strbuf, lwan_tpl_str_is_empty) + +#define TPL_VAR_STR_ESCAPE(var_) \ + TPL_VAR_SIMPLE(var_, lwan_append_str_escaped_to_strbuf, \ + lwan_tpl_str_is_empty) + +#define TPL_VAR_SENTINEL \ + { \ + .name = NULL, .offset = 0, \ + } + +/* + * These functions are not meant to be used directly. We do need a pointer to + * them, though, that's why they're exported. Eventually this will move to + * something more opaque. + */ +void lwan_append_int_to_strbuf(struct lwan_strbuf *buf, void *ptr); +bool lwan_tpl_int_is_empty(void *ptr); +void lwan_append_str_to_strbuf(struct lwan_strbuf *buf, void *ptr); +void lwan_append_str_escaped_to_strbuf(struct lwan_strbuf *buf, void *ptr); +bool lwan_tpl_str_is_empty(void *ptr); +void lwan_append_double_to_strbuf(struct lwan_strbuf *buf, void *ptr); +bool lwan_tpl_double_is_empty(void *ptr); + +struct lwan_tpl * +lwan_tpl_compile_value_full(struct lwan_value value, + const struct lwan_var_descriptor *descriptor, + enum lwan_tpl_flag flags); +static inline struct lwan_tpl * +lwan_tpl_compile_string_full(const char *string, + const struct lwan_var_descriptor *descriptor, + enum lwan_tpl_flag flags) +{ + struct lwan_value value = {.value = (char *)string, .len = strlen(string)}; + return lwan_tpl_compile_value_full(value, descriptor, flags); +} + +struct lwan_tpl * +lwan_tpl_compile_string(const char *string, + const struct lwan_var_descriptor *descriptor); +struct lwan_tpl * +lwan_tpl_compile_file(const char *filename, + const struct lwan_var_descriptor *descriptor); +struct lwan_strbuf *lwan_tpl_apply(struct lwan_tpl *tpl, void *variables); +bool lwan_tpl_apply_with_buffer(struct lwan_tpl *tpl, + struct lwan_strbuf *buf, + void *variables); +void lwan_tpl_free(struct lwan_tpl *tpl); diff --git a/src/lib/lwan-thread.c b/src/lib/lwan-thread.c new file mode 100644 index 000000000..5ce4ff6ed --- /dev/null +++ b/src/lib/lwan-thread.c @@ -0,0 +1,1731 @@ +/* + * lwan - web server + * Copyright (c) 2012, 2013 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(LWAN_HAVE_SO_ATTACH_REUSEPORT_CBPF) +#include +#endif + +#if defined(LWAN_HAVE_MBEDTLS) +#include +#include +#include +#include +#include + +#include +#include +#endif + +#include "list.h" +#include "lwan-private.h" +#include "lwan-tq.h" + +static void lwan_strbuf_free_defer(void *data) +{ + return lwan_strbuf_free((struct lwan_strbuf *)data); +} + +static void graceful_close(struct lwan *l, + struct lwan_connection *conn) +{ + int fd = lwan_connection_get_fd(l, conn); + + while (TIOCOUTQ) { + /* This ioctl isn't probably doing what it says on the tin; the details + * are subtle, but it seems to do the trick to allow gracefully closing + * the connection in some cases with minimal system calls. */ + int bytes_waiting; + int r = ioctl(fd, TIOCOUTQ, &bytes_waiting); + + if (!r && !bytes_waiting) /* See note about close(2) below. */ + return; + if (r < 0 && errno == EINTR) + continue; + + break; + } + + if (UNLIKELY(shutdown(fd, SHUT_WR) < 0)) { + if (UNLIKELY(errno == ENOTCONN)) + return; + } + + char buffer[128]; + for (int tries = 0; tries < 20; tries++) { + ssize_t r = recv(fd, buffer, sizeof(buffer), MSG_TRUNC); + + if (!r) + break; + + if (r < 0) { + switch (errno) { + case EAGAIN: + break; + case EINTR: + continue; + default: + return; + } + } + + coro_yield(conn->coro, CONN_CORO_WANT_READ); + } + + /* close(2) will be called when the coroutine yields with CONN_CORO_ABORT */ +} + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) +static void lwan_random_seed_prng_for_thread(const struct lwan_thread *t) +{ + (void)t; +} + +uint64_t lwan_random_uint64() +{ + static uint64_t value = 1; + + return ATOMIC_INC(value); +} +#else +static __thread __uint128_t lehmer64_state; + +static void lwan_random_seed_prng_for_thread(const struct lwan_thread *t) +{ + if (lwan_getentropy(&lehmer64_state, sizeof(lehmer64_state), 0) < 0) { + lwan_status_warning("Couldn't get proper entropy for PRNG, using fallback seed"); + uintptr_t ptr = (uintptr_t)t; + lehmer64_state |= fnv1a_64(&ptr, sizeof(ptr)); + lehmer64_state <<= 64; + lehmer64_state |= fnv1a_64(&t->epoll_fd, sizeof(t->epoll_fd)); + } +} + +uint64_t lwan_random_uint64() +{ + /* https://lemire.me/blog/2019/03/19/the-fastest-conventional-random-number-generator-that-can-pass-big-crush/ */ + lehmer64_state *= 0xda942042e4dd58b5ull; + return (uint64_t)(lehmer64_state >> 64); +} +#endif + +uint64_t lwan_request_get_id(struct lwan_request *request) +{ + struct lwan_request_parser_helper *helper = request->helper; + + if (helper->request_id == 0) { + helper->request_id = lwan_random_uint64(); + + if (UNLIKELY(helper->request_id == 0)) { + lwan_random_seed_prng_for_thread(request->conn->thread); + return lwan_request_get_id(request); + } + } + + return helper->request_id; +} + +#if defined(LWAN_HAVE_MBEDTLS) +static bool +lwan_setup_tls_keys(int fd, const mbedtls_ssl_context *ssl, int rx_or_tx) +{ + struct tls12_crypto_info_aes_gcm_128 info = { + .info = {.version = TLS_1_2_VERSION, + .cipher_type = TLS_CIPHER_AES_GCM_128}, + }; + const unsigned char *salt, *iv, *rec_seq; + const mbedtls_gcm_context *gcm_ctx; + const mbedtls_aes_context *aes_ctx; + + switch (rx_or_tx) { + case TLS_RX: + salt = ssl->transform->iv_dec; + rec_seq = ssl->in_ctr; + gcm_ctx = ssl->transform->cipher_ctx_dec.cipher_ctx; + break; + case TLS_TX: + salt = ssl->transform->iv_enc; + rec_seq = ssl->cur_out_ctr; + gcm_ctx = ssl->transform->cipher_ctx_enc.cipher_ctx; + break; + default: + __builtin_unreachable(); + } + + iv = salt + 4; + aes_ctx = gcm_ctx->cipher_ctx.cipher_ctx; + + memcpy(info.iv, iv, TLS_CIPHER_AES_GCM_128_IV_SIZE); + memcpy(info.rec_seq, rec_seq, TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE); + memcpy(info.key, aes_ctx->rk, TLS_CIPHER_AES_GCM_128_KEY_SIZE); + memcpy(info.salt, salt, TLS_CIPHER_AES_GCM_128_SALT_SIZE); + + if (UNLIKELY(setsockopt(fd, SOL_TLS, rx_or_tx, &info, sizeof(info)) < 0)) { + lwan_status_perror("Could not set %s kTLS keys for fd %d", + rx_or_tx == TLS_TX ? "transmission" : "reception", + fd); + lwan_always_bzero(&info, sizeof(info)); + return false; + } + + lwan_always_bzero(&info, sizeof(info)); + return true; +} + +__attribute__((format(printf, 2, 3))) +__attribute__((noinline, cold)) +static void lwan_status_mbedtls_error(int error_code, const char *fmt, ...) +{ + char *formatted; + va_list ap; + int r; + + va_start(ap, fmt); + r = vasprintf(&formatted, fmt, ap); + if (r >= 0) { + char mbedtls_errbuf[128]; + + mbedtls_strerror(error_code, mbedtls_errbuf, sizeof(mbedtls_errbuf)); + lwan_status_error("%s: %s", formatted, mbedtls_errbuf); + free(formatted); + } + va_end(ap); +} + +static void lwan_setup_tls_free_ssl_context(void *data) +{ + mbedtls_ssl_context *ssl = data; + + mbedtls_ssl_free(ssl); +} + +struct lwan_mbedtls_handshake_ctx { + int fd; + bool last_was_send; +}; + +static int lwan_mbedtls_send(void *ctx, const unsigned char *buf, size_t len) +{ + struct lwan_mbedtls_handshake_ctx *hs_ctx = ctx; + ssize_t r; + + /* We use MSG_MORE -- flushing when we transition from send() to recv() + * -- rather than buffering on our side because this contains key + * material that we would need to only copy, but also zero out after + * finishing the handshake. */ + + r = send(hs_ctx->fd, buf, len, MSG_MORE); + if (UNLIKELY(r < 0)) { + switch (errno) { + case EINTR: + case EAGAIN: + return MBEDTLS_ERR_SSL_WANT_WRITE; + + default: + /* It's not an internal error here, but this seemed the least + * innapropriate error code for this situation. lwan_setup_tls() + * doesn't care. */ + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + } + } + + if (UNLIKELY((ssize_t)(int)r != r)) + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + + hs_ctx->last_was_send = true; + return (int)r; +} + +static void flush_pending_output(int fd) +{ + int zero = 0; + setsockopt(fd, SOL_TCP, TCP_CORK, &zero, sizeof(zero)); +} + +static int lwan_mbedtls_recv(void *ctx, unsigned char *buf, size_t len) +{ + struct lwan_mbedtls_handshake_ctx *hs_ctx = ctx; + ssize_t r; + + if (hs_ctx->last_was_send) { + flush_pending_output(hs_ctx->fd); + hs_ctx->last_was_send = false; + } + + r = recv(hs_ctx->fd, buf, len, 0); + if (UNLIKELY(r < 0)) { + switch (errno) { + case EINTR: + case EAGAIN: + return MBEDTLS_ERR_SSL_WANT_READ; + + default: + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + } + } + + if (UNLIKELY((ssize_t)(int)r != r)) + return MBEDTLS_ERR_SSL_INTERNAL_ERROR; + + return (int)r; +} + +static bool lwan_setup_tls(const struct lwan *l, struct lwan_connection *conn) +{ + mbedtls_ssl_context ssl; + bool retval = false; + int r; + + mbedtls_ssl_init(&ssl); + + r = mbedtls_ssl_setup(&ssl, &l->tls->config); + if (UNLIKELY(r != 0)) { + lwan_status_mbedtls_error(r, "Could not setup TLS context"); + return false; + } + + /* Yielding the coroutine during the handshake enables the I/O loop to + * destroy this coro (e.g. on connection hangup) before we have the + * opportunity to free the SSL context. Defer this call for these + * cases. */ + coro_deferred defer = + coro_defer(conn->coro, lwan_setup_tls_free_ssl_context, &ssl); + + if (UNLIKELY(!defer)) { + lwan_status_error("Could not defer cleanup of the TLS context"); + return false; + } + + int fd = lwan_connection_get_fd(l, conn); + + struct lwan_mbedtls_handshake_ctx ctx = { .fd = fd }; + mbedtls_ssl_set_bio(&ssl, &ctx, lwan_mbedtls_send, + lwan_mbedtls_recv, NULL); + + while (true) { + switch (mbedtls_ssl_handshake(&ssl)) { + case 0: + flush_pending_output(fd); + goto enable_tls_ulp; + case MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS: + case MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS: + case MBEDTLS_ERR_SSL_WANT_READ: + coro_yield(conn->coro, CONN_CORO_WANT_READ); + break; + case MBEDTLS_ERR_SSL_WANT_WRITE: + coro_yield(conn->coro, CONN_CORO_WANT_WRITE); + break; + default: + goto fail; + } + } + +enable_tls_ulp: + if (UNLIKELY(setsockopt(fd, SOL_TCP, TCP_ULP, "tls", sizeof("tls")) < 0)) + goto fail; + if (UNLIKELY(!lwan_setup_tls_keys(fd, &ssl, TLS_RX))) + goto fail; + if (UNLIKELY(!lwan_setup_tls_keys(fd, &ssl, TLS_TX))) + goto fail; + + retval = true; + +fail: + coro_defer_disarm(conn->coro, defer); + mbedtls_ssl_free(&ssl); + return retval; +} +#endif + +__attribute__((cold)) +static bool send_buffer_without_coro(int fd, const char *buf, size_t buf_len, int flags) +{ + size_t total_sent = 0; + + for (int try = 0; try < 10; try++) { + size_t to_send = buf_len - total_sent; + if (!to_send) + return true; + + ssize_t sent = send(fd, buf + total_sent, to_send, flags); + if (sent <= 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN) + continue; + break; + } + + total_sent += (size_t)sent; + } + + return false; +} + +__attribute__((cold)) +static bool send_string_without_coro(int fd, const char *str, int flags) +{ + return send_buffer_without_coro(fd, str, strlen(str), flags); +} + +__attribute__((cold)) static void +send_last_response_without_coro(const struct lwan *l, + const struct lwan_connection *conn, + enum lwan_http_status status) +{ + int fd = lwan_connection_get_fd(l, conn); + + if (conn->flags & CONN_TLS) { + /* There's nothing that can be done here if a client is expecting a + * TLS connection: the TLS handshake requires a coroutine as it + * might yield. (In addition, the TLS handshake might allocate + * memory, and if you couldn't create a coroutine at this point, + * it's unlikely you'd be able to allocate memory for the TLS + * context anyway.) */ + goto shutdown_and_close; + } + + if (!send_string_without_coro(fd, "HTTP/1.0 ", MSG_MORE)) + goto shutdown_and_close; + + if (!send_string_without_coro( + fd, lwan_http_status_as_string_with_code(status), MSG_MORE)) + goto shutdown_and_close; + + if (!send_string_without_coro(fd, "\r\nConnection: close", MSG_MORE)) + goto shutdown_and_close; + + if (!send_string_without_coro(fd, "\r\nContent-Type: text/html", MSG_MORE)) + goto shutdown_and_close; + + if (send_buffer_without_coro(fd, l->headers.value, l->headers.len, + MSG_MORE)) { + struct lwan_strbuf buffer; + + lwan_strbuf_init(&buffer); + lwan_fill_default_response(&buffer, status); + + send_buffer_without_coro(fd, lwan_strbuf_get_buffer(&buffer), + lwan_strbuf_get_length(&buffer), 0); + + lwan_strbuf_free(&buffer); + } + +shutdown_and_close: + shutdown(fd, SHUT_RDWR); + close(fd); +} + +__attribute__((noreturn)) static int process_request_coro(struct coro *coro, + void *data) +{ + /* NOTE: This function should not return; coro_yield should be used + * instead. This ensures the storage for `strbuf` is alive when the + * coroutine ends and lwan_strbuf_free() is called. */ + char *header_start[N_HEADER_START]; + struct lwan_connection *conn = data; + struct lwan *lwan = conn->thread->lwan; + int fd = lwan_connection_get_fd(lwan, conn); + enum lwan_request_flags flags = lwan->config.request_flags; + const size_t request_buffer_size = lwan->config.request_buffer_size; + const int error_when_n_packets = lwan_calculate_n_packets(request_buffer_size); + struct lwan_strbuf strbuf = LWAN_STRBUF_STATIC_INIT; + struct lwan_value buffer; + char *next_request = NULL; + struct lwan_proxy proxy; + size_t init_gen; + + coro_defer(coro, lwan_strbuf_free_defer, &strbuf); + +#if defined(LWAN_HAVE_MBEDTLS) + if (conn->flags & CONN_TLS) { + if (UNLIKELY(!lwan_setup_tls(lwan, conn))) { + coro_yield(conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + } +#else + assert(!(conn->flags & CONN_TLS)); +#endif + + if (request_buffer_size > DEFAULT_BUFFER_SIZE) { + buffer = (struct lwan_value){ + .value = coro_malloc_full(conn->coro, request_buffer_size, free), + .len = request_buffer_size, + }; + + if (UNLIKELY(!buffer.value)) { + /* If CONN_TLS is set at this point, we can send responses just + * fine and they'll be encrypted by the kernel. However, + * send_last_response_without_coro() can't send the response if + * this bit is set as it has been designed to be used in cases + * where coroutines were not created yet. */ + conn->flags &= ~CONN_TLS; + + send_last_response_without_coro(lwan, conn, HTTP_UNAVAILABLE); + + coro_yield(conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + init_gen = 2; + } else { + buffer = (struct lwan_value){ + .value = alloca(DEFAULT_BUFFER_SIZE), + .len = DEFAULT_BUFFER_SIZE, + }; + + init_gen = 1; + } + + while (true) { + struct lwan_request_parser_helper helper = { + .buffer = &buffer, + .next_request = next_request, + .error_when_n_packets = error_when_n_packets, + .header_start = header_start, + }; + struct lwan_request request = {.conn = conn, + .global_response_headers = &lwan->headers, + .fd = fd, + .response = {.buffer = &strbuf}, + .flags = flags, + .proxy = &proxy, + .helper = &helper}; + + lwan_process_request(lwan, &request); + + /* Run the deferred instructions now (except those used to initialize + * the coroutine), so that if the connection is gracefully closed, + * the storage for ``helper'' is still there. */ + coro_deferred_run(coro, init_gen); + + if (UNLIKELY(!(conn->flags & CONN_IS_KEEP_ALIVE))) { + graceful_close(lwan, conn); + break; + } + + if (next_request && *next_request) { + conn->flags |= CONN_CORK; + + if (!(conn->flags & CONN_EVENTS_WRITE)) + coro_yield(coro, CONN_CORO_WANT_WRITE); + } else { + conn->flags &= ~CONN_CORK; + coro_yield(coro, CONN_CORO_WANT_READ); + } + + /* Ensure string buffer is reset between requests, and that the backing + * store isn't over 2KB. */ + lwan_strbuf_reset_trim(&strbuf, 2048); + + /* Only allow flags from config. */ + flags = request.flags & (REQUEST_PROXIED | REQUEST_ALLOW_CORS | REQUEST_WANTS_HSTS_HEADER); + next_request = helper.next_request; + } + + coro_yield(coro, CONN_CORO_ABORT); + __builtin_unreachable(); +} + +#define EPOLL_EVENTS(flags) (((uint32_t)flags) >> CONN_EPOLL_EVENT_SHIFT) +#define LWAN_EVENTS(flags) (((uint32_t)flags) & CONN_EPOLL_EVENT_MASK) + +static ALWAYS_INLINE uint32_t +conn_flags_to_epoll_events(enum lwan_connection_flags flags) +{ + assert((EPOLL_EVENTS(flags) & + (uint32_t) ~(EPOLLIN | EPOLLOUT | EPOLLRDHUP)) == 0); + return EPOLL_EVENTS(flags); +} + +static int update_epoll_flags(const struct lwan *lwan, + struct lwan_connection *conn, + int epoll_fd, + enum lwan_connection_coro_yield yield_result) +{ + static const enum lwan_connection_flags or_mask[CONN_CORO_MAX] = { + [CONN_CORO_YIELD] = 0, + + [CONN_CORO_WANT_READ_WRITE] = CONN_EVENTS_READ_WRITE, + [CONN_CORO_WANT_READ] = CONN_EVENTS_READ, + [CONN_CORO_WANT_WRITE] = CONN_EVENTS_WRITE, + + /* While the coro is suspended, we're not interested in either EPOLLIN + * or EPOLLOUT events. We still want to track this fd in epoll, though, + * so unset both so that only EPOLLRDHUP (plus the implicitly-set ones) + * are set. */ + [CONN_CORO_SUSPEND] = CONN_SUSPENDED, + + /* Ideally, when suspending a coroutine, the current flags&CONN_EVENTS_MASK + * would have to be stored and restored -- however, resuming as if the + * client coroutine is interested in a write event always guarantees that + * they'll be resumed as they're TCP sockets. There's a good chance that + * trying to read from a socket after resuming a coroutine will succeed, + * but if it doesn't because read() returns -EAGAIN, the I/O wrappers will + * yield with CONN_CORO_WANT_READ anyway. */ + [CONN_CORO_RESUME] = CONN_EVENTS_WRITE, + }; + static const enum lwan_connection_flags and_mask[CONN_CORO_MAX] = { + [CONN_CORO_YIELD] = ~0, + + [CONN_CORO_WANT_READ_WRITE] = ~0, + [CONN_CORO_WANT_READ] = ~CONN_EVENTS_WRITE, + [CONN_CORO_WANT_WRITE] = ~CONN_EVENTS_READ, + + [CONN_CORO_SUSPEND] = ~CONN_EVENTS_READ_WRITE, + [CONN_CORO_RESUME] = ~CONN_SUSPENDED, + }; + enum lwan_connection_flags prev_flags = conn->flags; + + conn->flags |= or_mask[yield_result]; + conn->flags &= and_mask[yield_result]; + + assert(!(conn->flags & CONN_LISTENER)); + assert((conn->flags & CONN_TLS) == (prev_flags & CONN_TLS)); + + if (LWAN_EVENTS(conn->flags) == LWAN_EVENTS(prev_flags)) + return 0; + + struct epoll_event event = {.events = conn_flags_to_epoll_events(conn->flags), + .data.ptr = conn}; + int fd = lwan_connection_get_fd(lwan, conn); + return epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event); +} + +static void unasync_await_conn(void *data1, void *data2) +{ + struct lwan_connection *async_fd_conn = data1; + + async_fd_conn->flags &= + ~(CONN_ASYNC_AWAIT | CONN_HUNG_UP | CONN_ASYNC_AWAITV); + assert(async_fd_conn->parent); + async_fd_conn->parent->flags &= ~CONN_ASYNC_AWAITV; + + async_fd_conn->thread = data2; + + /* If this file descriptor number is used again in the future as an HTTP + * connection, we need the coro pointer to be NULL so a new coroutine is + * created! */ + async_fd_conn->coro = NULL; + + /* While not strictly necessary, make sure that prev/next point to + * something valid rather than whatever junk was left from when their + * storage was used for the parent pointer. */ + async_fd_conn->prev = -1; + async_fd_conn->next = -1; +} + +static int prepare_await(const struct lwan *l, + enum lwan_connection_coro_yield yield_result, + int await_fd, + struct lwan_connection *conn, + int epoll_fd) +{ + static const enum lwan_connection_flags to_connection_flags[] = { + [CONN_CORO_WANT_READ] = CONN_EVENTS_READ, + [CONN_CORO_WANT_WRITE] = CONN_EVENTS_WRITE, + [CONN_CORO_WANT_READ_WRITE] = CONN_EVENTS_READ_WRITE, + }; + enum lwan_connection_flags flags; + int op; + + assert(await_fd >= 0); + assert(yield_result >= CONN_CORO_WANT_READ && + yield_result <= CONN_CORO_WANT_READ_WRITE); + + flags = to_connection_flags[yield_result]; + + struct lwan_connection *await_fd_conn = &l->conns[await_fd]; + if (LIKELY(await_fd_conn->flags & CONN_ASYNC_AWAIT)) { + if (LIKELY(LWAN_EVENTS(await_fd_conn->flags) == LWAN_EVENTS(flags))) { + return 0; + } + + op = EPOLL_CTL_MOD; + } else { + await_fd_conn->parent = conn; + + /* We assert() in the timeout queue that we're not freeing a + * coroutine when CONN_ASYNC_AWAIT is set in the connection, and are + * careful to not ever do that. This makes us get away with struct + * coro not being refcounted, even though this kinda feels like + * running with scissors. */ + assert(!await_fd_conn->coro); + await_fd_conn->coro = conn->coro; + + /* Since scheduling is performed during startup, we gotta take note + * of which thread was originally supposed to handle this particular + * file descriptor once we're done borrowing this lwan_connection + * for the awaited file descriptor. */ + struct lwan_thread *old_thread = await_fd_conn->thread; + await_fd_conn->thread = conn->thread; + + op = EPOLL_CTL_ADD; + flags |= CONN_ASYNC_AWAIT; + + coro_defer2(conn->coro, unasync_await_conn, await_fd_conn, old_thread); + } + + struct epoll_event event = {.events = conn_flags_to_epoll_events(flags), + .data.ptr = await_fd_conn}; + if (LIKELY(!epoll_ctl(epoll_fd, op, await_fd, &event))) { + await_fd_conn->flags &= ~CONN_EVENTS_MASK; + await_fd_conn->flags |= flags; + return 0; + } + + return -errno; +} + +static void clear_awaitv_flags(struct lwan_connection *conns, va_list ap_orig) +{ + va_list ap; + + va_copy(ap, ap_orig); + for (int fd = va_arg(ap, int); fd >= 0; fd = va_arg(ap, int)) { + conns[fd].flags &= ~CONN_ASYNC_AWAITV; + LWAN_NO_DISCARD(va_arg(ap, enum lwan_connection_coro_yield)); + } + va_end(ap); +} + +struct awaitv_state { + unsigned int num_awaiting; + enum lwan_connection_coro_yield request_conn_yield; +}; + +static int prepare_awaitv(struct lwan_request *r, + struct lwan *l, + va_list ap, + struct awaitv_state *state) +{ + int epoll_fd = r->conn->thread->epoll_fd; + + *state = (struct awaitv_state){ + .num_awaiting = 0, + .request_conn_yield = CONN_CORO_SUSPEND, + }; + + clear_awaitv_flags(l->conns, ap); + + for (int await_fd = va_arg(ap, int); await_fd >= 0; + await_fd = va_arg(ap, int)) { + struct lwan_connection *conn = &l->conns[await_fd]; + enum lwan_connection_coro_yield events = + va_arg(ap, enum lwan_connection_coro_yield); + + if (UNLIKELY(events < CONN_CORO_WANT_READ || + events > CONN_CORO_WANT_READ_WRITE)) { + return -EINVAL; + } + if (UNLIKELY(conn->flags & CONN_ASYNC_AWAITV)) { + lwan_status_debug("ignoring second awaitv call on same fd: %d", + await_fd); + continue; + } + + conn->flags |= CONN_ASYNC_AWAITV; + state->num_awaiting++; + + if (await_fd == r->fd) { + state->request_conn_yield = events; + continue; + } + + int ret = prepare_await(l, events, await_fd, r->conn, epoll_fd); + if (UNLIKELY(ret < 0)) { + errno = -ret; + lwan_status_perror("prepare_await(%d)", await_fd); + return ret; + } + } + + return 0; +} + +int lwan_request_awaitv_any(struct lwan_request *r, ...) +{ + struct lwan *l = r->conn->thread->lwan; + struct awaitv_state state; + va_list ap; + + va_start(ap, r); + int ret = prepare_awaitv(r, l, ap, &state); + va_end(ap); + + if (UNLIKELY(ret < 0)) { + errno = -ret; + lwan_status_perror("prepare_awaitv()"); + coro_yield(r->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + while (true) { + int64_t v = coro_yield(r->conn->coro, state.request_conn_yield); + struct lwan_connection *conn = (struct lwan_connection *)(uintptr_t)v; + + if (conn->flags & CONN_ASYNC_AWAITV) { + /* Ensure flags are unset in case awaitv_any() is called with + * a different set of file descriptors. */ + va_start(ap, r); + clear_awaitv_flags(l->conns, ap); + va_end(ap); + + int fd = lwan_connection_get_fd(l, conn); + return UNLIKELY(conn->flags & CONN_HUNG_UP) ? -fd : fd; + } + } +} + +int lwan_request_awaitv_all(struct lwan_request *r, ...) +{ + struct lwan *l = r->conn->thread->lwan; + struct awaitv_state state; + va_list ap; + + va_start(ap, r); + int ret = prepare_awaitv(r, l, ap, &state); + va_end(ap); + + if (UNLIKELY(ret < 0)) { + errno = -ret; + lwan_status_perror("prepare_awaitv()"); + coro_yield(r->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + while (state.num_awaiting) { + int64_t v = coro_yield(r->conn->coro, state.request_conn_yield); + struct lwan_connection *conn = (struct lwan_connection *)(uintptr_t)v; + + if (conn->flags & CONN_ASYNC_AWAITV) { + conn->flags &= ~CONN_ASYNC_AWAITV; + + if (UNLIKELY(conn->flags & CONN_HUNG_UP)) { + /* Ensure flags are unset in case awaitv_any() is called with + * a different set of file descriptors. */ + va_start(ap, r); + clear_awaitv_flags(l->conns, ap); + va_end(ap); + + return lwan_connection_get_fd(l, conn); + } + + state.num_awaiting--; + } + } + + return -EISCONN; +} + +static inline int async_await_fd(struct lwan_request *request, + int fd, + enum lwan_connection_coro_yield events) +{ + struct lwan_thread *thread = request->conn->thread; + struct lwan *lwan = thread->lwan; + struct lwan_connection *awaited = &lwan->conns[fd]; + + if (request->conn != awaited) { + int r = + prepare_await(lwan, events, fd, request->conn, thread->epoll_fd); + if (UNLIKELY(r < 0)) + return r; + + events = CONN_CORO_SUSPEND; + } + + while (true) { + int64_t from_coro = coro_yield(request->conn->coro, events); + + if ((struct lwan_connection *)(intptr_t)from_coro == awaited) { + return UNLIKELY(awaited->flags & CONN_HUNG_UP) + ? -ECONNRESET + : lwan_connection_get_fd(lwan, awaited); + } + } +} + +int lwan_request_await_read(struct lwan_request *r, int fd) +{ + return async_await_fd(r, fd, CONN_CORO_WANT_READ); +} + +int lwan_request_await_write(struct lwan_request *r, int fd) +{ + return async_await_fd(r, fd, CONN_CORO_WANT_WRITE); +} + +int lwan_request_await_read_write(struct lwan_request *r, int fd) +{ + return async_await_fd(r, fd, CONN_CORO_WANT_READ_WRITE); +} + +static ALWAYS_INLINE void resume_coro(struct timeout_queue *tq, + struct lwan_connection *conn_to_resume, + struct lwan_connection *conn_to_yield, + int epoll_fd) +{ + assert(conn_to_resume->coro); + assert(conn_to_yield->coro); + + int64_t from_coro = coro_resume_value(conn_to_resume->coro, + (int64_t)(intptr_t)conn_to_yield); + if (UNLIKELY(from_coro == CONN_CORO_ABORT)) { + timeout_queue_expire(tq, conn_to_resume); + return; + } + + enum lwan_connection_coro_yield yield = (uint32_t)from_coro; + int r = update_epoll_flags(tq->lwan, conn_to_resume, epoll_fd, yield); + if (LIKELY(!r)) + timeout_queue_move_to_last(tq, conn_to_resume); +} + +static void update_date_cache(struct lwan_thread *thread) +{ + time_t now = time(NULL); + + lwan_format_rfc_time(now, thread->date.date); + lwan_format_rfc_time(now + (time_t)thread->lwan->config.expires, + thread->date.expires); +} + +static ALWAYS_INLINE bool spawn_coro(struct lwan_connection *conn, + coro_context *switcher, + struct timeout_queue *tq) +{ + struct lwan_thread *t = conn->thread; +#if defined(LWAN_HAVE_MBEDTLS) + const enum lwan_connection_flags flags_to_keep = conn->flags & CONN_TLS; +#else + const enum lwan_connection_flags flags_to_keep = 0; +#endif + + assert(!conn->coro); + assert(!(conn->flags & (CONN_ASYNC_AWAITV | CONN_ASYNC_AWAIT | CONN_HUNG_UP))); + assert(!(conn->flags & CONN_LISTENER)); + assert(t); + assert((uintptr_t)t >= (uintptr_t)tq->lwan->thread.threads); + assert((uintptr_t)t < + (uintptr_t)(tq->lwan->thread.threads + tq->lwan->thread.count)); + + *conn = (struct lwan_connection){ + .coro = coro_new(switcher, process_request_coro, conn), + .flags = CONN_EVENTS_READ | flags_to_keep, + .time_to_expire = tq->current_time + tq->move_to_last_bump, + .thread = t, + }; + if (LIKELY(conn->coro)) { + timeout_queue_insert(tq, conn); + return true; + } + + conn->flags = 0; + + int fd = lwan_connection_get_fd(tq->lwan, conn); + + lwan_status_error("Couldn't spawn coroutine for file descriptor %d", fd); + + send_last_response_without_coro(tq->lwan, conn, HTTP_UNAVAILABLE); + return false; +} + +static bool process_pending_timers(struct timeout_queue *tq, + struct lwan_thread *t, + int epoll_fd) +{ + struct timeout *timeout; + bool should_expire_timers = false; + + while ((timeout = timeouts_get(t->wheel))) { + if (timeout == &tq->timeout) { + should_expire_timers = true; + continue; + } + + struct lwan_request *request = + container_of(timeout, struct lwan_request, timeout); + int r = update_epoll_flags(tq->lwan, request->conn, epoll_fd, + CONN_CORO_RESUME); + if (UNLIKELY(r < 0)) { + timeout_queue_expire(tq, request->conn); + } + } + + if (should_expire_timers) { + timeout_queue_expire_waiting(tq); + + /* tq timeout expires every 1000ms if there are connections, so + * update the date cache at this point as well. */ + update_date_cache(t); + + if (!timeout_queue_empty(tq)) { + timeouts_add(t->wheel, &tq->timeout, 1000); + return true; + } + + timeouts_del(t->wheel, &tq->timeout); + } + + return false; +} + +static int +turn_timer_wheel(struct timeout_queue *tq, struct lwan_thread *t, int epoll_fd) +{ + const int infinite_timeout = -1; + timeout_t wheel_timeout; + struct timespec now; + + if (UNLIKELY(clock_gettime(monotonic_clock_id, &now) < 0)) + lwan_status_critical("Could not get monotonic time"); + + timeouts_update(t->wheel, + (timeout_t)(now.tv_sec * 1000 + now.tv_nsec / 1000000)); + + /* Check if there's an expired timer. */ + wheel_timeout = timeouts_timeout(t->wheel); + if (wheel_timeout > 0) { + return (int)wheel_timeout; /* No, but will soon. Wake us up in + wheel_timeout ms. */ + } + + if (UNLIKELY((int64_t)wheel_timeout < 0)) + return infinite_timeout; /* None found. */ + + if (!process_pending_timers(tq, t, epoll_fd)) + return infinite_timeout; /* No more timers to process. */ + + /* After processing pending timers, determine when to wake up. */ + return (int)timeouts_timeout(t->wheel); +} + +static bool accept_waiting_clients(const struct lwan_thread *t, + const struct lwan_connection *listen_socket) +{ + const uint32_t read_events = conn_flags_to_epoll_events(CONN_EVENTS_READ); + struct lwan_connection *conns = t->lwan->conns; + int listen_fd = (int)(intptr_t)(listen_socket - conns); + enum lwan_connection_flags new_conn_flags = listen_socket->flags & CONN_TLS; + +#if !defined(NDEBUG) +# if defined(LWAN_HAVE_MBEDTLS) + if (listen_socket->flags & CONN_TLS) { + assert(listen_fd == t->tls_listen_fd); + } else { + assert(listen_fd == t->listen_fd); + } +# else + assert(!(new_conn_flags & CONN_TLS)); +# endif +#endif + + while (true) { + int fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); + + if (LIKELY(fd >= 0)) { + struct lwan_connection *conn = &conns[fd]; + struct epoll_event ev = {.data.ptr = conn, .events = read_events}; + int r; + + conn->flags = new_conn_flags; + + r = epoll_ctl(conn->thread->epoll_fd, EPOLL_CTL_ADD, fd, &ev); + if (UNLIKELY(r < 0)) { + lwan_status_perror("Could not add file descriptor %d to epoll " + "set %d. Dropping connection", + fd, conn->thread->epoll_fd); + send_last_response_without_coro(t->lwan, conn, HTTP_UNAVAILABLE); + conn->flags = 0; + } + + continue; + } + + switch (errno) { + default: + lwan_status_perror("Unexpected error while accepting connections"); + /* fallthrough */ + + case EAGAIN: + return true; + + case EBADF: + case ECONNABORTED: + case EINVAL: + lwan_status_info("Listening socket closed"); + return false; + } + } + + __builtin_unreachable(); +} + +static int create_listen_socket(struct lwan_thread *t, + unsigned int num, + bool tls) +{ + const struct lwan *lwan = t->lwan; + int listen_fd; + + listen_fd = lwan_create_listen_socket(lwan, num == 0, tls); + if (listen_fd < 0) + lwan_status_critical("Could not create listen_fd"); + + /* Ignore errors here, as this is just a hint */ +#if defined(LWAN_HAVE_SO_ATTACH_REUSEPORT_CBPF) + /* FIXME: this doesn't seem to work as expected. if the program returns + * a fixed number, sockets are always accepted by a thread pinned to + * that particular CPU; if SKF_AD_CPU is used, sockets are accepted by + * random threads as if this BPF script weren't installed at all. */ + + /* From socket(7): "These options may be set repeatedly at any time on + * any socket in the group to replace the current BPF program used by + * all sockets in the group." */ + if (num == 0) { + /* From socket(7): "The BPF program must return an index between 0 + * and N-1 representing the socket which should receive the packet + * (where N is the number of sockets in the group)." + * + * This should work because sockets are created in the same + * reuseport group, in the same order as the logical CPU#, and the + * worker threads for these sockets are pinned to the same CPU#. The + * MOD operation is there for cases where we have more CPUs than + * threads (e.g. by setting the "threads" setting in the configuration + * file); this isn't strictly necessary as any invalid value returned + * by this program will direct the connection to a random socket in + * the group. + * + * Unfortunately, this program doesn't work that way. Sockets seem + * to be delivered to a different thread every time. Maybe if we + * change this to eBPF, we'll be able to fetch the file descriptor + * and feed that into our scheduling table. */ + const uint32_t cpu_ad_cpu = (uint32_t)SKF_AD_OFF + SKF_AD_CPU; + const uint32_t n_sockets = lwan->thread.count; + struct sock_filter filter[] = { + {BPF_LD | BPF_W | BPF_ABS, 0, 0, cpu_ad_cpu}, /* A = current_cpu_idx */ + {BPF_ALU | BPF_MOD, 0, 0, n_sockets}, /* A %= socket_count */ + {BPF_RET | BPF_A, 0, 0, 0}, /* return A */ + }; + struct sock_fprog fprog = {.filter = filter, .len = N_ELEMENTS(filter)}; + + if (n_sockets > 1 && (n_sockets & (n_sockets - 1)) == 0) { + /* Not sure if the kernel will perform strength reduction + * on CBPF, so do it here. This is a common case. */ + assert(filter[1].code == (BPF_ALU | BPF_MOD)); + assert(filter[1].k == n_sockets); + + filter[1].code = BPF_ALU | BPF_AND; + filter[1].k = n_sockets - 1; + } + + (void)setsockopt(listen_fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, + &fprog, sizeof(fprog)); + (void)setsockopt(listen_fd, SOL_SOCKET, SO_LOCK_FILTER, (int[]){1}, + sizeof(int)); + } +#elif defined(LWAN_HAVE_SO_INCOMING_CPU) && defined(__x86_64__) + (void)setsockopt(listen_fd, SOL_SOCKET, SO_INCOMING_CPU, &t->cpu, + sizeof(t->cpu)); +#endif + + struct epoll_event event = { + .events = EPOLLIN | EPOLLET | EPOLLERR, + .data.ptr = &lwan->conns[listen_fd], + }; + if (epoll_ctl(t->epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) < 0) + lwan_status_critical_perror("Could not add socket to epoll"); + + return listen_fd; +} + +static void *thread_io_loop(void *data) +{ + struct lwan_thread *t = data; + int epoll_fd = t->epoll_fd; + const int max_events = LWAN_MIN((int)t->lwan->thread.max_fd, 1024); + struct lwan *lwan = t->lwan; + struct epoll_event *events; + coro_context switcher; + struct timeout_queue tq; + + if (t->cpu == UINT_MAX) { + lwan_status_debug("Worker thread #%zd starting", + t - t->lwan->thread.threads + 1); + } else { + lwan_status_debug("Worker thread #%zd starting on CPU %d", + t - t->lwan->thread.threads + 1, + t->cpu); + } + + lwan_set_thread_name("worker"); + + events = calloc((size_t)max_events, sizeof(*events)); + if (UNLIKELY(!events)) + lwan_status_critical("Could not allocate memory for events"); + + update_date_cache(t); + + timeout_queue_init(&tq, lwan); + + lwan_random_seed_prng_for_thread(t); + + pthread_barrier_wait(&lwan->thread.barrier); + + for (;;) { + int timeout = turn_timer_wheel(&tq, t, epoll_fd); + int n_fds = epoll_wait(epoll_fd, events, max_events, timeout); + bool created_coros = false; + + if (UNLIKELY(n_fds < 0)) { + if (errno == EBADF || errno == EINVAL) + break; + continue; + } + + for (struct epoll_event *event = events; n_fds--; event++) { + struct lwan_connection *conn = event->data.ptr; + + if (conn->flags & CONN_ASYNC_AWAIT) { + /* Assert that the connection is part of the conns array, + * since the storage for conn->parent is shared with + * prev/next. */ + assert(conn->parent >= lwan->conns); + assert(conn->parent <= &lwan->conns[lwan->thread.max_fd]); + + /* Also validate that conn->parent is in fact a HTTP client + * connection and not an awaited fd! */ + assert(!(conn->parent->flags & CONN_ASYNC_AWAIT)); + + /* CONN_ASYNC_AWAIT conns *must* have a coro and thread as + * it's the same as the HTTP client coro for API + * consistency, as struct lwan_connection isn't opaque. (If + * it were opaque, or at least a private API, though, we + * might be able to get away with reusing the space for + * these two pointers for something else in some cases. + * This has not been necessary yet, but might become useful + * in the future.) */ + assert(conn->coro); + assert(conn->coro == conn->parent->coro); + assert(conn->thread == conn->parent->thread); + + if (UNLIKELY(events->events & (EPOLLRDHUP | EPOLLHUP))) + conn->flags |= CONN_HUNG_UP; + + resume_coro(&tq, conn->parent, conn, epoll_fd); + + continue; + } + + if (conn->flags & CONN_LISTENER) { + if (LIKELY(accept_waiting_clients(t, conn))) + continue; + close(epoll_fd); + epoll_fd = -1; + break; + } + + if (UNLIKELY(event->events & (EPOLLRDHUP | EPOLLHUP))) { + timeout_queue_expire(&tq, conn); + continue; + } + + if (!conn->coro) { + if (UNLIKELY(!spawn_coro(conn, &switcher, &tq))) { + send_last_response_without_coro(t->lwan, conn, HTTP_UNAVAILABLE); + continue; + } + + created_coros = true; + } + + resume_coro(&tq, conn, conn, epoll_fd); + } + + if (created_coros) + timeouts_add(t->wheel, &tq.timeout, 1000); + } + + pthread_barrier_wait(&lwan->thread.barrier); + + timeout_queue_expire_all(&tq); + free(events); + + return NULL; +} + +static void create_thread(struct lwan *l, struct lwan_thread *thread) +{ + int ignore; + pthread_attr_t attr; + + thread->lwan = l; + + thread->wheel = timeouts_open(&ignore); + if (!thread->wheel) + lwan_status_critical("Could not create timer wheel"); + + if ((thread->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) + lwan_status_critical_perror("epoll_create"); + + if (pthread_attr_init(&attr)) + lwan_status_critical_perror("pthread_attr_init"); + + if (pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM)) + lwan_status_critical_perror("pthread_attr_setscope"); + + if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE)) + lwan_status_critical_perror("pthread_attr_setdetachstate"); + + if (pthread_create(&thread->self, &attr, thread_io_loop, thread)) + lwan_status_critical_perror("pthread_create"); + + if (pthread_attr_destroy(&attr)) + lwan_status_critical_perror("pthread_attr_destroy"); +} + +#if defined(__linux__) && defined(__x86_64__) +static bool read_cpu_topology(struct lwan *l, uint32_t siblings[]) +{ + char path[PATH_MAX]; + + for (uint32_t i = 0; i < l->available_cpus; i++) + siblings[i] = 0xbebacafe; + + for (unsigned int i = 0; i < l->available_cpus; i++) { + FILE *sib; + uint32_t id, sibling; + char separator; + + snprintf(path, sizeof(path), + "/sys/devices/system/cpu/cpu%d/topology/thread_siblings_list", + i); + + sib = fopen(path, "re"); + if (!sib) { + lwan_status_warning("Could not open `%s` to determine CPU topology", + path); + return false; + } + + switch (fscanf(sib, "%u%c%u", &id, &separator, &sibling)) { + case 2: /* No SMT */ + siblings[i] = id; + break; + case 3: /* SMT */ + if (!(separator == ',' || separator == '-')) { + lwan_status_critical("Expecting either ',' or '-' for sibling separator"); + __builtin_unreachable(); + } + + siblings[i] = sibling; + break; + default: + lwan_status_critical("%s has invalid format", path); + __builtin_unreachable(); + } + + fclose(sib); + } + + /* Perform some validation here, as some systems seem to filter out the + * result of sysconf() to obtain the number of configured and online + * CPUs but don't bother changing what's available through sysfs as far + * as the CPU topology information goes. It's better to fall back to a + * possibly non-optimal setup than just crash during startup while + * trying to perform an out-of-bounds array access. */ + for (unsigned int i = 0; i < l->available_cpus; i++) { + if (siblings[i] == 0xbebacafe) { + lwan_status_warning("Could not determine sibling for CPU %d", i); + return false; + } + + if (siblings[i] >= l->available_cpus) { + lwan_status_warning("CPU information topology says CPU %d exists, " + "but max available CPUs is %d (online CPUs: %d). " + "Is Lwan running in a (broken) container?", + siblings[i], l->available_cpus, l->online_cpus); + return false; + } + } + + return true; +} + +static void +siblings_to_schedtbl(struct lwan *l, uint32_t siblings[], uint32_t schedtbl[]) +{ + int32_t *seen = calloc(l->available_cpus, sizeof(int32_t)); + unsigned int n_schedtbl = 0; + + if (!seen) + lwan_status_critical("Could not allocate the seen array"); + + for (uint32_t i = 0; i < l->available_cpus; i++) + seen[i] = -1; + + for (uint32_t i = 0; i < l->available_cpus; i++) { + if (seen[siblings[i]] < 0) { + seen[siblings[i]] = (int32_t)i; + } else { + schedtbl[n_schedtbl++] = (uint32_t)seen[siblings[i]]; + schedtbl[n_schedtbl++] = i; + } + } + + for (uint32_t i = 0; i < l->available_cpus && n_schedtbl < l->available_cpus; i++) { + if (seen[i] == -1) { + schedtbl[n_schedtbl++] = i; + } + } + + free(seen); +} + +static bool +topology_to_schedtbl(struct lwan *l, uint32_t schedtbl[], uint32_t n_threads) +{ + uint32_t *siblings = calloc(l->available_cpus, sizeof(uint32_t)); + bool ret = false; + + if (siblings) { + if (read_cpu_topology(l, siblings)) { + siblings_to_schedtbl(l, siblings, schedtbl); + ret = true; + } else { + for (uint32_t i = 0; i < n_threads; i++) + schedtbl[i] = (i / 2) % l->thread.count; + } + + free(siblings); + } + + return ret; +} + +static void +adjust_thread_affinity(const struct lwan_thread *thread) +{ + cpu_set_t set; + + CPU_ZERO(&set); + CPU_SET(thread->cpu, &set); + + if (pthread_setaffinity_np(thread->self, sizeof(set), &set)) + lwan_status_warning("Could not set thread affinity"); +} +#else +#define adjust_thread_affinity(...) +#endif + +#if defined(LWAN_HAVE_MBEDTLS) +static bool is_tls_ulp_supported(void) +{ + FILE *available_ulp = fopen("/proc/sys/net/ipv4/tcp_available_ulp", "re"); + char buffer[512]; + bool available = false; + + if (!available_ulp) + return false; + + if (fgets(buffer, 512, available_ulp)) { + if (strstr(buffer, "tls")) + available = true; + } + + fclose(available_ulp); + return available; +} + +static bool lwan_init_tls(struct lwan *l) +{ + static const int aes128_ciphers[] = { + /* Only allow Ephemeral Diffie-Hellman key exchange, so Perfect + * Forward Secrecy is possible. */ + MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + MBEDTLS_TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, + MBEDTLS_TLS_DHE_PSK_WITH_AES_128_GCM_SHA256, + + /* FIXME: Other ciphers are supported by kTLS, notably AES256 and + * ChaCha20-Poly1305. Add those here and patch + * lwan_setup_tls_keys() to match. */ + + /* FIXME: Maybe allow this to be user-tunable like other servers do? */ + 0, + }; + int r; + + if (!l->config.ssl.cert || !l->config.ssl.key) + return false; + + if (!is_tls_ulp_supported()) { + lwan_status_critical( + "TLS ULP not loaded. Try running `modprobe tls` as root."); + } + + l->tls = calloc(1, sizeof(*l->tls)); + if (!l->tls) + lwan_status_critical("Could not allocate memory for SSL context"); + + lwan_status_debug("Initializing mbedTLS"); + + mbedtls_ssl_config_init(&l->tls->config); + mbedtls_x509_crt_init(&l->tls->server_cert); + mbedtls_pk_init(&l->tls->server_key); + mbedtls_entropy_init(&l->tls->entropy); + mbedtls_ctr_drbg_init(&l->tls->ctr_drbg); + + r = mbedtls_x509_crt_parse_file(&l->tls->server_cert, l->config.ssl.cert); + if (r) { + lwan_status_mbedtls_error(r, "Could not parse certificate at %s", + l->config.ssl.cert); + abort(); + } + + r = mbedtls_pk_parse_keyfile(&l->tls->server_key, l->config.ssl.key, NULL); + if (r) { + lwan_status_mbedtls_error(r, "Could not parse key file at %s", + l->config.ssl.key); + abort(); + } + + /* Even though this points to files that will probably be outside + * the reach of the server (if straightjackets are used), wipe this + * struct to get rid of the paths to these files. */ + lwan_always_bzero(l->config.ssl.cert, strlen(l->config.ssl.cert)); + free(l->config.ssl.cert); + lwan_always_bzero(l->config.ssl.key, strlen(l->config.ssl.key)); + free(l->config.ssl.key); + lwan_always_bzero(&l->config.ssl, sizeof(l->config.ssl)); + + mbedtls_ssl_conf_ca_chain(&l->tls->config, l->tls->server_cert.next, NULL); + r = mbedtls_ssl_conf_own_cert(&l->tls->config, &l->tls->server_cert, + &l->tls->server_key); + if (r) { + lwan_status_mbedtls_error(r, "Could not set cert/key"); + abort(); + } + + r = mbedtls_ctr_drbg_seed(&l->tls->ctr_drbg, mbedtls_entropy_func, + &l->tls->entropy, NULL, 0); + if (r) { + lwan_status_mbedtls_error(r, "Could not seed ctr_drbg"); + abort(); + } + + r = mbedtls_ssl_config_defaults(&l->tls->config, MBEDTLS_SSL_IS_SERVER, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT); + if (r) { + lwan_status_mbedtls_error(r, "Could not set mbedTLS default config"); + abort(); + } + + mbedtls_ssl_conf_rng(&l->tls->config, mbedtls_ctr_drbg_random, + &l->tls->ctr_drbg); + mbedtls_ssl_conf_ciphersuites(&l->tls->config, aes128_ciphers); + + mbedtls_ssl_conf_renegotiation(&l->tls->config, + MBEDTLS_SSL_RENEGOTIATION_DISABLED); + mbedtls_ssl_conf_legacy_renegotiation(&l->tls->config, + MBEDTLS_SSL_LEGACY_NO_RENEGOTIATION); + +#if defined(MBEDTLS_SSL_ALPN) + static const char *alpn_protos[] = {"http/1.1", NULL}; + mbedtls_ssl_conf_alpn_protocols(&l->tls->config, alpn_protos); +#endif + + return true; +} +#endif + +void lwan_thread_init(struct lwan *l) +{ + const unsigned int total_conns = l->thread.max_fd * l->thread.count; +#if defined(LWAN_HAVE_MBEDTLS) + const bool tls_initialized = lwan_init_tls(l); +#else + const bool tls_initialized = false; +#endif + + lwan_status_debug("Initializing threads"); + + l->thread.threads = + calloc((size_t)l->thread.count, sizeof(struct lwan_thread)); + if (!l->thread.threads) + lwan_status_critical("Could not allocate memory for threads"); + + for (unsigned int i = 0; i < l->thread.count; i++) + l->thread.threads[i].cpu = UINT_MAX; + + uint32_t *schedtbl; + +#if defined(__x86_64__) && defined(__linux__) + if (l->online_cpus > 1) { + static_assert(sizeof(struct lwan_connection) == 32, + "Two connections per cache line"); +#ifdef _SC_LEVEL1_DCACHE_LINESIZE + assert(sysconf(_SC_LEVEL1_DCACHE_LINESIZE) == 64); +#endif + lwan_status_debug("%d CPUs of %d are online. " + "Reading topology to pre-schedule clients", + l->online_cpus, l->available_cpus); + /* + * Pre-schedule each file descriptor, to reduce some operations in the + * fast path. + * + * Since struct lwan_connection is guaranteed to be 32-byte long, two of + * them can fill up a cache line. Assume siblings share cache lines and + * use the CPU topology to group two connections per cache line in such + * a way that false sharing is avoided. + */ + schedtbl = calloc(l->thread.count, sizeof(uint32_t)); + bool adjust_affinity = topology_to_schedtbl(l, schedtbl, l->thread.count); + + for (unsigned int i = 0; i < total_conns; i++) { + unsigned int thread_id = schedtbl[i % l->thread.count]; + l->conns[i].thread = &l->thread.threads[thread_id]; + } + + if (!adjust_affinity) { + free(schedtbl); + schedtbl = NULL; + } + } else +#endif /* __x86_64__ && __linux__ */ + { + lwan_status_debug("Using round-robin to preschedule clients"); + + for (unsigned int i = 0; i < l->thread.count; i++) + l->thread.threads[i].cpu = i % l->online_cpus; + for (unsigned int i = 0; i < total_conns; i++) + l->conns[i].thread = &l->thread.threads[i % l->thread.count]; + + schedtbl = NULL; + } + + for (unsigned int i = 0; i < l->thread.count; i++) { + struct lwan_thread *thread; + + if (schedtbl) { + /* For SO_ATTACH_REUSEPORT_CBPF to work with the program + * we provide the kernel, sockets have to be added to the + * reuseport group in an order consistent with the + * CPU ID (SKF_AD_CPU field): so group the threads + * according to the CPU topology to avoid false sharing + * the connections array, and pin the N-th thread to the + * N-th CPU. */ + + /* FIXME: I don't know why this isn't working as I intended: + * clients are still accepted by a thread that's not the + * worker thread that's supposed to handle that particular + * file descriptor. According to socket(7), the plain + * SO_REUSEPORT mechanism might be used if the returned + * index is wrong, so maybe that's what's happening? I don't + * know, gotta debug the kernel to figure this out. */ + thread = &l->thread.threads[schedtbl[i]]; + + /* FIXME: figure out which CPUs are actually online */ + thread->cpu = i; + } else { + thread = &l->thread.threads[i]; + } + + if (pthread_barrier_init(&l->thread.barrier, NULL, 2)) + lwan_status_critical("Could not create barrier"); + + create_thread(l, thread); + + if ((thread->listen_fd = create_listen_socket(thread, i, false)) < 0) + lwan_status_critical_perror("Could not create listening socket"); + l->conns[thread->listen_fd].flags |= CONN_LISTENER; + + if (tls_initialized) { + if ((thread->tls_listen_fd = create_listen_socket(thread, i, true)) < 0) + lwan_status_critical_perror("Could not create TLS listening socket"); + l->conns[thread->tls_listen_fd].flags |= CONN_LISTENER | CONN_TLS; + } else { + thread->tls_listen_fd = -1; + } + + if (schedtbl) + adjust_thread_affinity(thread); + + pthread_barrier_wait(&l->thread.barrier); + } + + lwan_status_debug("Worker threads created and ready to serve"); + + free(schedtbl); +} + +void lwan_thread_shutdown(struct lwan *l) +{ + lwan_status_debug("Shutting down threads"); + + for (unsigned int i = 0; i < l->thread.count; i++) { + struct lwan_thread *t = &l->thread.threads[i]; + int epoll_fd = t->epoll_fd; + int listen_fd = t->listen_fd; + + t->listen_fd = -1; + t->epoll_fd = -1; + close(epoll_fd); + close(listen_fd); + } + + pthread_barrier_wait(&l->thread.barrier); + pthread_barrier_destroy(&l->thread.barrier); + + for (unsigned int i = 0; i < l->thread.count; i++) { + struct lwan_thread *t = &l->thread.threads[i]; + + pthread_join(l->thread.threads[i].self, NULL); + timeouts_close(t->wheel); + } + + free(l->thread.threads); + +#if defined(LWAN_HAVE_MBEDTLS) + if (l->tls) { + mbedtls_ssl_config_free(&l->tls->config); + mbedtls_x509_crt_free(&l->tls->server_cert); + mbedtls_pk_free(&l->tls->server_key); + mbedtls_entropy_free(&l->tls->entropy); + mbedtls_ctr_drbg_free(&l->tls->ctr_drbg); + free(l->tls); + } +#endif +} diff --git a/src/lib/lwan-time.c b/src/lib/lwan-time.c new file mode 100644 index 000000000..8f5b163bc --- /dev/null +++ b/src/lib/lwan-time.c @@ -0,0 +1,159 @@ +/* + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include "lwan-private.h" +#include "int-to-str.h" + +static int parse_2_digit_num_no_end_check(const char *str, unsigned int max) +{ + static const unsigned int tens[16] = { + 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, [11 ... 15] = UINT_MAX, + }; + const unsigned int n_tens = (unsigned int)(str[0] - '0'); + const unsigned int n_ones = (unsigned int)(str[1] - '0'); + const unsigned int val = tens[n_tens & 15] + n_ones; + return UNLIKELY(val > max) ? -EINVAL : (int)val; +} + +static int +parse_2_digit_num(const char *str, const char end_chr, unsigned int max) +{ + if (UNLIKELY(str[2] != end_chr)) + return -EINVAL; + return parse_2_digit_num_no_end_check(str, max); +} + +int lwan_parse_rfc_time(const char in[static 30], time_t *out) +{ + /* This function is used instead of strptime() because locale + * information can affect the parsing. Instead of defining + * the locale to "C", use hardcoded constants. */ + struct tm tm; + const char *str = in; + + STRING_SWITCH(str) { + case STR4_INT('S','u','n',','): tm.tm_wday = 0; break; + case STR4_INT('M','o','n',','): tm.tm_wday = 1; break; + case STR4_INT('T','u','e',','): tm.tm_wday = 2; break; + case STR4_INT('W','e','d',','): tm.tm_wday = 3; break; + case STR4_INT('T','h','u',','): tm.tm_wday = 4; break; + case STR4_INT('F','r','i',','): tm.tm_wday = 5; break; + case STR4_INT('S','a','t',','): tm.tm_wday = 6; break; + default: return -EINVAL; + } + str += 5; + + tm.tm_mday = parse_2_digit_num(str, ' ', 31); + if (UNLIKELY(tm.tm_mday <= 0)) + return -EINVAL; + str += 3; + + STRING_SWITCH(str) { + case STR4_INT('J','a','n',' '): tm.tm_mon = 0; break; + case STR4_INT('F','e','b',' '): tm.tm_mon = 1; break; + case STR4_INT('M','a','r',' '): tm.tm_mon = 2; break; + case STR4_INT('A','p','r',' '): tm.tm_mon = 3; break; + case STR4_INT('M','a','y',' '): tm.tm_mon = 4; break; + case STR4_INT('J','u','n',' '): tm.tm_mon = 5; break; + case STR4_INT('J','u','l',' '): tm.tm_mon = 6; break; + case STR4_INT('A','u','g',' '): tm.tm_mon = 7; break; + case STR4_INT('S','e','p',' '): tm.tm_mon = 8; break; + case STR4_INT('O','c','t',' '): tm.tm_mon = 9; break; + case STR4_INT('N','o','v',' '): tm.tm_mon = 10; break; + case STR4_INT('D','e','c',' '): tm.tm_mon = 11; break; + default: return -EINVAL; + } + str += 4; + + int year_hundreds = parse_2_digit_num_no_end_check(str, 21); + int year_ones = parse_2_digit_num_no_end_check(str + 2, 99); + if (UNLIKELY(year_hundreds < 0 || year_ones < 0)) + return -EINVAL; + tm.tm_year = (year_hundreds * 100 + year_ones) - 1900; + if (UNLIKELY(tm.tm_year < 0 || tm.tm_year > 1000)) + return -EINVAL; + str += 5; + + tm.tm_hour = parse_2_digit_num(str, ':', 23); + str += 3; + tm.tm_min = parse_2_digit_num(str, ':', 59); + str += 3; + tm.tm_sec = parse_2_digit_num(str, ' ', 59); + str += 3; + + STRING_SWITCH(str) { + case STR4_INT('G','M','T','\0'): + tm.tm_isdst = -1; + + *out = timegm(&tm); + + if (LIKELY(*out > 0)) + return 0; + + /* Fallthrough */ + default: + return -EINVAL; + } +} + +static inline char * +append_two_digits(char *p, unsigned int digits) +{ + return mempcpy(p, uint_to_string_2_digits(digits), 2); +} + +int lwan_format_rfc_time(const time_t in, char out[static 30]) +{ + static const char *weekdays = "Sun,Mon,Tue,Wed,Thu,Fri,Sat,"; + static const char *months = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec "; + struct tm tm; + char *p; + + if (UNLIKELY(!gmtime_r(&in, &tm))) + return -errno; + + p = mempcpy(out, weekdays + tm.tm_wday * 4, 4); + *p++ = ' '; + + p = append_two_digits(p, (unsigned int)tm.tm_mday); + *p++ = ' '; + p = mempcpy(p, months + tm.tm_mon * 4, 4); + + tm.tm_year += 1900; + p = append_two_digits(p, (unsigned int)tm.tm_year / 100); + p = append_two_digits(p, (unsigned int)tm.tm_year % 100); + + *p++ = ' '; + + p = append_two_digits(p, (unsigned int)tm.tm_hour); + *p++ = ':'; + p = append_two_digits(p, (unsigned int)tm.tm_min); + *p++ = ':'; + p = append_two_digits(p, (unsigned int)tm.tm_sec); + + memcpy(p, " GMT", 5); + + return 0; +} diff --git a/src/lib/lwan-tq.c b/src/lib/lwan-tq.c new file mode 100644 index 000000000..20e778886 --- /dev/null +++ b/src/lib/lwan-tq.c @@ -0,0 +1,136 @@ +/* + * lwan - web server + * Copyright (c) 2019 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include + +#include "lwan-private.h" +#include "lwan-tq.h" + +static inline int timeout_queue_node_to_idx(struct timeout_queue *tq, + struct lwan_connection *conn) +{ + return (conn == &tq->head) ? -1 : (int)(intptr_t)(conn - tq->conns); +} + +static inline struct lwan_connection * +timeout_queue_idx_to_node(struct timeout_queue *tq, int idx) +{ + return (idx < 0) ? &tq->head : &tq->conns[idx]; +} + +inline void timeout_queue_insert(struct timeout_queue *tq, + struct lwan_connection *new_node) +{ + assert(!(new_node->flags & (CONN_HUNG_UP | CONN_ASYNC_AWAIT))); + + new_node->next = -1; + new_node->prev = tq->head.prev; + struct lwan_connection *prev = timeout_queue_idx_to_node(tq, tq->head.prev); + tq->head.prev = prev->next = timeout_queue_node_to_idx(tq, new_node); +} + +static inline void timeout_queue_remove(struct timeout_queue *tq, + struct lwan_connection *node) +{ + struct lwan_connection *prev = timeout_queue_idx_to_node(tq, node->prev); + struct lwan_connection *next = timeout_queue_idx_to_node(tq, node->next); + + next->prev = node->prev; + prev->next = node->next; +} + +inline bool timeout_queue_empty(struct timeout_queue *tq) +{ + return tq->head.next < 0; +} + +inline void timeout_queue_move_to_last(struct timeout_queue *tq, + struct lwan_connection *conn) +{ + /* CONN_IS_KEEP_ALIVE isn't checked here because non-keep-alive connections + * are closed in the request processing coroutine after they have been + * served. In practice, if this is called, it's a keep-alive connection. */ + conn->time_to_expire = tq->current_time + tq->move_to_last_bump; + + timeout_queue_remove(tq, conn); + timeout_queue_insert(tq, conn); +} + +void timeout_queue_init(struct timeout_queue *tq, const struct lwan *lwan) +{ + *tq = (struct timeout_queue){ + .lwan = lwan, + .conns = lwan->conns, + .current_time = 0, + .move_to_last_bump = lwan->config.keep_alive_timeout, + .head.next = -1, + .head.prev = -1, + .timeout = (struct timeout){}, + }; +} + +void timeout_queue_expire(struct timeout_queue *tq, + struct lwan_connection *conn) +{ + assert(!(conn->flags & (CONN_HUNG_UP | CONN_ASYNC_AWAIT))); + + timeout_queue_remove(tq, conn); + + if (LIKELY(conn->coro)) { + coro_free(conn->coro); + conn->coro = NULL; + } + + close(lwan_connection_get_fd(tq->lwan, conn)); +} + +void timeout_queue_expire_waiting(struct timeout_queue *tq) +{ + tq->current_time++; + + while (!timeout_queue_empty(tq)) { + struct lwan_connection *conn = + timeout_queue_idx_to_node(tq, tq->head.next); + + if (conn->time_to_expire > tq->current_time) + return; + + if (conn->flags & CONN_IS_WEBSOCKET) { + if (LIKELY(lwan_send_websocket_ping_for_tq(conn))) { + timeout_queue_move_to_last(tq, conn); + continue; + } + } + + timeout_queue_expire(tq, conn); + } + + /* Timeout queue exhausted: reset epoch */ + tq->current_time = 0; +} + +void timeout_queue_expire_all(struct timeout_queue *tq) +{ + while (!timeout_queue_empty(tq)) { + struct lwan_connection *conn = + timeout_queue_idx_to_node(tq, tq->head.next); + timeout_queue_expire(tq, conn); + } +} diff --git a/src/lib/lwan-tq.h b/src/lib/lwan-tq.h new file mode 100644 index 000000000..bc42363d6 --- /dev/null +++ b/src/lib/lwan-tq.h @@ -0,0 +1,43 @@ +/* + * lwan - web server + * Copyright (c) 2019 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +struct timeout_queue { + const struct lwan *lwan; + struct lwan_connection *conns; + struct lwan_connection head; + struct timeout timeout; + unsigned int current_time; + unsigned int move_to_last_bump; +}; + +void timeout_queue_init(struct timeout_queue *tq, const struct lwan *l); + +void timeout_queue_insert(struct timeout_queue *tq, + struct lwan_connection *new_node); +void timeout_queue_expire(struct timeout_queue *tq, struct lwan_connection *node); +void timeout_queue_move_to_last(struct timeout_queue *tq, + struct lwan_connection *conn); + +void timeout_queue_expire_waiting(struct timeout_queue *tq); +void timeout_queue_expire_all(struct timeout_queue *tq); + +bool timeout_queue_empty(struct timeout_queue *tq); diff --git a/common/lwan-trie.c b/src/lib/lwan-trie.c similarity index 51% rename from common/lwan-trie.c rename to src/lib/lwan-trie.c index d943c278a..d26630ab6 100644 --- a/common/lwan-trie.c +++ b/src/lib/lwan-trie.c @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2012 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -14,16 +14,28 @@ * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. */ #include #include -#include "lwan.h" +#include "lwan-private.h" -bool -lwan_trie_init(lwan_trie_t *trie, void (*free_node)(void *data)) +struct lwan_trie_node { + struct lwan_trie_node *next[8]; + struct lwan_trie_leaf *leaf; + int ref_count; +}; + +struct lwan_trie_leaf { + char *key; + void *data; + struct lwan_trie_leaf *next; +}; + +bool lwan_trie_init(struct lwan_trie *trie, void (*free_node)(void *data)) { if (!trie) return false; @@ -32,10 +44,10 @@ lwan_trie_init(lwan_trie_t *trie, void (*free_node)(void *data)) return true; } -static ALWAYS_INLINE lwan_trie_leaf_t * -find_leaf_with_key(lwan_trie_node_t *node, const char *key, size_t len) +static ALWAYS_INLINE struct lwan_trie_leaf * +find_leaf_with_key(struct lwan_trie_node *node, const char *key, size_t len) { - lwan_trie_leaf_t *leaf = node->leaf; + struct lwan_trie_leaf *leaf = node->leaf; if (!leaf) return NULL; @@ -51,23 +63,23 @@ find_leaf_with_key(lwan_trie_node_t *node, const char *key, size_t len) return NULL; } -#define GET_NODE() \ - do { \ - if (!(node = *knode)) { \ - *knode = node = calloc(1, sizeof(*node)); \ - if (!node) \ - goto oom; \ - } \ - ++node->ref_count; \ - } while(0) - -void -lwan_trie_add(lwan_trie_t *trie, const char *key, void *data) +#define GET_NODE() \ + do { \ + if (!(node = *knode)) { \ + *knode = node = lwan_aligned_alloc(sizeof(*node), 64); \ + if (!node) \ + goto oom; \ + memset(node, 0, sizeof(*node)); \ + } \ + ++node->ref_count; \ + } while (0) + +void lwan_trie_add(struct lwan_trie *trie, const char *key, void *data) { if (UNLIKELY(!trie || !key || !data)) return; - lwan_trie_node_t **knode, *node; + struct lwan_trie_node **knode, *node; const char *orig_key = key; /* Traverse the trie, allocating nodes if necessary */ @@ -77,12 +89,15 @@ lwan_trie_add(lwan_trie_t *trie, const char *key, void *data) /* Get the leaf node (allocate it if necessary) */ GET_NODE(); - lwan_trie_leaf_t *leaf = find_leaf_with_key(node, orig_key, (size_t)(key - orig_key)); + struct lwan_trie_leaf *leaf = + find_leaf_with_key(node, orig_key, (size_t)(key - orig_key)); bool had_key = leaf; if (!leaf) { - leaf = malloc(sizeof(*leaf)); + leaf = lwan_aligned_alloc(sizeof(*leaf), 64); if (!leaf) lwan_status_critical_perror("malloc"); + } else if (trie->free_node) { + trie->free_node(leaf->data); } leaf->data = data; @@ -99,10 +114,10 @@ lwan_trie_add(lwan_trie_t *trie, const char *key, void *data) #undef GET_NODE -static ALWAYS_INLINE lwan_trie_node_t * -lookup_node(lwan_trie_node_t *root, const char *key, bool prefix, size_t *prefix_len) +static ALWAYS_INLINE struct lwan_trie_node * +lookup_node(struct lwan_trie_node *root, const char *key, size_t *prefix_len) { - lwan_trie_node_t *node, *previous_node = NULL; + struct lwan_trie_node *node, *previous_node = NULL; const char *orig_key = key; for (node = root; node && *key; node = node->next[(int)(*key++ & 7)]) { @@ -111,56 +126,42 @@ lookup_node(lwan_trie_node_t *root, const char *key, bool prefix, size_t *prefix } *prefix_len = (size_t)(key - orig_key); + if (node && node->leaf) return node; - if (prefix && previous_node) - return previous_node; - return NULL; -} + return previous_node; +} -ALWAYS_INLINE void * -lwan_trie_lookup_full(lwan_trie_t *trie, const char *key, bool prefix) +ALWAYS_INLINE void *lwan_trie_lookup_prefix(struct lwan_trie *trie, + const char *key) { - if (UNLIKELY(!trie)) - return NULL; + assert(trie); + assert(key); size_t prefix_len; - lwan_trie_node_t *node = lookup_node(trie->root, key, prefix, &prefix_len); - if (!node) - return NULL; - lwan_trie_leaf_t *leaf = find_leaf_with_key(node, key, prefix_len); - return leaf ? leaf->data : NULL; -} + struct lwan_trie_node *node = lookup_node(trie->root, key, &prefix_len); -ALWAYS_INLINE void * -lwan_trie_lookup_prefix(lwan_trie_t *trie, const char *key) -{ - return lwan_trie_lookup_full(trie, key, true); -} + if (node) { + struct lwan_trie_leaf *leaf = find_leaf_with_key(node, key, prefix_len); -ALWAYS_INLINE void * -lwan_trie_lookup_exact(lwan_trie_t *trie, const char *key) -{ - return lwan_trie_lookup_full(trie, key, false); -} + if (leaf) + return leaf->data; + } -ALWAYS_INLINE int32_t -lwan_trie_entry_count(lwan_trie_t *trie) -{ - return (trie && trie->root) ? trie->root->ref_count : 0; + return NULL; } -static void -lwan_trie_node_destroy(lwan_trie_t *trie, lwan_trie_node_t *node) +static void lwan_trie_node_destroy(struct lwan_trie *trie, + struct lwan_trie_node *node) { if (!node) return; int32_t nodes_destroyed = node->ref_count; - for (lwan_trie_leaf_t *leaf = node->leaf; leaf;) { - lwan_trie_leaf_t *tmp = leaf->next; + for (struct lwan_trie_leaf *leaf = node->leaf; leaf;) { + struct lwan_trie_leaf *tmp = leaf->next; if (trie->free_node) trie->free_node(leaf->data); @@ -180,8 +181,7 @@ lwan_trie_node_destroy(lwan_trie_t *trie, lwan_trie_node_t *node) free(node); } -void -lwan_trie_destroy(lwan_trie_t *trie) +void lwan_trie_destroy(struct lwan_trie *trie) { if (!trie || !trie->root) return; diff --git a/src/lib/lwan-trie.h b/src/lib/lwan-trie.h new file mode 100644 index 000000000..cd822bb52 --- /dev/null +++ b/src/lib/lwan-trie.h @@ -0,0 +1,38 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include +#include + +struct lwan_trie_node; + +struct lwan_trie { + struct lwan_trie_node *root; + void (*free_node)(void *data); +}; + +bool lwan_trie_init(struct lwan_trie *trie, void (*free_node)(void *data)); +void lwan_trie_destroy(struct lwan_trie *trie); + +void lwan_trie_add(struct lwan_trie *trie, const char *key, void *data); + +void *lwan_trie_lookup_prefix(struct lwan_trie *trie, const char *key); diff --git a/src/lib/lwan-websocket.c b/src/lib/lwan-websocket.c new file mode 100644 index 000000000..abeb64a4b --- /dev/null +++ b/src/lib/lwan-websocket.c @@ -0,0 +1,430 @@ +/* + * lwan - web server + * Copyright (c) 2019 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#if defined(__x86_64__) +#include +#endif + +#include "lwan-io-wrappers.h" +#include "lwan-private.h" + +enum ws_opcode { + WS_OPCODE_CONTINUATION = 0, + WS_OPCODE_TEXT = 1, + WS_OPCODE_BINARY = 2, + WS_OPCODE_CLOSE = 8, + WS_OPCODE_PING = 9, + WS_OPCODE_PONG = 10, + + WS_OPCODE_RSVD_1 = 3, + WS_OPCODE_RSVD_2 = 4, + WS_OPCODE_RSVD_3 = 5, + WS_OPCODE_RSVD_4 = 6, + WS_OPCODE_RSVD_5 = 7, + + WS_OPCODE_RSVD_CONTROL_1 = 11, + WS_OPCODE_RSVD_CONTROL_2 = 12, + WS_OPCODE_RSVD_CONTROL_3 = 13, + WS_OPCODE_RSVD_CONTROL_4 = 14, + WS_OPCODE_RSVD_CONTROL_5 = 15, + + WS_OPCODE_INVALID = 16, +}; + +#define WS_MASKED 0x80 + +static ALWAYS_INLINE bool +write_websocket_frame_full(struct lwan_request *request, + unsigned char header_byte, + char *msg, + size_t len, + bool use_coro) +{ + uint8_t frame[10] = {header_byte}; + size_t frame_len; + + if (len <= 125) { + frame[1] = (uint8_t)len; + frame_len = 2; + } else if (len <= 65535) { + frame[1] = 0x7e; + memcpy(frame + 2, &(uint16_t){htons((uint16_t)len)}, sizeof(uint16_t)); + frame_len = 4; + } else { + frame[1] = 0x7f; + memcpy(frame + 2, &(uint64_t){htobe64((uint64_t)len)}, + sizeof(uint64_t)); + frame_len = 10; + } + + struct iovec vec[] = { + {.iov_base = frame, .iov_len = frame_len}, + {.iov_base = msg, .iov_len = len}, + }; + + if (LIKELY(use_coro)) { + lwan_writev(request, vec, N_ELEMENTS(vec)); + return true; + } + + int curr_iov = 0; + for (int try = 0; try < 10; try++) { + ssize_t written = writev(request->fd, &vec[curr_iov], + (int)N_ELEMENTS(vec) - curr_iov); + if (written < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + return false; + } + + while (curr_iov < (int)N_ELEMENTS(vec) && + written >= (ssize_t)vec[curr_iov].iov_len) { + written -= (ssize_t)vec[curr_iov].iov_len; + curr_iov++; + } + if (curr_iov == (int)N_ELEMENTS(vec)) + return true; + + vec[curr_iov].iov_base = (char *)vec[curr_iov].iov_base + written; + vec[curr_iov].iov_len -= (size_t)written; + } + + return false; +} + +static bool write_websocket_frame(struct lwan_request *request, + unsigned char header_byte, + char *msg, + size_t len) +{ + return write_websocket_frame_full(request, header_byte, msg, len, true); +} + +static inline void lwan_response_websocket_write(struct lwan_request *request, + unsigned char op) +{ + size_t len = lwan_strbuf_get_length(request->response.buffer); + char *msg = lwan_strbuf_get_buffer(request->response.buffer); + unsigned char header = WS_MASKED | op; + + if (!(request->conn->flags & CONN_IS_WEBSOCKET)) + return; + + write_websocket_frame(request, header, msg, len); + lwan_strbuf_reset(request->response.buffer); +} + +void lwan_response_websocket_write_text(struct lwan_request *request) +{ + lwan_response_websocket_write(request, WS_OPCODE_TEXT); +} + +void lwan_response_websocket_write_binary(struct lwan_request *request) +{ + lwan_response_websocket_write(request, WS_OPCODE_BINARY); +} + +static size_t get_frame_length(struct lwan_request *request, uint16_t header) +{ + uint64_t len; + + switch (header & 0x7f) { + case 0x7e: + lwan_recv(request, &len, 2, 0); + len = (uint64_t)ntohs((uint16_t)len); + + if (len < 0x7e) { + lwan_status_warning( + "Can't use 16-bit encoding for frame length of %zu", len); + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + return (size_t)len; + case 0x7f: + lwan_recv(request, &len, 8, 0); + len = be64toh(len); + + if (UNLIKELY(len > SSIZE_MAX)) { + lwan_status_warning("Frame length of %zu won't fit a ssize_t", len); + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + if (UNLIKELY(len <= 0xffff)) { + lwan_status_warning( + "Can't use 64-bit encoding for frame length of %zu", len); + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + return (size_t)len; + default: + return (size_t)(header & 0x7f); + } +} + +static void unmask(char *msg, size_t msg_len, char mask[static 4]) +{ + uint32_t mask32; + + /* TODO: handle alignment of `msg` to use (at least) NT loads + * as we're rewriting msg anyway. (NT writes aren't that + * useful as the unmasked value will be used right after.) */ + +#if defined(__AVX2__) + if (msg_len >= 32) { + const __m256i mask256 = + _mm256_castps_si256(_mm256_broadcast_ss((const float *)mask)); + do { + const __m256i v = _mm256_lddqu_si256((const __m256i *)msg); + _mm256_storeu_si256((__m256i *)msg, _mm256_xor_si256(v, mask256)); + msg += 32; + msg_len -= 32; + } while (msg_len >= 32); + + if (msg_len >= 16) { + const __m128i mask128 = _mm256_extracti128_si256(mask256, 0); + const __m128i v = _mm_lddqu_si128((const __m128i *)msg); + _mm_storeu_si128((__m128i *)msg, _mm_xor_si128(v, mask128)); + msg += 16; + msg_len -= 16; + } + + mask32 = (uint32_t)_mm256_extract_epi32(mask256, 0); + } else { + mask32 = string_as_uint32(mask); + } +#elif defined(__SSE3__) + if (msg_len >= 16) { + const __m128i mask128 = + _mm_castps_si128(_mm_load_ps1((const float *)mask)); + + do { + const __m128i v = _mm_lddqu_si128((const __m128i *)msg); + _mm_storeu_si128((__m128i *)msg, _mm_xor_si128(v, mask128)); + msg += 16; + msg_len -= 16; + } while (msg_len >= 16); + + mask32 = _mm_extract_epi32(mask128, 0); + } else { + mask32 = string_as_uint32(mask); + } +#else + mask32 = string_as_uint32(mask); +#endif + +#if __SIZEOF_POINTER__ == 8 + if (msg_len >= 8) { + const uint64_t mask64 = (uint64_t)mask32 << 32 | (uint64_t)mask32; + + do { + uint64_t v = string_as_uint64(msg); + v ^= mask64; + msg = mempcpy(msg, &v, sizeof(v)); + msg_len -= 8; + } while (msg_len >= 8); + } +#endif + + if (msg_len >= 4) { + do { + uint32_t v = string_as_uint32(msg); + v ^= mask32; + msg = mempcpy(msg, &v, sizeof(v)); + msg_len -= 4; + } while (msg_len >= 4); + } + + switch (msg_len) { + case 3: + msg[2] ^= mask[2]; /* fallthrough */ + case 2: + msg[1] ^= mask[1]; /* fallthrough */ + case 1: + msg[0] ^= mask[0]; + break; + default: + __builtin_unreachable(); + } +} + +static void +ping_pong(struct lwan_request *request, uint16_t header, enum ws_opcode opcode) +{ + const size_t len = header & 0x7f; + char msg[4 + 128]; + + assert(header & WS_MASKED); + assert(opcode == WS_OPCODE_PING || opcode == WS_OPCODE_PONG); + + if (UNLIKELY(len > 125)) { + lwan_status_debug("Received %s frame with length %zu." + "Max is 125. Aborting connection.", + opcode == WS_OPCODE_PING ? "PING" : "PONG", len); + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + if (opcode == WS_OPCODE_PING) { + lwan_recv(request, msg, len + 4, 0); + unmask(msg + 4, len, msg); + write_websocket_frame(request, WS_MASKED | WS_OPCODE_PONG, msg, len); + } else { + /* From MDN: "You might also get a pong without ever sending a ping; + * ignore this if it happens." */ + + /* FIXME: should we care about the contents of PONG packets? */ + lwan_recv(request, msg, len + 4, MSG_TRUNC); + } +} + +bool lwan_send_websocket_ping_for_tq(struct lwan_connection *conn) +{ + uint32_t mask32 = (uint32_t)lwan_random_uint64(); + char mask[sizeof(mask32)]; + struct timespec payload; + + memcpy(mask, &mask32, sizeof(mask32)); + + if (UNLIKELY(clock_gettime(monotonic_clock_id, &payload) < 0)) + return false; + + unmask((char *)&payload, sizeof(payload), mask); + + /* use_coro is set to false here because this function is called outside + * a connection coroutine and the I/O wrappers might yield, which of course + * wouldn't work */ + struct lwan_request req = { + .conn = conn, + .fd = lwan_connection_get_fd(conn->thread->lwan, conn), + }; + return write_websocket_frame_full(&req, WS_MASKED | WS_OPCODE_PING, + (char *)&payload, sizeof(payload), false); +} + +int lwan_response_websocket_read_hint(struct lwan_request *request, + size_t size_hint) +{ + enum ws_opcode opcode = WS_OPCODE_INVALID; + enum ws_opcode last_opcode; + uint16_t header; + bool continuation = false; + + if (UNLIKELY(!(request->conn->flags & CONN_IS_WEBSOCKET))) + return ENOTCONN; + + lwan_strbuf_reset_trim(request->response.buffer, size_hint); + +next_frame: + last_opcode = opcode; + + ssize_t r = lwan_recv(request, &header, sizeof(header), + MSG_DONTWAIT | MSG_NOSIGNAL); + if (UNLIKELY(r < 0)) + return errno; + header = htons(header); + continuation = false; + + if (UNLIKELY(header & 0x7000)) { + lwan_status_debug("RSV1...RSV3 has non-zero value %d, aborting", + header & 0x7000); + /* No extensions are supported yet, so fail connection per RFC6455. */ + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + if (UNLIKELY(!(header & WS_MASKED))) { + lwan_status_debug("Client sent an unmasked WebSockets frame, aborting"); + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + opcode = (header & 0x0f00) >> 8; + switch (opcode) { + case WS_OPCODE_CONTINUATION: + if (UNLIKELY(last_opcode > WS_OPCODE_BINARY)) { + /* Continuation frames are only available for opcodes [0..2] */ + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + continuation = true; + break; + + case WS_OPCODE_TEXT: + case WS_OPCODE_BINARY: + break; + + case WS_OPCODE_CLOSE: + request->conn->flags &= ~CONN_IS_WEBSOCKET; + break; + + case WS_OPCODE_PONG: + case WS_OPCODE_PING: + ping_pong(request, header, opcode); + goto next_frame; + + case WS_OPCODE_RSVD_1 ... WS_OPCODE_RSVD_5: + case WS_OPCODE_RSVD_CONTROL_1 ... WS_OPCODE_RSVD_CONTROL_5: + case WS_OPCODE_INVALID: + /* RFC6455: ...the receiving endpoint MUST _Fail the WebSocket + * Connection_ */ + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + size_t frame_len = get_frame_length(request, header); + char *msg = lwan_strbuf_extend_unsafe(request->response.buffer, frame_len); + if (UNLIKELY(!msg)) { + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); + } + + char mask[4]; + struct iovec vec[] = { + {.iov_base = mask, .iov_len = sizeof(mask)}, + {.iov_base = msg, .iov_len = frame_len}, + }; + lwan_readv(request, vec, N_ELEMENTS(vec)); + unmask(msg, frame_len, mask); + + if (continuation && !(header & 0x8000)) + goto next_frame; + + return (request->conn->flags & CONN_IS_WEBSOCKET) ? 0 : ECONNRESET; +} + +inline int lwan_response_websocket_read(struct lwan_request *request) +{ + /* Ensure that a rogue client won't keep increasing the memory usage in an + * uncontrolled manner by curbing the backing store to 1KB at most by + * default. If an application expects messages to be larger than 1024 bytes + * on average, they can call lwan_response_websocket_read_hint() directly + * with a larger value to avoid malloc chatter (things should still work, + * but will be slightly more inefficient). */ + return lwan_response_websocket_read_hint(request, 1024); +} diff --git a/src/lib/lwan.c b/src/lib/lwan.c new file mode 100644 index 000000000..3fc2ed154 --- /dev/null +++ b/src/lib/lwan.c @@ -0,0 +1,1041 @@ +/* + * lwan - web server + * Copyright (c) 2012, 2013 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#include "lwan-config.h" +#include "lwan-http-authorize.h" + +#if defined(LWAN_HAVE_LUA) +#include "lwan-lua.h" +#endif + +/* Ideally, this would check if all items in enum lwan_request_flags, + * when bitwise-or'd together, would not have have any bit set that + * is also set in REQUEST_METHOD_MASK. */ +static_assert(REQUEST_ACCEPT_DEFLATE > REQUEST_METHOD_MASK, + "enough bits to store request methods"); + +/* See detect_fastest_monotonic_clock() */ +clockid_t monotonic_clock_id = CLOCK_MONOTONIC; + +static const struct lwan_config default_config = { + .listener = "localhost:8080", + .keep_alive_timeout = 15, + .quiet = false, + .proxy_protocol = false, + .allow_cors = false, + .expires = 1 * ONE_WEEK, + .n_threads = 0, + .request_buffer_size = DEFAULT_BUFFER_SIZE, + .max_post_data_size = 10 * DEFAULT_BUFFER_SIZE, + .allow_post_temp_file = false, + .max_put_data_size = 10 * DEFAULT_BUFFER_SIZE, + .allow_put_temp_file = false, + .max_file_descriptors = 524288, +}; + +LWAN_HANDLER_ROUTE(brew_coffee, NULL /* do not autodetect this route */) +{ + /* Placeholder handler so that __start_lwan_handler and __stop_lwan_handler + * symbols will get defined. + */ + return HTTP_I_AM_A_TEAPOT; +} + +__attribute__((no_sanitize_address)) +static void *find_handler(const char *name) +{ + const struct lwan_handler_info *handler; + + LWAN_SECTION_FOREACH(lwan_handler, handler) { + if (streq(handler->name, name)) + return handler->handler; + } + + return NULL; +} + +__attribute__((no_sanitize_address)) +static const struct lwan_module *find_module(const char *name) +{ + const struct lwan_module_info *module; + + LWAN_SECTION_FOREACH(lwan_module, module) { + if (streq(module->name, name)) + return module->module; + } + + return NULL; +} + +static void destroy_urlmap(void *data) +{ + struct lwan_url_map *url_map = data; + + if (url_map->module) { + const struct lwan_module *module = url_map->module; + + if (module->destroy) + module->destroy(url_map->data); + } else if (url_map->data && url_map->flags & HANDLER_DATA_IS_HASH_TABLE) { + hash_unref(url_map->data); + } + + free(url_map->authorization.realm); + free(url_map->authorization.password_file); + free((char *)url_map->prefix); + free(url_map); +} + +static struct lwan_url_map *add_url_map(struct lwan_trie *t, const char *prefix, + const struct lwan_url_map *map) +{ + struct lwan_url_map *copy = malloc(sizeof(*copy)); + + if (!copy) + lwan_status_critical_perror("Could not copy URL map"); + + memcpy(copy, map, sizeof(*copy)); + + copy->prefix = strdup(prefix ? prefix : copy->prefix); + if (!copy->prefix) + lwan_status_critical_perror("Could not copy URL prefix"); + + copy->prefix_len = strlen(copy->prefix); + lwan_trie_add(t, copy->prefix, copy); + + return copy; +} + +static bool can_override_header(const char *name) +{ + /* NOTE: Update lwan_prepare_response_header_full() in lwan-response.c + * if new headers are added here. */ + + if (strcaseequal_neutral(name, "Date")) + return false; + if (strcaseequal_neutral(name, "Expires")) + return false; + if (strcaseequal_neutral(name, "WWW-Authenticate")) + return false; + if (strcaseequal_neutral(name, "Connection")) + return false; + if (strcaseequal_neutral(name, "Content-Type")) + return false; + if (strcaseequal_neutral(name, "Transfer-Encoding")) + return false; + if (strcaseequal_neutral_len(name, "Access-Control-Allow-", + sizeof("Access-Control-Allow-") - 1)) + return false; + + return true; +} + +static void build_response_headers(struct lwan *l, + const struct lwan_key_value *kv) +{ + struct lwan_strbuf strbuf; + bool set_server = false; + + assert(l); + + lwan_strbuf_init(&strbuf); + + for (; kv && kv->key; kv++) { + if (!can_override_header(kv->key)) { + lwan_status_warning("Cannot override header '%s'", kv->key); + } else { + if (strcaseequal_neutral(kv->key, "Server")) + set_server = true; + + lwan_strbuf_append_printf(&strbuf, "\r\n%s: %s", kv->key, + kv->value); + } + } + + if (!set_server) + lwan_strbuf_append_strz(&strbuf, "\r\nServer: lwan"); + + lwan_strbuf_append_strz(&strbuf, "\r\n\r\n"); + + l->headers = lwan_strbuf_to_value(&strbuf); +} + +static void parse_global_headers(struct config *c, + struct lwan *lwan) +{ + struct lwan_key_value_array hdrs; + const struct config_line *l; + struct lwan_key_value *kv; + + lwan_key_value_array_init(&hdrs); + + while ((l = config_read_line(c))) { + switch (l->type) { + case CONFIG_LINE_TYPE_SECTION: + config_error( + c, "No sections are supported under the 'headers' section"); + goto cleanup; + + case CONFIG_LINE_TYPE_LINE: + kv = lwan_key_value_array_append(&hdrs); + if (!kv) { + lwan_status_critical_perror( + "Could not allocate memory for custom response header"); + } + + kv->key = strdup(l->key); + if (!kv->key) { + lwan_status_critical_perror( + "Could not allocate memory for custom response header"); + } + + kv->value = strdup(l->value); + if (!kv->value) { + lwan_status_critical_perror( + "Could not allocate memory for custom response header"); + } + break; + + case CONFIG_LINE_TYPE_SECTION_END: + kv = lwan_key_value_array_append(&hdrs); + if (!kv) { + lwan_status_critical_perror( + "Could not allocate memory for custom response header"); + } + + kv->key = NULL; + kv->value = NULL; + + build_response_headers(lwan, lwan_key_value_array_get_array(&hdrs)); + goto cleanup; + } + } + + config_error(c, "EOF while looking for end of 'headers' section"); + +cleanup: + LWAN_ARRAY_FOREACH (&hdrs, kv) { + free(kv->key); + free(kv->value); + } + lwan_key_value_array_reset(&hdrs); +} + +static void parse_listener_prefix_authorization(struct config *c, + const struct config_line *l, + struct lwan_url_map *url_map) +{ + if (!streq(l->value, "basic")) { + config_error(c, "Only basic authorization supported"); + return; + } + + memset(&url_map->authorization, 0, sizeof(url_map->authorization)); + + while ((l = config_read_line(c))) { + switch (l->type) { + case CONFIG_LINE_TYPE_LINE: + if (streq(l->key, "realm")) { + free(url_map->authorization.realm); + url_map->authorization.realm = strdup(l->value); + } else if (streq(l->key, "password_file")) { + free(url_map->authorization.password_file); + url_map->authorization.password_file = realpath(l->value, NULL); + if (!url_map->authorization.password_file) + config_error(c, "Could not determine full path for password file: %s", l->value); + } + break; + + case CONFIG_LINE_TYPE_SECTION: + config_error(c, "Unexpected section: %s", l->key); + goto error; + + case CONFIG_LINE_TYPE_SECTION_END: + if (!url_map->authorization.realm) + url_map->authorization.realm = strdup("Lwan"); + if (!url_map->authorization.password_file) + url_map->authorization.password_file = strdup("htpasswd"); + + url_map->flags |= HANDLER_MUST_AUTHORIZE; + return; + } + } + + config_error(c, "Could not find end of authorization section"); + +error: + free(url_map->authorization.realm); + free(url_map->authorization.password_file); +} + +__attribute__((no_sanitize_address)) +static const char *get_module_name(const struct lwan_module *module) +{ + const struct lwan_module_info *iter; + + LWAN_SECTION_FOREACH(lwan_module, iter) { + if (iter->module == module) + return iter->name; + } + + return ""; +} + +static void parse_listener_prefix(struct config *c, + const struct config_line *l, + struct lwan *lwan, + const struct lwan_module *module, + void *handler) +{ + struct lwan_url_map url_map = {}; + struct hash *hash = hash_str_new(free, free); + char *prefix = strdupa(l->value); + struct config *isolated; + + if (!hash) + lwan_status_critical("Could not allocate hash table"); + + isolated = config_isolate_section(c, l); + if (!isolated) { + config_error(c, "Could not isolate configuration file"); + goto out; + } + + while ((l = config_read_line(c))) { + switch (l->type) { + case CONFIG_LINE_TYPE_LINE: { + char *key_copy = strdup(l->key); + char *value_copy = strdup(l->value); + + if (!key_copy) + lwan_status_critical("Could not copy key from config file"); + if (!value_copy) + lwan_status_critical("Could not copy value from config file"); + + hash_add(hash, key_copy, value_copy); + break; + } + + case CONFIG_LINE_TYPE_SECTION: + if (streq(l->key, "authorization")) { + parse_listener_prefix_authorization(c, l, &url_map); + } else if (!config_skip_section(c, l)) { + config_error(c, "Could not skip section"); + goto out; + } + break; + + case CONFIG_LINE_TYPE_SECTION_END: + goto add_map; + } + } + + config_error(c, "Expecting section end while parsing prefix"); + goto out; + +add_map: + assert((handler && !module) || (!handler && module)); + + if (handler) { + url_map.handler = handler; + url_map.flags |= HANDLER_PARSE_MASK | HANDLER_DATA_IS_HASH_TABLE; + url_map.data = hash; + url_map.module = NULL; + + hash = NULL; + } else if (module->create_from_hash && module->handle_request) { + lwan_status_debug("Initializing module %s from config", + get_module_name(module)); + + url_map.data = module->create_from_hash(prefix, hash); + if (!url_map.data) { + config_error(c, "Could not create module instance"); + goto out; + } + + if (module->parse_conf && !module->parse_conf(url_map.data, isolated)) { + const char *msg = config_last_error(isolated); + + config_error(c, "Error from module: %s", msg ? msg : "Unknown"); + goto out; + } + + url_map.handler = module->handle_request; + url_map.flags |= module->flags; + url_map.module = module; + } else if (UNLIKELY(!module->create_from_hash)) { + config_error(c, "Module isn't prepared to load settings from a file; " + "create_from_hash() method isn't present"); + goto out; + } else if (UNLIKELY(!module->handle_request)) { + config_error(c, "Module does not have handle_request() method"); + goto out; + } + + add_url_map(&lwan->url_map_trie, prefix, &url_map); + +out: + hash_unref(hash); + config_close(isolated); +} + +static void register_url_map(struct lwan *l, const struct lwan_url_map *map) +{ + struct lwan_url_map *copy = add_url_map(&l->url_map_trie, NULL, map); + + if (copy->module && copy->module->create) { + lwan_status_debug("Initializing module %s from struct", + get_module_name(copy->module)); + + copy->data = copy->module->create(map->prefix, copy->args); + if (!copy->data) { + lwan_status_critical("Could not initialize module %s", + get_module_name(copy->module)); + } + + copy->flags = copy->module->flags; + copy->handler = copy->module->handle_request; + } else { + copy->flags = HANDLER_PARSE_MASK; + } +} + +void lwan_set_url_map(struct lwan *l, const struct lwan_url_map *map) +{ + lwan_trie_destroy(&l->url_map_trie); + if (UNLIKELY(!lwan_trie_init(&l->url_map_trie, destroy_urlmap))) + lwan_status_critical_perror("Could not initialize trie"); + + for (; map->prefix; map++) + register_url_map(l, map); +} + +__attribute__((no_sanitize_address)) +void lwan_detect_url_map(struct lwan *l) +{ + const struct lwan_handler_info *iter; + + lwan_trie_destroy(&l->url_map_trie); + if (UNLIKELY(!lwan_trie_init(&l->url_map_trie, destroy_urlmap))) + lwan_status_critical_perror("Could not initialize trie"); + + LWAN_SECTION_FOREACH(lwan_handler, iter) { + if (!iter->route) + continue; + + lwan_status_debug("Using handler `%s' for route `%s'", + iter->name, iter->route); + + const struct lwan_url_map map = {.prefix = iter->route, + .handler = iter->handler, + .flags = HANDLER_PARSE_MASK}; + register_url_map(l, &map); + } +} + +const char *lwan_get_config_path(char *path_buf, size_t path_buf_len) +{ + char buffer[PATH_MAX]; + + if (proc_pidpath(getpid(), buffer, sizeof(buffer)) < 0) + goto out; + + char *path = strrchr(buffer, '/'); + if (!path) + goto out; + int ret = snprintf(path_buf, path_buf_len, "%s.conf", path + 1); + if (ret < 0 || ret >= (int)path_buf_len) + goto out; + + return path_buf; + +out: + return "lwan.conf"; +} + +static void parse_tls_listener(struct config *conf, const struct config_line *line, struct lwan *lwan) +{ +#if !defined(LWAN_HAVE_MBEDTLS) + config_error(conf, "Lwan has been built without mbedTLS support"); + return; +#endif + + lwan->config.tls_listener = strdup(line->value); + if (!lwan->config.tls_listener) { + config_error(conf, "Could not allocate memory for tls_listener"); + return; + } + + lwan->config.ssl.cert = NULL; + lwan->config.ssl.key = NULL; + + while ((line = config_read_line(conf))) { + switch (line->type) { + case CONFIG_LINE_TYPE_SECTION_END: + if (!lwan->config.ssl.cert) + config_error(conf, "Missing path to certificate"); + if (!lwan->config.ssl.key) + config_error(conf, "Missing path to private key"); + return; + case CONFIG_LINE_TYPE_SECTION: + config_error(conf, "Unexpected section: %s", line->key); + return; + case CONFIG_LINE_TYPE_LINE: + if (streq(line->key, "cert")) { + free(lwan->config.ssl.cert); + lwan->config.ssl.cert = strdup(line->value); + if (!lwan->config.ssl.cert) + return lwan_status_critical("Could not copy string"); + } else if (streq(line->key, "key")) { + free(lwan->config.ssl.key); + lwan->config.ssl.key = strdup(line->value); + if (!lwan->config.ssl.key) + return lwan_status_critical("Could not copy string"); + } else if (streq(line->key, "hsts")) { + lwan->config.ssl.send_hsts_header = parse_bool(line->value, false); + } else { + config_error(conf, "Unexpected key: %s", line->key); + } + } + } + + config_error(conf, "Expecting section end while parsing SSL configuration"); +} + +static void +parse_listener(struct config *c, const struct config_line *l, struct lwan *lwan) +{ + lwan->config.listener = strdup(l->value); + if (!lwan->config.listener) + config_error(c, "Could not allocate memory for listener"); + + while ((l = config_read_line(c))) { + switch (l->type) { + case CONFIG_LINE_TYPE_LINE: + config_error(c, "Unexpected key %s", l->key); + return; + case CONFIG_LINE_TYPE_SECTION: + config_error(c, "Unexpected section %s", l->key); + return; + case CONFIG_LINE_TYPE_SECTION_END: + return; + } + } + + config_error(c, "Unexpected EOF while parsing listener"); +} + +static void +parse_site(struct config *c, const struct config_line *l, struct lwan *lwan) +{ + while ((l = config_read_line(c))) { + switch (l->type) { + case CONFIG_LINE_TYPE_LINE: + config_error(c, "Expecting prefix section"); + return; + case CONFIG_LINE_TYPE_SECTION: + /* FIXME: per-site authorization? */ + + if (l->key[0] == '&') { + void *handler = find_handler(l->key + 1); + if (handler) { + parse_listener_prefix(c, l, lwan, NULL, handler); + continue; + } + + config_error(c, "Could not find handler name: %s", l->key + 1); + return; + } + + const struct lwan_module *module = find_module(l->key); + if (module) { + parse_listener_prefix(c, l, lwan, module, NULL); + continue; + } + + config_error(c, "Invalid section or module not found: %s", l->key); + return; + case CONFIG_LINE_TYPE_SECTION_END: + return; + } + } + + config_error(c, "Expecting section end while parsing listener"); +} + +static bool setup_from_config(struct lwan *lwan, const char *path) +{ + const struct config_line *line; + struct config *conf; + bool has_site = false; + bool has_listener = false; + bool has_tls_listener = false; + char path_buf[PATH_MAX]; + + if (!path) + path = lwan_get_config_path(path_buf, sizeof(path_buf)); + lwan_status_info("Loading configuration file: %s", path); + + conf = config_open(path); + if (!conf) + return false; + + if (!lwan_trie_init(&lwan->url_map_trie, destroy_urlmap)) + return false; + + while ((line = config_read_line(conf))) { + switch (line->type) { + case CONFIG_LINE_TYPE_LINE: + if (streq(line->key, "keep_alive_timeout")) { + lwan->config.keep_alive_timeout = (unsigned int)parse_long( + line->value, default_config.keep_alive_timeout); + } else if (streq(line->key, "quiet")) { + lwan->config.quiet = + parse_bool(line->value, default_config.quiet); + } else if (streq(line->key, "proxy_protocol")) { + lwan->config.proxy_protocol = + parse_bool(line->value, default_config.proxy_protocol); + } else if (streq(line->key, "allow_cors")) { + lwan->config.allow_cors = + parse_bool(line->value, default_config.allow_cors); + } else if (streq(line->key, "expires")) { + lwan->config.expires = + parse_time_period(line->value, default_config.expires); + } else if (streq(line->key, "error_template")) { + free(lwan->config.error_template); + lwan->config.error_template = strdup(line->value); + } else if (streq(line->key, "threads")) { + long n_threads = + parse_long(line->value, default_config.n_threads); + if (n_threads < 0) + config_error(conf, "Invalid number of threads: %ld", + n_threads); + lwan->config.n_threads = (unsigned int)n_threads; + } else if (streq(line->key, "request_buffer_size")) { + long request_buffer_size = parse_long( + line->value, (long)default_config.request_buffer_size); + + if (request_buffer_size > 16 * (1 << 20)) { + config_error(conf, + "Request buffer can't be over 16MiB"); + } else if (request_buffer_size < DEFAULT_BUFFER_SIZE) { + lwan_status_warning("Using request buffer size of %d bytes instead of the " + "requested %ld bytes", + DEFAULT_BUFFER_SIZE, + request_buffer_size); + + request_buffer_size = DEFAULT_BUFFER_SIZE; + } + + lwan->config.request_buffer_size = (size_t)request_buffer_size; + } else if (streq(line->key, "max_file_descriptors")) { + long max_file_descriptors = parse_long( + line->value, (long)default_config.max_file_descriptors); + + if (max_file_descriptors < 0) { + config_error(conf, "Maximum number of file descriptors can't be negative"); + } else if (max_file_descriptors > 2000000l) { + config_error(conf, "2M file descriptors should be sufficient!"); + } else if (max_file_descriptors == 0) { + max_file_descriptors = default_config.max_file_descriptors; + } + + lwan->config.max_file_descriptors = (unsigned int)max_file_descriptors; + } else if (streq(line->key, "max_post_data_size")) { + long max_post_data_size = parse_long( + line->value, (long)default_config.max_post_data_size); + + if (max_post_data_size < 0) { + config_error(conf, "Negative maximum post data size"); + } else if (max_post_data_size > 128 * (1 << 20)) { + config_error(conf, + "Maximum post data can't be over 128MiB"); + } + + lwan->config.max_post_data_size = (size_t)max_post_data_size; + } else if (streq(line->key, "max_put_data_size")) { + long max_put_data_size = parse_long( + line->value, (long)default_config.max_put_data_size); + + if (max_put_data_size < 0) { + config_error(conf, "Negative maximum put data size"); + } else if (max_put_data_size > 128 * (1 << 20)) { + config_error(conf, + "Maximum put data can't be over 128MiB"); + } + lwan->config.max_put_data_size = (size_t)max_put_data_size; + } else if (streq(line->key, "allow_temp_files")) { + bool has_post, has_put; + + if (strstr(line->value, "all")) { + has_post = has_put = true; + } else { + has_post = !!strstr(line->value, "post"); + has_put = !!strstr(line->value, "put"); + } + + lwan->config.allow_post_temp_file = has_post; + lwan->config.allow_put_temp_file = has_put; + } else { + config_error(conf, "Unknown config key: %s", line->key); + } + break; + case CONFIG_LINE_TYPE_SECTION: + if (streq(line->key, "site")) { + if (!has_site) { + parse_site(conf, line, lwan); + has_site = true; + } else { + config_error(conf, "Only one site may be configured"); + } + } else if (streq(line->key, "straitjacket") || streq(line->key, "straightjacket")) { + lwan_straitjacket_enforce_from_config(conf); + } else if (streq(line->key, "headers")) { + parse_global_headers(conf, lwan); + } else if (streq(line->key, "listener")) { + if (has_listener) { + config_error(conf, "Listener already set up"); + } else { + parse_listener(conf, line, lwan); + has_listener = true; + } + } else if (streq(line->key, "tls_listener")) { + if (has_tls_listener) { + config_error(conf, "TLS Listener already set up"); + } else { + parse_tls_listener(conf, line, lwan); + has_tls_listener = true; + } + } else { + config_error(conf, "Unknown section type: %s", line->key); + } + break; + case CONFIG_LINE_TYPE_SECTION_END: + config_error(conf, "Unexpected section end"); + } + } + + if (config_last_error(conf)) { + lwan_status_critical("Error on config file \"%s\", line %d: %s", path, + config_cur_line(conf), config_last_error(conf)); + lwan_trie_destroy(&lwan->url_map_trie); + } + + config_close(conf); + + return true; +} + +static void try_setup_from_config(struct lwan *l, + const struct lwan_config *config) +{ + if (!setup_from_config(l, config->config_file_path)) { + if (config->config_file_path) { + lwan_status_critical("Could not read config file: %s", + config->config_file_path); + } + } + + lwan_status_init(l); /* `quiet` key might have changed value. */ + + l->config.request_flags = + (l->config.proxy_protocol ? REQUEST_ALLOW_PROXY_REQS : 0) | + (l->config.allow_cors ? REQUEST_ALLOW_CORS : 0) | + (l->config.ssl.send_hsts_header ? REQUEST_WANTS_HSTS_HEADER : 0); +} + +static rlim_t setup_open_file_count_limits(struct lwan *l) +{ + struct rlimit r; + + if (getrlimit(RLIMIT_NOFILE, &r) < 0) { + lwan_status_perror("Could not obtain maximum number of file " + "descriptors. Assuming %d", + OPEN_MAX); + return OPEN_MAX; + } + + if (r.rlim_max != r.rlim_cur) { + const rlim_t current = r.rlim_cur; + + if (r.rlim_max == RLIM_INFINITY && r.rlim_cur < OPEN_MAX) { + r.rlim_cur = OPEN_MAX; + } else if (r.rlim_cur < r.rlim_max) { + r.rlim_cur = r.rlim_max; + } else { + /* Shouldn't happen, so just return the current value. */ + goto out; + } + + r.rlim_cur = LWAN_MIN(l->config.max_file_descriptors, + r.rlim_cur); + + if (setrlimit(RLIMIT_NOFILE, &r) < 0) { + lwan_status_perror("Could not raise maximum number of file " + "descriptors to %" PRIu64 ". Leaving at " + "%" PRIu64, r.rlim_max, current); + r.rlim_cur = current; + } + } + +out: + if (r.rlim_cur < 10 * l->thread.count) { + lwan_status_critical("Number of file descriptors (%ld) is smaller than 10x " + "the number of threads (%d)\n", + r.rlim_cur, + 10 * l->thread.count); + } + + return r.rlim_cur; +} + +static void allocate_connections(struct lwan *l, size_t max_open_files) +{ + const size_t sz = max_open_files * sizeof(struct lwan_connection); + + l->conns = lwan_aligned_alloc(sz, 64); + if (UNLIKELY(!l->conns)) + lwan_status_critical_perror("lwan_alloc_aligned"); + + memset(l->conns, 0, sz); +} + +static void get_number_of_cpus(struct lwan *l) +{ + long n_online_cpus = sysconf(_SC_NPROCESSORS_ONLN); + long n_available_cpus = sysconf(_SC_NPROCESSORS_CONF); + + if (n_online_cpus < 0) { + lwan_status_warning( + "Could not get number of online CPUs, assuming 1 CPU"); + n_online_cpus = 1; + } + + if (n_available_cpus < 0) { + lwan_status_warning( + "Could not get number of available CPUs, assuming %ld CPUs", + n_online_cpus); + n_available_cpus = n_online_cpus; + } + + l->online_cpus = (unsigned int)n_online_cpus; + l->available_cpus = (unsigned int)n_available_cpus; +} + +void lwan_init(struct lwan *l) { lwan_init_with_config(l, &default_config); } + +const struct lwan_config *lwan_get_default_config(void) +{ + return &default_config; +} + +static char *dup_or_null(const char *s) +{ + return s ? strdup(s) : NULL; +} + +DEFINE_ARRAY_TYPE(constructor_array, struct lwan_constructor_callback_info) + +static int constructor_sort(const void *a, const void *b) +{ + const struct lwan_constructor_callback_info *ca = a; + const struct lwan_constructor_callback_info *cb = b; + return (ca->prio < cb->prio) - (ca->prio > cb->prio); +} + +__attribute__((no_sanitize_address)) static void +call_constructors(struct lwan *l) +{ + struct constructor_array constructors; + const struct lwan_constructor_callback_info *iter; + + constructor_array_init(&constructors); + LWAN_SECTION_FOREACH(lwan_constructor, iter) + { + struct lwan_constructor_callback_info *info = + constructor_array_append(&constructors); + if (!info) + lwan_status_critical("Could not append to constructor array"); + *info = *iter; + } + constructor_array_sort(&constructors, constructor_sort); + + LWAN_ARRAY_FOREACH (&constructors, iter) { + iter->func(l); + } + + constructor_array_reset(&constructors); +} + +void lwan_init_with_config(struct lwan *l, const struct lwan_config *config) +{ + /* Load defaults */ + memset(l, 0, sizeof(*l)); + memcpy(&l->config, config, sizeof(*config)); + l->config.listener = dup_or_null(l->config.listener); + l->config.config_file_path = dup_or_null(l->config.config_file_path); + l->config.ssl.key = dup_or_null(l->config.ssl.key); + l->config.ssl.cert = dup_or_null(l->config.ssl.cert); + + /* Initialize status first, as it is used by other things during + * their initialization. */ + lwan_status_init(l); + + call_constructors(l); + + /* These will only print debugging messages. Debug messages are always + * printed if we're on a debug build, so the quiet setting will be + * respected. */ + lwan_job_thread_init(); + lwan_tables_init(); + + /* Get the number of CPUs here because straightjacket might be active + * and this will block access to /proc and /sys, which will cause + * get_number_of_cpus() to get incorrect fallback values. */ + get_number_of_cpus(l); + + try_setup_from_config(l, config); + + if (!l->headers.len) + build_response_headers(l, config->global_headers); + + lwan_response_init(l); + + /* Continue initialization as normal. */ + lwan_status_debug("Initializing lwan web server"); + + if (!l->config.n_threads) { + l->thread.count = l->online_cpus; + if (l->thread.count == 1) + l->thread.count = 2; + } else if (l->config.n_threads > 3 * l->online_cpus) { + l->thread.count = l->online_cpus * 3; + + lwan_status_warning("%d threads requested, but only %d online CPUs " + "(out of %d configured CPUs); capping to %d threads", + l->config.n_threads, l->online_cpus, l->available_cpus, + 3 * l->online_cpus); + } else if (l->config.n_threads > 255) { + l->thread.count = 256; + + lwan_status_warning("%d threads requested, but max 256 supported", + l->config.n_threads); + } else { + l->thread.count = l->config.n_threads; + } + + rlim_t max_open_files = setup_open_file_count_limits(l); + allocate_connections(l, (size_t)max_open_files); + + l->thread.max_fd = (unsigned)max_open_files / (unsigned)l->thread.count; + lwan_status_info("Using %d threads, maximum %d sockets per thread", + l->thread.count, l->thread.max_fd); + + signal(SIGPIPE, SIG_IGN); + + lwan_readahead_init(); + lwan_thread_init(l); + lwan_http_authorize_init(); +} + +void lwan_shutdown(struct lwan *l) +{ + lwan_status_info("Shutting down"); + + free(l->config.listener); + free(l->config.error_template); + free(l->config.config_file_path); + + lwan_always_bzero(l->config.ssl.cert, strlen(l->config.ssl.cert)); + free(l->config.ssl.cert); + lwan_always_bzero(l->config.ssl.key, strlen(l->config.ssl.key)); + free(l->config.ssl.key); + + lwan_job_thread_shutdown(); + lwan_thread_shutdown(l); + + lwan_status_debug("Shutting down URL handlers"); + lwan_trie_destroy(&l->url_map_trie); + + free(l->headers.value); + free(l->conns); + + lwan_response_shutdown(l); + lwan_tables_shutdown(); + lwan_status_shutdown(l); + lwan_http_authorize_shutdown(); + lwan_readahead_shutdown(); +} + +void lwan_main_loop(struct lwan *l __attribute__((unused))) +{ + lwan_status_info("Ready to serve"); + + lwan_job_thread_main_loop(); +} + +#ifdef CLOCK_MONOTONIC_COARSE +LWAN_CONSTRUCTOR(detect_fastest_monotonic_clock, 0) +{ + struct timespec ts; + + if (!clock_gettime(CLOCK_MONOTONIC_COARSE, &ts)) + monotonic_clock_id = CLOCK_MONOTONIC_COARSE; +} +#endif + +void lwan_set_thread_name(const char *name) +{ + char thread_name[16]; + char process_name[PATH_MAX]; + char *tmp; + int ret; + + if (proc_pidpath(getpid(), process_name, sizeof(process_name)) < 0) + return; + + tmp = strrchr(process_name, '/'); + if (!tmp) + return; + + ret = snprintf(thread_name, sizeof(thread_name), "%s %s", tmp + 1, name); + if (ret < 0) + return; + + pthread_set_name_np(pthread_self(), thread_name); +} diff --git a/src/lib/lwan.h b/src/lib/lwan.h new file mode 100644 index 000000000..3ac9ef819 --- /dev/null +++ b/src/lib/lwan.h @@ -0,0 +1,671 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#if defined(__cplusplus) +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include + +#include "hash.h" +#include "timeout.h" +#include "lwan-array.h" +#include "lwan-config.h" +#include "lwan-coro.h" +#include "lwan-status.h" +#include "lwan-strbuf.h" +#include "lwan-trie.h" + +#if defined(__cplusplus) +#define ZERO_IF_IS_ARRAY(array) 0 +#else +/* This macro expands to 0 if its parameter is an array, and causes a + * compilation error otherwise. This is used by the N_ELEMENTS() macro to catch + * invalid usages of this macro (e.g. when using arrays decayed to pointers) */ +#define ZERO_IF_IS_ARRAY(array) \ + (!sizeof(char[1 - 2 * __builtin_types_compatible_p( \ + __typeof__(array), __typeof__(&(array)[0]))])) +#endif + +#define N_ELEMENTS(array) \ + (ZERO_IF_IS_ARRAY(array) | sizeof(array) / sizeof(array[0])) + + +#ifdef __APPLE__ +#define LWAN_SECTION_NAME(name_) "__DATA," #name_ +#else +#define LWAN_SECTION_NAME(name_) #name_ +#endif + +#define LWAN_MODULE_REF(name_) lwan_module_info_##name_.module +#define LWAN_MODULE_FORWARD_DECL(name_) \ + extern const struct lwan_module_info lwan_module_info_##name_; +#define LWAN_REGISTER_MODULE(name_, module_) \ + const struct lwan_module_info \ + __attribute__((used, section(LWAN_SECTION_NAME(lwan_module)))) \ + lwan_module_info_##name_ = {.name = #name_, .module = module_} + +#define LWAN_HANDLER_REF(name_) lwan_handler_##name_ + +#define LWAN_HANDLER_ROUTE(name_, route_) \ + static enum lwan_http_status lwan_handler_##name_( \ + struct lwan_request *, struct lwan_response *, void *); \ + static const struct lwan_handler_info \ + __attribute__((used, section(LWAN_SECTION_NAME(lwan_handler)))) \ + __attribute__((aligned(8))) /* FIXME: why is this alignment needed? */ \ + lwan_handler_info_##name_ = { \ + .name = #name_, \ + .route = route_, \ + .handler = lwan_handler_##name_, \ + }; \ + __attribute__((used)) static enum lwan_http_status lwan_handler_##name_( \ + struct lwan_request *request __attribute__((unused)), \ + struct lwan_response *response __attribute__((unused)), \ + void *data __attribute__((unused))) +#define LWAN_HANDLER(name_) LWAN_HANDLER_ROUTE(name_, NULL) + +#define ALWAYS_INLINE inline __attribute__((always_inline)) + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define STR4_INT(a, b, c, d) ((uint32_t)((a) | (b) << 8 | (c) << 16 | (d) << 24)) +#define STR2_INT(a, b) ((uint16_t)((a) | (b) << 8)) +#define STR8_INT(a, b, c, d, e, f, g, h) \ + ((uint64_t)STR4_INT(a, b, c, d) | (uint64_t)STR4_INT(e, f, g, h) << 32) +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define STR4_INT(d, c, b, a) ((uint32_t)((a) | (b) << 8 | (c) << 16 | (d) << 24)) +#define STR2_INT(b, a) ((uint16_t)((a) | (b) << 8)) +#define STR8_INT(a, b, c, d, e, f, g, h) \ + ((uint64_t)STR4_INT(a, b, c, d) << 32 | (uint64_t)STR4_INT(e, f, g, h)) +#elif __BYTE_ORDER__ == __ORDER_PDP_ENDIAN__ +#error A PDP? Seriously? +#endif + +static ALWAYS_INLINE uint16_t string_as_uint16(const char *s) +{ + uint16_t u; + + memcpy(&u, s, sizeof(u)); + + return u; +} + +static ALWAYS_INLINE uint32_t string_as_uint32(const char *s) +{ + uint32_t u; + + memcpy(&u, s, sizeof(u)); + + return u; +} + +static ALWAYS_INLINE uint64_t string_as_uint64(const char *s) +{ + uint64_t u; + + memcpy(&u, s, sizeof(u)); + + return u; +} + +#define LOWER2(s) ((s) | (uint16_t)0x2020) +#define LOWER4(s) ((s) | (uint32_t)0x20202020) +#define LOWER8(s) ((s) | (uint64_t)0x2020202020202020) + +#define STR2_INT_L(a, b) LOWER2(STR2_INT(a, b)) +#define STR4_INT_L(a, b, c, d) LOWER4(STR4_INT(a, b, c, d)) +#define STR8_INT_L(a, b, c, d, e, f, g, h) LOWER8(STR8_INT(a, b, c, d, e, f, g, h)) + +#define STRING_SWITCH_SMALL(s) switch (string_as_uint16(s)) +#define STRING_SWITCH_SMALL_L(s) switch (LOWER2(string_as_uint16(s))) +#define STRING_SWITCH(s) switch (string_as_uint32(s)) +#define STRING_SWITCH_L(s) switch (LOWER4(string_as_uint32(s))) +#define STRING_SWITCH_LARGE(s) switch (string_as_uint64(s)) +#define STRING_SWITCH_LARGE_L(s) switch (LOWER8(string_as_uint64(s))) + +#define LIKELY_IS(x, y) __builtin_expect((x), (y)) +#define LIKELY(x) LIKELY_IS(!!(x), 1) +#define UNLIKELY(x) LIKELY_IS((x), 0) + +#define ATOMIC_READ(V) (*(volatile typeof(V) *)&(V)) +#define ATOMIC_OP(P, O, V) (__sync_##O##_and_fetch((P), (V))) +#define ATOMIC_AAF(P, V) ATOMIC_OP((P), add, (V)) +#define ATOMIC_SAF(P, V) ATOMIC_OP((P), sub, (V)) +#define ATOMIC_INC(V) ATOMIC_AAF(&(V), 1) +#define ATOMIC_DEC(V) ATOMIC_SAF(&(V), 1) + +#if defined(__cplusplus) +#define LWAN_ARRAY_PARAM(length) [length] +#else +#define LWAN_ARRAY_PARAM(length) [static length] +#endif + +#if defined(LWAN_HAVE_ACCESS_ATTRIBUTE) +#define LWAN_ACCESS_PARAM(...) __attribute__((access(__VA_ARGS__))) +#else +#define LWAN_ACCESS_PARAM(...) +#endif + +#include "lwan-http-status.h" + +#define GENERATE_ENUM_ITEM(id, code, short, long) HTTP_ ## id = code, +enum lwan_http_status { + HTTP_CLASS__INFORMATIONAL = 100, + HTTP_CLASS__SUCCESS = 200, + HTTP_CLASS__REDIRECT = 300, + HTTP_CLASS__CLIENT_ERROR = 400, + HTTP_CLASS__SERVER_ERROR = 500, + + FOR_EACH_HTTP_STATUS(GENERATE_ENUM_ITEM) +}; +#undef GENERATE_ENUM_ITEM + +enum lwan_handler_flags { + HANDLER_EXPECTS_BODY_DATA = 1 << 0, + HANDLER_MUST_AUTHORIZE = 1 << 1, + HANDLER_CAN_REWRITE_URL = 1 << 2, + HANDLER_DATA_IS_HASH_TABLE = 1 << 3, + + HANDLER_PARSE_MASK = HANDLER_EXPECTS_BODY_DATA, +}; + +/* 1<<0 set: response has body; see has_response_body() in lwan-response.c */ +/* 1<<3 set: request has body; see request_has_body() in lwan-request.c */ +#define FOR_EACH_REQUEST_METHOD(X) \ + X(GET, get, (1 << 0), (STR4_INT('G', 'E', 'T', ' ')), 0.6) \ + X(POST, post, (1 << 3 | 1 << 1 | 1 << 0), (STR4_INT('P', 'O', 'S', 'T')), 0.2)\ + X(HEAD, head, (1 << 1), (STR4_INT('H', 'E', 'A', 'D')), 0.2) \ + X(OPTIONS, options, (1 << 2), (STR4_INT('O', 'P', 'T', 'I')), 0.1) \ + X(DELETE, delete, (1 << 1 | 1 << 2), (STR4_INT('D', 'E', 'L', 'E')), 0.1) \ + X(PUT, put, (1 << 3 | 1 << 2 | 1 << 0), (STR4_INT('P', 'U', 'T', ' ')), 0.1) + +#define SELECT_MASK(upper, lower, mask, constant, probability) mask | +#define GENERATE_ENUM_ITEM(upper, lower, mask, constant, probability) REQUEST_METHOD_##upper = mask, + +enum lwan_request_flags { + REQUEST_ALL_FLAGS = -1, + + REQUEST_METHOD_MASK = FOR_EACH_REQUEST_METHOD(SELECT_MASK) 0, + FOR_EACH_REQUEST_METHOD(GENERATE_ENUM_ITEM) + + REQUEST_ACCEPT_DEFLATE = 1 << 4, + REQUEST_ACCEPT_GZIP = 1 << 5, + REQUEST_ACCEPT_BROTLI = 1 << 6, + REQUEST_ACCEPT_ZSTD = 1 << 7, + REQUEST_ACCEPT_MASK = 1 << 4 | 1 << 5 | 1 << 6 | 1 << 7, + + REQUEST_IS_HTTP_1_0 = 1 << 8, + REQUEST_ALLOW_PROXY_REQS = 1 << 9, + REQUEST_PROXIED = 1 << 10, + REQUEST_ALLOW_CORS = 1 << 11, + + RESPONSE_SENT_HEADERS = 1 << 12, + RESPONSE_CHUNKED_ENCODING = 1 << 13, + RESPONSE_NO_CONTENT_LENGTH = 1 << 14, + RESPONSE_NO_EXPIRES = 1 << 15, + RESPONSE_URL_REWRITTEN = 1 << 16, + + RESPONSE_STREAM = 1 << 17, + + REQUEST_PARSED_QUERY_STRING = 1 << 18, + REQUEST_PARSED_IF_MODIFIED_SINCE = 1 << 19, + REQUEST_PARSED_RANGE = 1 << 20, + REQUEST_PARSED_FORM_DATA = 1 << 21, + REQUEST_PARSED_COOKIES = 1 << 22, + REQUEST_PARSED_ACCEPT_ENCODING = 1 << 23, + + RESPONSE_INCLUDE_REQUEST_ID = 1 << 24, + + REQUEST_HAS_QUERY_STRING = 1 << 25, + + REQUEST_WANTS_HSTS_HEADER = 1 << 26, +}; + +#undef SELECT_MASK +#undef GENERATE_ENUM_ITEM + +#define CONN_EPOLL_EVENT_SHIFT 16 +#define CONN_EPOLL_EVENT_MASK ((1 << CONN_EPOLL_EVENT_SHIFT) - 1) + +enum lwan_connection_flags { + CONN_MASK = -1, + + /* Upper 16-bit of CONN_EVENTS_* store the epoll event interest + * mask for those events. */ + CONN_EVENTS_READ = ((EPOLLIN | EPOLLRDHUP) << CONN_EPOLL_EVENT_SHIFT) | 1 << 0, + CONN_EVENTS_WRITE = ((EPOLLOUT | EPOLLRDHUP) << CONN_EPOLL_EVENT_SHIFT) | 1 << 1, + CONN_EVENTS_READ_WRITE = CONN_EVENTS_READ | CONN_EVENTS_WRITE, + CONN_EVENTS_MASK = 1 << 0 | 1 << 1, + + CONN_IS_KEEP_ALIVE = 1 << 2, + + /* WebSockets-related flags. */ + CONN_IS_UPGRADE = 1 << 3, + CONN_IS_WEBSOCKET = 1 << 4, + + /* These are used for a few different things: + * - Avoid re-deferring callbacks to remove request from the timeout wheel + * after it has slept previously and is requesting to sleep again. (The + * timeout defer is disarmed right after resuming, and is only there + * because connections may be closed when they're suspended.) + * - Distinguish file descriptor in event loop between the connection and + * an awaited file descriptor. (This is set in the connection that's + * awaiting since the pointer to the connection is used as user_data in both + * cases. This is required to be able to resume the connection coroutine + * after the await is completed, and to bubble up errors in awaited file + * descriptors to request handlers rather than abruptly closing the + * connection.) */ + CONN_SUSPENDED_MASK = 1 << 5, + CONN_SUSPENDED = (EPOLLRDHUP << CONN_EPOLL_EVENT_SHIFT) | CONN_SUSPENDED_MASK, + CONN_HAS_REMOVE_SLEEP_DEFER = 1 << 6, + + /* Used when HTTP pipelining has been detected. This enables usage of the + * MSG_MORE flags when sending responses to batch as many short responses + * as possible in a single TCP fragment. */ + CONN_CORK = 1 << 7, + + /* Set only on file descriptors being watched by async/await to determine + * which epoll operation to use when suspending/resuming (ADD/MOD). Reset + * whenever associated client connection is closed. */ + CONN_ASYNC_AWAIT = 1 << 8, + + /* Used to both implement lwan_request_awaitv_all() correctly, and to + * ensure that spurious resumes from fds that weren't in the multiple + * await call won't return to the request handler. */ + CONN_ASYNC_AWAITV = 1 << 9, + + CONN_SENT_CONNECTION_HEADER = 1 << 10, + + /* Is this a TLS connection? */ + CONN_TLS = 1 << 11, + + /* Both are used to know if an epoll event pertains to a listener rather + * than a client. */ + CONN_LISTENER = 1 << 12, + + /* Only valid when CONN_ASYNC_AWAIT is set. Set on file descriptors that + * got (EPOLLHUP|EPOLLRDHUP) events from epoll so that request handlers + * can deal with this fact. */ + CONN_HUNG_UP = 1 << 13, + + CONN_FLAG_LAST = CONN_HUNG_UP, +}; + +static_assert(CONN_FLAG_LAST < ((1 << 15) - 1), + "Enough space for epoll events in conn flags"); + +enum lwan_connection_coro_yield { + /* Returns to the event loop and terminates the coroutine, freeing + * all resources associated with it, including calling deferred + * callback, and the coroutine itself. */ + CONN_CORO_ABORT, + + /* Return to the event loop without changing the epoll event mask + * or any other flag in this coroutine. */ + CONN_CORO_YIELD, + + /* Returns to the event loop, and optionally change the epoll event + * mask (if it's not already the expected one.) */ + CONN_CORO_WANT_READ, + CONN_CORO_WANT_WRITE, + CONN_CORO_WANT_READ_WRITE, + + /* If a connection coroutine yields with CONN_CORO_SUSPEND, then + * it'll be resumed using CONN_CORO_RESUME from the event loop. + * CONN_CORO_RESUME should never be used from within connections + * themselves, and should be considered a private API. */ + CONN_CORO_SUSPEND, + CONN_CORO_RESUME, + + CONN_CORO_MAX, +}; + +struct lwan_key_value { + char *key; + char *value; +}; + +struct lwan_request; + +struct lwan_response { + struct lwan_strbuf *buffer; + const char *mime_type; + + union { + struct { + const struct lwan_key_value *headers; + }; + + struct { + enum lwan_http_status (*callback)(struct lwan_request *request, + void *data); + void *data; + } stream; + }; +}; + +struct lwan_value { + char *value; + size_t len; +}; + +struct lwan_connection { + /* This structure is exactly 32-bytes on x86-64. If it is changed, + * make sure the scheduler (lwan-thread.c) is updated as well. */ + enum lwan_connection_flags flags; + + unsigned int time_to_expire; + + struct coro *coro; + struct lwan_thread *thread; + + /* This union is here to support async/await when a handler is waiting + * on multiple file descriptors. By storing a pointer to the parent + * connection here, we're able to register the awaited file descriptor + * in epoll using a pointer to the awaited file descriptor struct, + * allowing us to yield to the handler this information and signal which + * file descriptor caused the handler to be awoken. (We can yield just + * the file descriptor plus another integer with values to signal things + * like timeouts and whatnot. Future problems!) + * + * Also, when CONN_ASYNC_AWAIT is set, `coro` points to parent->coro, + * so that conn->coro is consistently usable. Gotta be careful though, + * because struct coros are not refcounted and this could explode with + * a double free. */ + union { + /* For HTTP client connections handling inside the timeout queue */ + struct { + int prev; + int next; + }; + + /* For awaited file descriptor, only valid if flags&CONN_ASYNC_AWAIT */ + struct lwan_connection *parent; + }; +}; + +struct lwan_proxy { + union { + struct sockaddr_in ipv4; + struct sockaddr_in6 ipv6; + } from, to; +}; + +DEFINE_ARRAY_TYPE(lwan_key_value_array, struct lwan_key_value) + +struct lwan_request_parser_helper; + +struct lwan_request { + enum lwan_request_flags flags; + int fd; + struct lwan_connection *conn; + const struct lwan_value *const global_response_headers; + + struct lwan_request_parser_helper *helper; + + struct lwan_value url; + struct lwan_value original_url; + struct lwan_response response; + + struct lwan_proxy *proxy; + struct timeout timeout; +}; + +struct lwan_module { + enum lwan_http_status (*handle_request)(struct lwan_request *request, + struct lwan_response *response, + void *instance); + + void *(*create)(const char *prefix, void *args); + void *(*create_from_hash)(const char *prefix, const struct hash *hash); + void (*destroy)(void *instance); + + bool (*parse_conf)(void *instance, struct config *config); + + enum lwan_handler_flags flags; +}; + +struct lwan_module_info { + const char *name; + const struct lwan_module *module; +}; + +struct lwan_url_map_route_info { + const char *route; + enum lwan_http_status (*handler)(struct lwan_request *request, + struct lwan_response *response, + void *data); +}; + +struct lwan_handler_info { + const char *name; + enum lwan_http_status (*handler)(struct lwan_request *request, + struct lwan_response *response, + void *data); + const char *route; +}; + +struct lwan_url_map { + enum lwan_http_status (*handler)(struct lwan_request *request, + struct lwan_response *response, + void *data); + void *data; + + const char *prefix; + size_t prefix_len; + enum lwan_handler_flags flags; + + const struct lwan_module *module; + void *args; + + struct { + char *realm; + char *password_file; + } authorization; +}; + +struct lwan_straitjacket { + const char *user_name; + const char *chroot_path; + bool drop_capabilities; +}; + +struct lwan_config { + /* Field will be overridden during initialization. */ + enum lwan_request_flags request_flags; + struct lwan_key_value *global_headers; + + char *listener; + char *tls_listener; + char *error_template; + char *config_file_path; + + struct { + char *cert; + char *key; + bool send_hsts_header; + } ssl; + + size_t max_post_data_size; + size_t max_put_data_size; + size_t request_buffer_size; + + unsigned int keep_alive_timeout; + unsigned int expires; + unsigned int n_threads; + unsigned int max_file_descriptors; + + unsigned int quiet : 1; + unsigned int proxy_protocol : 1; + unsigned int allow_cors : 1; + unsigned int allow_post_temp_file : 1; + unsigned int allow_put_temp_file : 1; +}; + +struct lwan { + struct lwan_trie url_map_trie; + struct lwan_connection *conns; + struct lwan_value headers; + +#if defined(LWAN_HAVE_MBEDTLS) + struct lwan_tls_context *tls; +#endif + + struct { + struct lwan_thread *threads; + + unsigned int max_fd; + unsigned int count; + pthread_barrier_t barrier; + } thread; + + struct lwan_config config; + + unsigned int online_cpus; + unsigned int available_cpus; +}; + +void lwan_set_url_map(struct lwan *l, const struct lwan_url_map *map); +void lwan_detect_url_map(struct lwan *l); +void lwan_main_loop(struct lwan *l); + +size_t lwan_prepare_response_header(struct lwan_request *request, + enum lwan_http_status status, + char header_buffer[], + size_t header_buffer_size) + __attribute__((warn_unused_result)) + LWAN_ACCESS_PARAM(write_only, 3, 4); + +const char *lwan_request_get_post_param(struct lwan_request *request, + const char *key) + __attribute__((warn_unused_result, pure)); + +const char *lwan_request_get_query_param(struct lwan_request *request, + const char *key) + __attribute__((warn_unused_result, pure)); +const char *lwan_request_get_cookie(struct lwan_request *request, + const char *key) + __attribute__((warn_unused_result, pure)); +const char *lwan_request_get_header(struct lwan_request *request, + const char *header) + __attribute__((warn_unused_result)); + +void lwan_request_sleep(struct lwan_request *request, uint64_t ms); + +bool lwan_response_set_chunked(struct lwan_request *request, + enum lwan_http_status status); +bool lwan_response_set_chunked_full(struct lwan_request *request, + enum lwan_http_status status, + const struct lwan_key_value *additional_headers); +void lwan_response_send_chunk(struct lwan_request *request); +void lwan_response_send_chunk_full(struct lwan_request *request, + struct lwan_strbuf *strbuf); + +bool lwan_response_set_event_stream(struct lwan_request *request, + enum lwan_http_status status); +void lwan_response_send_event(struct lwan_request *request, const char *event); + +const char *lwan_determine_mime_type_for_file_name(const char *file_name) + __attribute__((pure)) __attribute__((warn_unused_result)); + +void lwan_init(struct lwan *l); +void lwan_init_with_config(struct lwan *l, const struct lwan_config *config); +void lwan_shutdown(struct lwan *l); + +static inline int lwan_main(void) +{ + struct lwan l; + + lwan_init(&l); + + lwan_detect_url_map(&l); + lwan_main_loop(&l); + + lwan_shutdown(&l); + + return 0; +} + +const struct lwan_config *lwan_get_default_config(void); + +const char *lwan_request_get_host(struct lwan_request *request); + +const char * +lwan_request_get_remote_address(const struct lwan_request *request, + char buffer LWAN_ARRAY_PARAM(INET6_ADDRSTRLEN)) + __attribute__((warn_unused_result)) + LWAN_ACCESS_PARAM(write_only, 2); + +const char *lwan_request_get_remote_address_and_port( + const struct lwan_request *request, + char buffer LWAN_ARRAY_PARAM(INET6_ADDRSTRLEN), uint16_t *port) + __attribute__((warn_unused_result)) + LWAN_ACCESS_PARAM(write_only, 2) + LWAN_ACCESS_PARAM(write_only, 3); + +static inline enum lwan_request_flags +lwan_request_get_method(const struct lwan_request *request) +{ + return (enum lwan_request_flags)(request->flags & REQUEST_METHOD_MASK); +} +const char *lwan_request_get_method_str(const struct lwan_request *request); + +int lwan_request_get_range(struct lwan_request *request, + off_t *from, + off_t *to); +int lwan_request_get_if_modified_since(struct lwan_request *request, + time_t *value); +const struct lwan_value * +lwan_request_get_request_body(struct lwan_request *request); +const struct lwan_value * +lwan_request_get_content_type(struct lwan_request *request); +const struct lwan_key_value_array * +lwan_request_get_cookies(struct lwan_request *request); +const struct lwan_key_value_array * +lwan_request_get_query_params(struct lwan_request *request); +const struct lwan_key_value_array * +lwan_request_get_post_params(struct lwan_request *request); +enum lwan_request_flags +lwan_request_get_accept_encoding(struct lwan_request *request); + +enum lwan_http_status +lwan_request_websocket_upgrade(struct lwan_request *request); +void lwan_response_websocket_write_text(struct lwan_request *request); +void lwan_response_websocket_write_binary(struct lwan_request *request); +int lwan_response_websocket_read(struct lwan_request *request); +int lwan_response_websocket_read_hint(struct lwan_request *request, size_t size_hint); + +int lwan_request_await_read(struct lwan_request *r, int fd); +int lwan_request_await_write(struct lwan_request *r, int fd); +int lwan_request_await_read_write(struct lwan_request *r, int fd); +int lwan_request_awaitv_any(struct lwan_request *r, ...); +int lwan_request_awaitv_all(struct lwan_request *r, ...); + +void lwan_straitjacket_enforce(const struct lwan_straitjacket *sj); + +#if defined(__cplusplus) +} +#endif diff --git a/src/lib/missing-epoll.c b/src/lib/missing-epoll.c new file mode 100644 index 000000000..598866d3c --- /dev/null +++ b/src/lib/missing-epoll.c @@ -0,0 +1,173 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan-private.h" + +#if !defined(LWAN_HAVE_EPOLL) && defined(LWAN_HAVE_KQUEUE) +#include +#include +#include + +int epoll_create1(int flags) +{ +#if defined(LWAN_HAVE_KQUEUE1) + return kqueue1(flags & EPOLL_CLOEXEC ? O_CLOEXEC : 0); +#else + int fd = kqueue(); + + if (flags & EPOLL_CLOEXEC) { + int flags; + + flags = fcntl(fd, F_GETFD); + if (flags < 0) + return -1; + + if (fcntl(fd, F_SETFD, flags | O_CLOEXEC) < 0) + return -1; + } + + return fd; +#endif +} + +static int epoll_no_event_marker; + +int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) +{ + struct kevent ev; + + switch (op) { + case EPOLL_CTL_ADD: + case EPOLL_CTL_MOD: { + int events = 0; + void *udata = event->data.ptr; + int flags = EV_ADD; + + if (event->events & EPOLLIN) { + events = EVFILT_READ; + } else if (event->events & EPOLLOUT) { + events = EVFILT_WRITE; + } else { + /* kqueue needs an event filter to track a file descriptor, + * but epoll doesn't. So create a fake one here and check for + * it when converting from kevents to epoll_events. */ + events = EVFILT_WRITE; + udata = &epoll_no_event_marker; + } + + if (event->events & EPOLLONESHOT) + flags |= EV_ONESHOT; + if (event->events & EPOLLET) + flags |= EV_CLEAR; + + flags |= EV_ERROR; /* EPOLLERR is always set. */ + flags |= EV_EOF; /* EPOLLHUP is always set. */ + + EV_SET(&ev, fd, events, flags, 0, 0, udata); + break; + } + + case EPOLL_CTL_DEL: + EV_SET(&ev, fd, 0, EV_DELETE, 0, 0, 0); + break; + + default: + errno = EINVAL; + return -1; + } + + return kevent(epfd, &ev, 1, NULL, 0, NULL); +} + +static struct timespec *to_timespec(struct timespec *t, int ms) +{ + if (ms < 0) + return NULL; + + t->tv_sec = ms / 1000; + t->tv_nsec = (ms % 1000) * 1000000; + + return t; +} + +static int kevent_ident_cmp(const void *ptr0, const void *ptr1) +{ + const struct kevent *ev0 = ptr0; + const struct kevent *ev1 = ptr1; + return (ev0->ident > ev1->ident) - (ev0->ident < ev1->ident); +} + +int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) +{ + struct epoll_event *ev = events; + struct kevent *evs = alloca(sizeof(*evs) * LWAN_MIN(1024, maxevents)); + struct timespec tmspec; + int i, r; + + r = kevent(epfd, NULL, 0, evs, maxevents, to_timespec(&tmspec, timeout)); + if (UNLIKELY(r < 0)) { + return -1; + } + + qsort(evs, (size_t)r, sizeof(struct kevent), kevent_ident_cmp); + + uintptr_t last = (uintptr_t)&epoll_no_event_marker; + for (i = 0; i < r; i++) { + struct kevent *kev = &evs[i]; + + if (kev->ident != last) { + if (last >= 0) + ev++; + + ev->events = 0; + ev->data.ptr = kev->udata; + } + + if (kev->flags & EV_ERROR) { + ev->events |= EPOLLERR; + } + if (kev->flags & EV_EOF) { + ev->events |= EPOLLRDHUP; + } + if (kev->filter == EVFILT_READ) { + ev->events |= EPOLLIN; + } else if (kev->filter == EVFILT_WRITE && + kev->udata != &epoll_no_event_marker) { + ev->events |= EPOLLOUT; + ev->data.ptr = kev->udata; + } + + last = kev->ident; + } + + return (int)(intptr_t)(ev - events); +} +#elif !defined(LWAN_HAVE_EPOLL) +#error epoll() not implemented for this platform +#endif diff --git a/src/lib/missing-pthread.c b/src/lib/missing-pthread.c new file mode 100644 index 000000000..fdbeacbeb --- /dev/null +++ b/src/lib/missing-pthread.c @@ -0,0 +1,102 @@ +/* + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#define _GNU_SOURCE +#include +#include +#include + +#ifdef __FreeBSD__ +#include +#endif + +#include "lwan-private.h" + +#ifndef LWAN_HAVE_PTHREADBARRIER +#define PTHREAD_BARRIER_SERIAL_THREAD -1 + +int +pthread_barrier_init(pthread_barrier_t *restrict barrier, + const pthread_barrierattr_t *restrict attr __attribute__((unused)), + unsigned int count) { + if (count == 0) { + return -1; + } + + barrier->count = count; + barrier->in = 0; + + if (pthread_mutex_init(&barrier->mutex, NULL) < 0) + return -1; + + if (pthread_cond_init(&barrier->cond, NULL) < 0) { + pthread_mutex_destroy(&barrier->mutex); + return -1; + } + + return 0; +} + +int +pthread_barrier_destroy(pthread_barrier_t *barrier) +{ + pthread_mutex_destroy(&barrier->mutex); + pthread_cond_destroy(&barrier->cond); + barrier->in = 0; + + return 0; +} + +int +pthread_barrier_wait(pthread_barrier_t *barrier) +{ + pthread_mutex_lock(&barrier->mutex); + + barrier->in++; + if (barrier->in >= barrier->count) { + barrier->in = 0; + pthread_cond_broadcast(&barrier->cond); + pthread_mutex_unlock(&barrier->mutex); + + return PTHREAD_BARRIER_SERIAL_THREAD; + } + + pthread_cond_wait(&barrier->cond, &barrier->mutex); + pthread_mutex_unlock(&barrier->mutex); + + return 0; +} +#endif + +#if defined(__linux__) +int pthread_set_name_np(pthread_t thread, const char *name) +{ + return pthread_setname_np(thread, name); +} +#elif defined(__APPLE__) +int pthread_set_name_np(pthread_t thread, const char *name) +{ + if (!pthread_equal(thread, pthread_self())) + return EPERM; + + /* macOS takes a char*; I don't know if it's modified or not, so + * copy it on the stack. */ + return pthread_setname_np(strdupa(name)); +} +#endif diff --git a/src/lib/missing.c b/src/lib/missing.c new file mode 100644 index 000000000..42298ea5f --- /dev/null +++ b/src/lib/missing.c @@ -0,0 +1,615 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lwan.h" + +#ifndef LWAN_HAVE_MEMPCPY +void *mempcpy(void *dest, const void *src, size_t len) +{ + char *p = memcpy(dest, src, len); + return p + len; +} +#endif + +#ifndef LWAN_HAVE_MEMRCHR +void *memrchr(const void *s, int c, size_t n) +{ + const char *end = (const char *)s + n + 1; + const char *prev = NULL; + + for (const char *cur = s; cur <= end; prev = cur++) { + cur = (const char *)memchr(cur, c, (size_t)(end - cur)); + if (!cur) + break; + } + + return (void *)prev; +} +#endif + +#ifndef LWAN_HAVE_PIPE2 +int pipe2(int pipefd[2], int flags) +{ + int r; + + r = pipe(pipefd); + if (r < 0) + return r; + + if (fcntl(pipefd[0], F_SETFL, flags) < 0 || + fcntl(pipefd[1], F_SETFL, flags) < 0) { + int saved_errno = errno; + + close(pipefd[0]); + close(pipefd[1]); + + errno = saved_errno; + return -1; + } + + return 0; +} +#endif + +#ifndef LWAN_HAVE_ACCEPT4 +int accept4(int sock, struct sockaddr *addr, socklen_t *addrlen, int flags) +{ + int fd = accept(sock, addr, addrlen); + int newflags = 0; + + if (fd < 0) + return fd; + + if (flags & SOCK_NONBLOCK) { + newflags |= O_NONBLOCK; + flags &= ~SOCK_NONBLOCK; + } + if (flags & SOCK_CLOEXEC) { + newflags |= O_CLOEXEC; + flags &= ~SOCK_CLOEXEC; + } + if (flags) { + errno = -EINVAL; + return -1; + } + + if (fcntl(fd, F_SETFL, newflags) < 0) { + int saved_errno = errno; + + close(fd); + + errno = saved_errno; + return -1; + } + + return fd; +} +#endif + +#ifndef LWAN_HAVE_CLOCK_GETTIME +int clock_gettime(clockid_t clk_id, struct timespec *ts) +{ + switch (clk_id) { + case CLOCK_MONOTONIC: + case CLOCK_MONOTONIC_COARSE: + /* FIXME: time() isn't monotonic */ + ts->tv_sec = time(NULL); + ts->tv_nsec = 0; + return 0; + } + + errno = EINVAL; + return -1; +} +#endif + +#if defined(__linux__) || defined(__CYGWIN__) +#if defined(LWAN_HAVE_GETAUXVAL) +#include +#endif + +int proc_pidpath(pid_t pid, void *buffer, size_t buffersize) +{ + ssize_t path_len; + + if (getpid() != pid) { + errno = EACCES; + + return -1; + } + +#if defined(LWAN_HAVE_GETAUXVAL) + const char *execfn = (const char *)getauxval(AT_EXECFN); + + if (execfn) { + size_t len = strlen(execfn); + + if (len + 1 < buffersize) { + memcpy(buffer, execfn, len + 1); + + return 0; + } + } +#endif + + path_len = readlink("/proc/self/exe", buffer, buffersize); + if (path_len < 0) + return -1; + + if (path_len < (ssize_t)buffersize) { + ((char *)buffer)[path_len] = '\0'; + + return 0; + } + + errno = EOVERFLOW; + return -1; +} + +#elif defined(__FreeBSD__) +#include + +int proc_pidpath(pid_t pid, void *buffer, size_t buffersize) +{ + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; + size_t path_len = buffersize; + + if (getpid() != pid) { + errno = EACCES; + + return -1; + } + + if (sysctl(mib, N_ELEMENTS(mib), buffer, &path_len, NULL, 0) < 0) + return -1; + + return 0; +} +#elif defined(LWAN_HAVE_DLADDR) && !defined(__APPLE__) +#include + +int proc_pidpath(pid_t pid, void *buffer, size_t buffersize) +{ + Dl_info info; + + if (getpid() != pid) { + errno = EACCES; + return -1; + } + + extern int main(); + if (dladdr(main, &info)) { + if (!info.dli_fname) + goto fallback; + + if (buffersize < PATH_MAX - 1) + goto fallback; + + if (realpath(info.dli_fname, buffer)) + return 0; + } + +fallback: + if (strlcpy(buffer, "lwan", buffersize) >= buffersize) { + errno = ENOMEM; + return -1; + } + + return 0; +} +#elif !defined(__APPLE__) +#error proc_pidpath() not implemented for this architecture +#endif + +#if defined(__linux__) + +#if !defined(LWAN_HAVE_GETTID) +#include + +pid_t gettid(void) { return (pid_t)syscall(SYS_gettid); } +#endif + +#elif defined(__FreeBSD__) +#include + +pid_t gettid(void) +{ + long ret; + + thr_self(&ret); + + return (pid_t)ret; +} +#elif defined(__APPLE__) && MAC_OS_X_VERSION_MAX_ALLOWED >= 101200 +#include + +pid_t gettid(void) { return syscall(SYS_thread_selfid); } +#else +pid_t gettid(void) { return (pid_t)pthread_self(); } +#endif + +#if defined(__APPLE__) +/* NOTE: Although saved UID/GID cannot be set using sysctl(), for the use + * case in Lwan, it's possible to obtain the value and check if they're the + * ones expected -- and abort if it's not. Should be good enough for a + * wrapper like this. */ + +#include + +static int get_current_proc_info(struct kinfo_proc *kp) +{ + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; + size_t len = sizeof(*kp); + + return sysctl(mib, N_ELEMENTS(mib), kp, &len, NULL, 0); +} + +int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid) +{ + struct kinfo_proc kp; + + if (!get_current_proc_info(&kp)) { + *ruid = getuid(); + *euid = geteuid(); + *suid = kp.kp_eproc.e_pcred.p_svuid; + + return 0; + } + + return -1; +} + +int setresuid(uid_t ruid, uid_t euid, uid_t suid __attribute__((unused))) +{ + return setreuid(ruid, euid); +} + +int setresgid(gid_t rgid, gid_t egid, gid_t sgid __attribute__((unused))) +{ + return setregid(rgid, egid); +} + +int getresgid(gid_t *rgid, gid_t *egid, gid_t *sgid) +{ + struct kinfo_proc kp; + + if (!get_current_proc_info(&kp)) { + *rgid = getgid(); + *egid = getegid(); + *sgid = kp.kp_eproc.e_pcred.p_svgid; + + return 0; + } + + return -1; +} +#endif + +#if !defined(LWAN_HAVE_MKOSTEMP) +int mkostemp(char *tmpl, int flags) +{ + int fd, fl; + + fd = mkstemp(tmpl); + if (fd < 0) + return -1; + + fl = fcntl(fd, F_GETFD); + if (fl < 0) + goto out; + + if (flags & O_CLOEXEC) + fl |= FD_CLOEXEC; + + if (fcntl(fd, F_SETFD, fl) < 0) + goto out; + + return fd; + +out: + close(fd); + return -1; +} +#endif + +#if !defined(LWAN_HAVE_REALLOCARRAY) +/* $OpenBSD: reallocarray.c,v 1.2 2014/12/08 03:45:00 bcook Exp $ */ +/* + * Copyright (c) 2008 Otto Moerbeek + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#if !defined(LWAN_HAVE_BUILTIN_MUL_OVERFLOW) +/* + * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX + * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW + */ +#define MUL_NO_OVERFLOW ((size_t)1 << (sizeof(size_t) * 4)) + +static inline bool umull_overflow(size_t a, size_t b, size_t *out) +{ + if ((a >= MUL_NO_OVERFLOW || b >= MUL_NO_OVERFLOW) && a > 0 && + SIZE_MAX / a < b) + return true; + *out = a * b; + return false; +} +#else +#define umull_overflow __builtin_mul_overflow +#endif + +void *reallocarray(void *optr, size_t nmemb, size_t size) +{ + size_t total_size; + if (UNLIKELY(umull_overflow(nmemb, size, &total_size))) { + errno = ENOMEM; + return NULL; + } + if (UNLIKELY(total_size == 0)) { + free(optr); + return malloc(1); + } + return realloc(optr, total_size); +} +#endif /* LWAN_HAVE_REALLOCARRAY */ + +#if !defined(LWAN_HAVE_READAHEAD) +ssize_t readahead(int fd, off_t offset, size_t count) +{ +#if defined(LWAN_HAVE_POSIX_FADVISE) + return (ssize_t)posix_fadvise(fd, offset, (off_t)count, + POSIX_FADV_WILLNEED); +#else + (void)fd; + (void)offset; + (void)count; + + return 0; +#endif +} +#endif + +#if !defined(LWAN_HAVE_GET_CURRENT_DIR_NAME) +#include + +char *get_current_dir_name(void) +{ + char buffer[PATH_MAX]; + char *ret; + + ret = getcwd(buffer, sizeof(buffer)); + return strdup(ret ? ret : "/"); +} +#endif + +#ifndef __linux__ +int capset(struct __user_cap_header_struct *header, + struct __user_cap_data_struct *data) +{ +#ifdef __OpenBSD__ + if (header->version != _LINUX_CAPABILITY_VERSION_1) + return -EINVAL; + if (header->pid != 0) + return -EINVAL; + if (data->effective == 0 && data->permitted == 0) + return pledge("stdio rpath tmppath inet error", NULL); +#else + (void)header; + (void)data; +#endif + + return 0; +} +#endif + +#if !defined(LWAN_HAVE_FWRITE_UNLOCKED) +size_t fwrite_unlocked(const void *ptr, size_t size, size_t n, FILE *stream) +{ + size_t to_write = size * n; + const size_t total_to_write = to_write; + + if (!to_write) + return 0; + + fflush/* _unlocked? */(stream); + + while (to_write) { + ssize_t r = write(fileno(stream), ptr, to_write); + if (r < 0) { + if (errno == EINTR) + continue; + break; + } + + to_write -= (size_t)r; + } + + return (total_to_write - to_write) / size; +} +#endif + +#if !defined(LWAN_HAVE_STATFS) +int statfs(const char *path, struct statfs *buf) +{ + (void)path; + (void)buf; + + *errno = ENOSYS; + return -1; +} +#endif + +static int lwan_getentropy_fallback(void *buffer, size_t buffer_len) +{ + int fd; + + fd = open("/dev/urandom", O_CLOEXEC | O_RDONLY); + if (fd < 0) { + fd = open("/dev/random", O_CLOEXEC | O_RDONLY); + if (fd < 0) + return -1; + } + ssize_t total_read = read(fd, buffer, buffer_len); + close(fd); + + return total_read == (ssize_t)buffer_len ? 0 : -1; +} + +#if defined(SYS_getrandom) +long int lwan_getentropy(void *buffer, size_t buffer_len, int flags) +{ + long r = syscall(SYS_getrandom, buffer, buffer_len, flags); + + if (r < 0) + return lwan_getentropy_fallback(buffer, buffer_len); + + return r; +} +#elif defined(LWAN_HAVE_GETENTROPY) +long int lwan_getentropy(void *buffer, size_t buffer_len, int flags) +{ + (void)flags; + + if (!getentropy(buffer, buffer_len)) + return 0; + + return lwan_getentropy_fallback(buffer, buffer_len); +} +#else +long int lwan_getentropy(void *buffer, size_t buffer_len, int flags) +{ + (void)flags; + return lwan_getentropy_fallback(buffer, buffer_len); +} +#endif + +static inline int isalpha_neutral(char c) +{ + /* Use this instead of isalpha() from ctype.h because they consider + * the current locale. This assumes CHAR_BIT == 8. */ + static const unsigned char table[32] = { + 0, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 7, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + unsigned char uc = (unsigned char)c; + return table[uc >> 3] & 1 << (uc & 7); +} + +bool strcaseequal_neutral_len(const char *a, const char *b, size_t len) +{ + while (len--) { + char ca = *a++; + char cb = *b++; + + /* See which bits are different in either character */ + switch (ca ^ cb) { + case 0: /* ca and cb are the same: advance */ + if (ca == '\0') { + /* If `ca` is 0 here, then cb must be 0 too, so we don't + * need to check both. */ + return true; + } + continue; + case 32: /* Only 5th bit is set: advance if either are uppercase + * ASCII characters, but differ in case only */ + /* If either is an uppercase ASCII character, then move on */ + if (isalpha_neutral(ca) || isalpha_neutral(cb)) + continue; + /* Fallthrough */ + default: + return false; + } + } + + assert((ssize_t)len < 0); + return true; +} + +ALWAYS_INLINE bool strcaseequal_neutral(const char *a, const char *b) +{ + return strcaseequal_neutral_len(a, b, SIZE_MAX); +} + +LWAN_SELF_TEST(strcaseequal_neutral) +{ + assert(strcaseequal_neutral("LWAN", "lwan") == true); + assert(strcaseequal_neutral("LwAn", "lWaN") == true); + assert(strcaseequal_neutral("SomE-HeaDer", "some-header") == true); + + assert(strcaseequal_neutral("SomE-HeaDeP", "some-header") == false); + assert(strcaseequal_neutral("LwAN", "lwam") == false); + assert(strcaseequal_neutral("LwAn", "lWaM") == false); + + assert(strcaseequal_neutral_len("Host: localhost:8080", "Host", 4) == true); + assert(strcaseequal_neutral_len("Host", "Host: localhost:8080", 4) == true); + assert(strcaseequal_neutral_len("Host", "Hosh: not-localhost:1234", 4) == false); + assert(strcaseequal_neutral_len("Host: something-else:443", "Host: localhost:8080", 4) == true); + + static_assert(CHAR_BIT == 8, "sane CHAR_BIT value"); + static_assert('*' == 42, "ASCII character set"); + static_assert('0' == 48, "ASCII character set"); + static_assert('a' == 97, "ASCII character set"); +} + +#ifndef LWAN_HAVE_STPCPY +char *stpncpy(char *restrict dst, const char *restrict src, size_t sz) +{ + /* Implementation from the Linux stpcpy(3) man page. */ + char *p = mempcpy(dst, src, sz); + *p = 0; + return p; +} + +char *stpcpy(char *restrict dst, const char *restrict src) +{ + return stpncpy(dst, src, strlen(src)); +} +#endif diff --git a/src/lib/missing/assert.h b/src/lib/missing/assert.h new file mode 100644 index 000000000..be1916a62 --- /dev/null +++ b/src/lib/missing/assert.h @@ -0,0 +1,42 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include_next + +#ifndef MISSING_ASSERT_H +#define MISSING_ASSERT_H + +#undef static_assert +#if defined(LWAN_HAVE_STATIC_ASSERT) +# define static_assert(expr, msg) _Static_assert(expr, msg) +#else +# define static_assert(expr, msg) +#endif + +/* Macro to enable self-test on startup in debug builds. + * Details: https://tia.mat.br/posts/2023/12/11/self-test.html */ +#if defined(NDEBUG) +#define LWAN_SELF_TEST(name) \ + __attribute__((unused)) static void self_test_##name(void) +#else +#define LWAN_SELF_TEST(name) \ + __attribute__((constructor)) static void self_test_##name(void) +#endif + +#endif /* MISSING_ASSERT_H */ diff --git a/src/lib/missing/endian.h b/src/lib/missing/endian.h new file mode 100644 index 000000000..210522ebf --- /dev/null +++ b/src/lib/missing/endian.h @@ -0,0 +1,129 @@ +// This file originates to a work of Mathias Panzenböck, portable_endian.h, of +// which he puts in public domain. +// +// License text was as next. +// +// "License": Public Domain +// I, Mathias Panzenböck, place this file hereby into the public domain. Use +// it at your own risk for whatever you like. In case there are jurisdictions +// that don't support putting things in the public domain you can also consider +// it to be "dual licensed" under the BSD, MIT and Apache licenses, if you want +// to. This code is trivial anyway. Consider it an example on how to get the +// endian conversion functions on different platforms. + +#pragma once + +#if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && \ + !defined(__WINDOWS__) + +#define __WINDOWS__ + +#endif + +#if defined(__linux__) || defined(__CYGWIN__) + +#include_next + +#elif defined(__APPLE__) + +#include + +#define htobe16(x) OSSwapHostToBigInt16(x) +#define htole16(x) OSSwapHostToLittleInt16(x) +#define be16toh(x) OSSwapBigToHostInt16(x) +#define le16toh(x) OSSwapLittleToHostInt16(x) + +#define htobe32(x) OSSwapHostToBigInt32(x) +#define htole32(x) OSSwapHostToLittleInt32(x) +#define be32toh(x) OSSwapBigToHostInt32(x) +#define le32toh(x) OSSwapLittleToHostInt32(x) + +#define htobe64(x) OSSwapHostToBigInt64(x) +#define htole64(x) OSSwapHostToLittleInt64(x) +#define be64toh(x) OSSwapBigToHostInt64(x) +#define le64toh(x) OSSwapLittleToHostInt64(x) + +#define __BYTE_ORDER BYTE_ORDER +#define __BIG_ENDIAN BIG_ENDIAN +#define __LITTLE_ENDIAN LITTLE_ENDIAN +#define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__OpenBSD__) + +#include + +#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) + +#include + +#ifndef be16toh +#define be16toh(x) betoh16(x) +#define le16toh(x) letoh16(x) +#endif + +#ifndef be32toh +#define be32toh(x) betoh32(x) +#define le32toh(x) letoh32(x) +#endif + +#ifndef be64toh +#define be64toh(x) betoh64(x) +#define le64toh(x) letoh64(x) +#endif + +#elif defined(__WINDOWS__) + +#include +#include + +#if BYTE_ORDER == LITTLE_ENDIAN + +#define htobe16(x) htons(x) +#define htole16(x) (x) +#define be16toh(x) ntohs(x) +#define le16toh(x) (x) + +#define htobe32(x) htonl(x) +#define htole32(x) (x) +#define be32toh(x) ntohl(x) +#define le32toh(x) (x) + +#define htobe64(x) htonll(x) +#define htole64(x) (x) +#define be64toh(x) ntohll(x) +#define le64toh(x) (x) + +#elif BYTE_ORDER == BIG_ENDIAN + +/* that would be xbox 360 */ +#define htobe16(x) (x) +#define htole16(x) __builtin_bswap16(x) +#define be16toh(x) (x) +#define le16toh(x) __builtin_bswap16(x) + +#define htobe32(x) (x) +#define htole32(x) __builtin_bswap32(x) +#define be32toh(x) (x) +#define le32toh(x) __builtin_bswap32(x) + +#define htobe64(x) (x) +#define htole64(x) __builtin_bswap64(x) +#define be64toh(x) (x) +#define le64toh(x) __builtin_bswap64(x) + +#else + +#error byte order not supported + +#endif + +#define __BYTE_ORDER BYTE_ORDER +#define __BIG_ENDIAN BIG_ENDIAN +#define __LITTLE_ENDIAN LITTLE_ENDIAN +#define __PDP_ENDIAN PDP_ENDIAN + +#else + +#error platform not supported + +#endif diff --git a/src/lib/missing/fcntl.h b/src/lib/missing/fcntl.h new file mode 100644 index 000000000..3c36adb3e --- /dev/null +++ b/src/lib/missing/fcntl.h @@ -0,0 +1,62 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_FCNTL_H +#define MISSING_FCNTL_H + +#ifndef O_NOATIME +#define O_NOATIME 0 +#endif + +#ifndef O_PATH +#define O_PATH 0 +#endif + +#if defined(__linux__) + +/* Definitions for O_TMPFILE obtained from glibc/Linux kernel. */ +#if !defined(__O_TMPFILE) +#if defined(__alpha__) +#define __O_TMPFILE 0100000000 +#elif defined(__sparc__) || defined(__sparc64__) +#define __O_TMPFILE 0200000000 +#elif defined(__parisc__) || defined(__hppa__) +#define __O_TMPFILE 0400000000 +#else +#define __O_TMPFILE 020000000 +#endif +#endif + +/* a horrid kludge trying to make sure that this will fail on old kernels */ +#ifndef O_TMPFILE +#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY) +#endif + +#endif /* __linux__ */ + +#ifndef LWAN_HAVE_READAHEAD +#include + +ssize_t readahead(int fd, off_t offset, size_t count); +#endif + +#endif /* MISSING_FCNTL_H */ diff --git a/src/lib/missing/ioprio.h b/src/lib/missing/ioprio.h new file mode 100644 index 000000000..a2f20a464 --- /dev/null +++ b/src/lib/missing/ioprio.h @@ -0,0 +1,50 @@ +/* + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +#include +#include + +#if defined(__linux__) && defined(SYS_ioprio_set) + +#define IOPRIO_WHO_PROCESS 1 +#define IOPRIO_CLASS_IDLE 3 +#define IOPRIO_PRIO_VALUE(class, data) (((class) << 13) | (data)) + +static inline int ioprio_set(int which, int who, int ioprio) +{ + return (int)syscall(SYS_ioprio_set, which, who, ioprio); +} + +#else + +#define IOPRIO_WHO_PROCESS 0 +#define IOPRIO_PRIO_VALUE(arg1, arg2) 0 +#define IOPRIO_CLASS_IDLE 0 + +static inline int ioprio_set(int which __attribute__((unused)), + int who __attribute__((unused)), + int ioprio __attribute__((unused))) +{ + return 0; +} + +#endif diff --git a/src/lib/missing/libproc.h b/src/lib/missing/libproc.h new file mode 100644 index 000000000..8ab3bdfea --- /dev/null +++ b/src/lib/missing/libproc.h @@ -0,0 +1,28 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#if defined(__APPLE__) +#include_next +#else +#include +#include + +int proc_pidpath(pid_t pid, void *buffer, size_t buffersize); +#endif diff --git a/src/lib/missing/limits.h b/src/lib/missing/limits.h new file mode 100644 index 000000000..5c9a85520 --- /dev/null +++ b/src/lib/missing/limits.h @@ -0,0 +1,59 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_LIMITS_H +#define MISSING_LIMITS_H + +#ifndef PATH_MAX +# define PATH_MAX 4096 +#endif + +#ifndef OPEN_MAX +# include +# ifdef NOFILE +# define OPEN_MAX NOFILE +# else +# define OPEN_MAX 65535 +# endif +#endif + +#ifndef OFF_MAX +# include +#if SIZE_MAX == ULLONG_MAX +# define OFF_MAX LLONG_MAX +#else +# define OFF_MAX LONG_MAX +#endif +#endif + +#ifndef PAGE_SIZE +# include +# ifndef PAGE_SIZE +# ifdef EXEC_PAGESIZE +# define PAGE_SIZE EXEC_PAGESIZE +# else +# define PAGE_SIZE 4096 +# endif +# endif +#endif + +#endif /* MISSING_LIMITS_H */ diff --git a/src/lib/missing/linux/capability.h b/src/lib/missing/linux/capability.h new file mode 100644 index 000000000..53befa170 --- /dev/null +++ b/src/lib/missing/linux/capability.h @@ -0,0 +1,57 @@ +/* + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#ifndef __MISSING_CAPABILITY_H__ +#define __MISSING_CAPABILITY_H__ + +#if defined(__linux__) && defined(LWAN_HAVE_LINUX_CAPABILITY) + +#include_next +#include +#include +#include + +static inline int capset(struct __user_cap_header_struct *header, + struct __user_cap_data_struct *data) +{ +#if defined(SYS_capset) + return (int)syscall(SYS_capset, header, data); +#else + return 0; +#endif +} + +#else +struct __user_cap_data_struct { + unsigned int effective, permitted, inheritable; +}; + +struct __user_cap_header_struct { +#define _LINUX_CAPABILITY_VERSION_1 0 + unsigned int version; + int pid; +}; + +int capset(struct __user_cap_header_struct *header, + struct __user_cap_data_struct *data); + +#endif + +#endif /* __MISSING_CAPABILITY_H__ */ diff --git a/src/lib/missing/pthread.h b/src/lib/missing/pthread.h new file mode 100644 index 000000000..505fe8ef4 --- /dev/null +++ b/src/lib/missing/pthread.h @@ -0,0 +1,50 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_PTHREAD_H +#define MISSING_PTHREAD_H + +#ifndef LWAN_HAVE_PTHREADBARRIER +typedef int pthread_barrierattr_t; +typedef struct pthread_barrier { + unsigned int count; + unsigned int in; + pthread_mutex_t mutex; + pthread_cond_t cond; +} pthread_barrier_t; + +int pthread_barrier_init(pthread_barrier_t *restrict barrier, + const pthread_barrierattr_t *restrict attr, + unsigned int count); +int pthread_barrier_destroy(pthread_barrier_t *barrier); +int pthread_barrier_wait(pthread_barrier_t *barrier); +#endif + +#if defined(__FreeBSD__) || defined(__OpenBSD__) +#include +#endif + +#ifndef LWAN_HAVE_PTHREAD_SET_NAME_NP +int pthread_set_name_np(pthread_t thread, const char *name); +#endif + +#endif /* MISSING_PTHREAD_H */ diff --git a/src/lib/missing/pwd.h b/src/lib/missing/pwd.h new file mode 100644 index 000000000..e93467788 --- /dev/null +++ b/src/lib/missing/pwd.h @@ -0,0 +1,30 @@ +/* + * lwan - web server + * Copyright (c) 2019 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_PWD_H +#define MISSING_PWD_H + +#ifndef NSS_BUFLEN_PASSWD +#define NSS_BUFLEN_PASSWD 1024 +#endif + +#endif /* MISSING_PWD_H */ diff --git a/src/lib/missing/stdio.h b/src/lib/missing/stdio.h new file mode 100644 index 000000000..ecc5b5d5a --- /dev/null +++ b/src/lib/missing/stdio.h @@ -0,0 +1,30 @@ +/* + * lwan - web server + * Copyright (c) 2019 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_STDIO_H +#define MISSING_STDIO_H + +#if !defined(LWAN_HAVE_FWRITE_UNLOCKED) +size_t fwrite_unlocked(const void *ptr, size_t size, size_t n, FILE *stream); +#endif + +#endif /* MISSING_STDIO_H */ diff --git a/src/lib/missing/stdlib.h b/src/lib/missing/stdlib.h new file mode 100644 index 000000000..5a191df8e --- /dev/null +++ b/src/lib/missing/stdlib.h @@ -0,0 +1,61 @@ +/* + * lwan - web server + * Copyright (c) 2017 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_STDLIB_H +#define MISSING_STDLIB_H + +#if defined(__GLIBC__) +#include +#if defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 17) +#define LWAN_HAVE_SECURE_GETENV +#endif +#if defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 19) +#define LWAN_HAVE_MKOSTEMPS +#endif +#if defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 7) +#define LWAN_HAVE_CORRECT_UMASK_TMPFILE +#endif +#endif + +#if !defined(LWAN_HAVE_SECURE_GETENV) +static inline char *secure_getenv(const char *name) { return getenv(name); } +#endif + +#if !defined(LWAN_HAVE_MKOSTEMP) +int mkostemp(char *tmpl, int flags); +#endif + +#if defined(LWAN_HAVE_CORRECT_UMASK_TMPFILE) +#define umask_for_tmpfile(mask_) \ + ({ \ + (void)(mask_); \ + 0U; \ + }) +#else +#define umask_for_tmpfile(mask_) umask(mask_) +#endif + +#if !defined(LWAN_HAVE_REALLOCARRAY) +void *reallocarray(void *optr, size_t nmemb, size_t size); +#endif + +#endif /* MISSING_STDLIB_H */ diff --git a/src/lib/missing/string.h b/src/lib/missing/string.h new file mode 100644 index 000000000..be42e986d --- /dev/null +++ b/src/lib/missing/string.h @@ -0,0 +1,82 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_STRING_H +#define MISSING_STRING_H + +#include + +#define strndupa_impl(s, l) \ + ({ \ + char *strndupa_tmp_s = alloca(l + 1); \ + strndupa_tmp_s[l] = '\0'; \ + strncpy(strndupa_tmp_s, s, l); \ + }) + +#ifndef strndupa +#define strndupa(s, l) strndupa_impl((s), strnlen((s), (l))) +#undef NEED_ALLOCA_H +#define NEED_ALLOCA_H +#endif + +#ifndef strdupa +#define strdupa(s) strndupa((s), strlen(s)) +#undef NEED_ALLOCA_H +#define NEED_ALLOCA_H +#endif + +#ifdef NEED_ALLOCA_H +#undef NEED_ALLOCA_H +#ifdef LWAN_HAVE_ALLOCA_H +#include +#else +#include +#endif +#endif + +#ifndef LWAN_HAVE_MEMPCPY +void *mempcpy(void *dest, const void *src, size_t len); +#endif + +#ifndef LWAN_HAVE_MEMRCHR +void *memrchr(const void *s, int c, size_t n); +#endif + +#ifndef LWAN_HAVE_STPCPY +char *stpcpy(char *restrict dst, const char *restrict src); +char *stpncpy(char *restrict dst, const char *restrict src, size_t sz); +#endif + +static inline int streq(const char *a, const char *b) +{ + return strcmp(a, b) == 0; +} + +static inline void *mempmove(void *dest, const void *src, size_t len) +{ + return (char *)memmove(dest, src, len) + len; +} + +bool strcaseequal_neutral(const char *a, const char *b); +bool strcaseequal_neutral_len(const char *a, const char *b, size_t len); + +#endif /* MISSING_STRING_H */ diff --git a/src/lib/missing/sys/epoll.h b/src/lib/missing/sys/epoll.h new file mode 100644 index 000000000..92d81a4a0 --- /dev/null +++ b/src/lib/missing/sys/epoll.h @@ -0,0 +1,58 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#if !defined(LWAN_HAVE_EPOLL) +#pragma once +#include + +enum epoll_event_flag { + EPOLLIN = 1 << 0, + EPOLLOUT = 1 << 1, + EPOLLONESHOT = 1 << 2, + EPOLLRDHUP = 1 << 3, + EPOLLERR = 1 << 4, + EPOLLET = 1 << 5, + EPOLLHUP = EPOLLRDHUP +}; + +enum epoll_op { EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL }; + +enum epoll_create_flags { EPOLL_CLOEXEC = 1 << 0 }; + +struct epoll_event { + uint32_t events; + union { + void *ptr; + int fd; + uint32_t u32; + uint64_t u64; + } data; +}; + +int epoll_create1(int flags); +int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); +int epoll_wait(int epfd, + struct epoll_event *events, + int maxevents, + int timeout); + +#else +#include_next +#endif diff --git a/src/lib/missing/sys/ioctl.h b/src/lib/missing/sys/ioctl.h new file mode 100644 index 000000000..2f2cee344 --- /dev/null +++ b/src/lib/missing/sys/ioctl.h @@ -0,0 +1,37 @@ +/* + * lwan - web server + * Copyright (c) 2019 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_SYS_IOCTL_H +#define MISSING_SYS_IOCTL_H + +#ifndef TIOCOUTQ + +#ifdef FIONWRITE /* FreeBSD, ... */ +#define TIOCOUTQ FIONWRITE +#else +#define TIOCOUTQ 0 +#endif /* FIONWRITE */ + +#endif /* TIOCOUTQ */ + +#endif /* MISSING_SYS_IOCTL_H */ + diff --git a/src/lib/missing/sys/mman.h b/src/lib/missing/sys/mman.h new file mode 100644 index 000000000..c1da95403 --- /dev/null +++ b/src/lib/missing/sys/mman.h @@ -0,0 +1,30 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef _MISSING_MMAN_H_ +#define _MISSING_MMAN_H_ + +#ifndef MAP_HUGETLB +#define MAP_HUGETLB 0 +#endif + +#endif /* _MISSING_MMAN_H_ */ diff --git a/src/lib/missing/sys/sendfile.h b/src/lib/missing/sys/sendfile.h new file mode 100644 index 000000000..35ac211a3 --- /dev/null +++ b/src/lib/missing/sys/sendfile.h @@ -0,0 +1,28 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#if defined(__FreeBSD__) || defined(__APPLE__) +#include +#elif defined(__OpenBSD__) +/* OpenBSD has no sendfile(); implement the Lwan wrapper directly in + * lwan-io-wrappers.c */ +#else +#include_next +#endif diff --git a/src/lib/missing/sys/socket.h b/src/lib/missing/sys/socket.h new file mode 100644 index 000000000..2ec51d878 --- /dev/null +++ b/src/lib/missing/sys/socket.h @@ -0,0 +1,50 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_SYS_SOCKET_H +#define MISSING_SYS_SOCKET_H + +#ifndef MSG_MORE +#define MSG_MORE 0 +#endif + +#ifndef MSG_NOSIGNAL +#define MSG_NOSIGNAL 0 +#endif + +#ifndef SOCK_CLOEXEC +#define SOCK_CLOEXEC 0 +#endif + +#ifndef SOCK_NONBLOCK +#define SOCK_NONBLOCK 00004000 +#endif + +#ifndef SOMAXCONN +#define SOMAXCONN 128 +#endif + +#ifndef LWAN_HAVE_ACCEPT4 +int accept4(int sock, struct sockaddr *addr, socklen_t *addrlen, int flags); +#endif + +#endif /* MISSING_SYS_SOCKET_H */ diff --git a/src/lib/missing/sys/syscall.h b/src/lib/missing/sys/syscall.h new file mode 100644 index 000000000..6384a1e72 --- /dev/null +++ b/src/lib/missing/sys/syscall.h @@ -0,0 +1,23 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#ifndef __CYGWIN__ +#include_next +#endif diff --git a/src/lib/missing/sys/sysctl.h b/src/lib/missing/sys/sysctl.h new file mode 100644 index 000000000..be07a41e2 --- /dev/null +++ b/src/lib/missing/sys/sysctl.h @@ -0,0 +1,24 @@ +/* + * lwan - web server + * Copyright (c) 2025 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) +#include_next +#endif + diff --git a/src/lib/missing/sys/types.h b/src/lib/missing/sys/types.h new file mode 100644 index 000000000..3d06bec38 --- /dev/null +++ b/src/lib/missing/sys/types.h @@ -0,0 +1,30 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_SYS_TYPES_H +#define MISSING_SYS_TYPES_H + +#ifndef LWAN_HAVE_GETTID +pid_t gettid(void); +#endif + +#endif diff --git a/src/lib/missing/sys/vfs.h b/src/lib/missing/sys/vfs.h new file mode 100644 index 000000000..a7e8f431c --- /dev/null +++ b/src/lib/missing/sys/vfs.h @@ -0,0 +1,44 @@ +/* + * lwan - web server + * Copyright (c) 2020 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__APPLE__) +#include +#include +#elif defined(__linux__) +#include_next +#include +#endif + +#ifndef _MISSING_VFS_H_ +#define _MISSING_VFS_H_ + +#if !defined(LWAN_HAVE_STATFS) +struct statfs { + int f_type; +}; + +int statfs(const char *path, struct statfs *buf); +#endif + +#ifndef TMPFS_MAGIC +#define TMPFS_MAGIC 0xbebacafe +#endif + +#endif /* _MISSING_VFS_H_ */ diff --git a/src/lib/missing/time.h b/src/lib/missing/time.h new file mode 100644 index 000000000..3deb4d769 --- /dev/null +++ b/src/lib/missing/time.h @@ -0,0 +1,47 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_TIME_H +#define MISSING_TIME_H + +#ifndef LWAN_HAVE_CLOCK_GETTIME +typedef int clockid_t; +int clock_gettime(clockid_t clk_id, struct timespec *ts); +#endif + +#if !defined(CLOCK_MONOTONIC_COARSE) && defined(CLOCK_MONOTONIC_FAST) +# define CLOCK_MONOTONIC_COARSE CLOCK_MONOTONIC_FAST /* FreeBSD */ +#elif !defined(CLOCK_MONOTONIC_COARSE) && defined(CLOCK_MONOTONIC_RAW_APPROX) +# define CLOCK_MONOTONIC_COARSE CLOCK_MONOTONIC_RAW_APPROX /* New-ish macOS */ +#elif !defined(CLOCK_MONOTONIC_COARSE) +# if defined(CLOCK_MONOTONIC) +# define CLOCK_MONOTONIC_COARSE CLOCK_MONOTONIC +# else +# define CLOCK_MONOTONIC_COARSE 0xbebac0ca /* Old macOS, usually */ +# endif +#endif + +#if !defined(CLOCK_MONOTONIC) +#define CLOCK_MONOTONIC 0xbebacafe +#endif + +#endif /* MISSING_TIME_H */ diff --git a/src/lib/missing/unistd.h b/src/lib/missing/unistd.h new file mode 100644 index 000000000..8257b2ee5 --- /dev/null +++ b/src/lib/missing/unistd.h @@ -0,0 +1,50 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include_next + +#ifndef MISSING_UNISTD_H +#define MISSING_UNISTD_H + +#ifndef LWAN_HAVE_PIPE2 +int pipe2(int pipefd[2], int flags); +#endif + +#if defined(__APPLE__) +int setresuid(uid_t ruid, uid_t euid, uid_t suid) + __attribute__((warn_unused_result)); +int setresgid(gid_t rgid, gid_t egid, gid_t sgid) + __attribute__((warn_unused_result)); +int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid) + __attribute__((warn_unused_result)); +int getresgid(gid_t *rgid, gid_t *egid, gid_t *sgid) + __attribute__((warn_unused_result)); +#endif + +#if !defined(LWAN_HAVE_GET_CURRENT_DIR_NAME) +char *get_current_dir_name(void); +#endif + +#if defined(__APPLE__) +/* getrandom() is defined in on macOS */ +#include +#endif + +#endif /* MISSING_UNISTD_H */ diff --git a/common/patterns.c b/src/lib/patterns.c similarity index 98% rename from common/patterns.c rename to src/lib/patterns.c index 93a2b9ecc..cbd6dfaea 100644 --- a/common/patterns.c +++ b/src/lib/patterns.c @@ -1,4 +1,4 @@ -/* $OpenBSD: patterns.c,v 1.3 2015/06/26 10:07:48 semarie Exp $ */ +/* $OpenBSD: patterns.c,v 1.5 2016/02/14 18:20:59 semarie Exp $ */ /* * Copyright (c) 2015 Reyk Floeter @@ -26,7 +26,7 @@ /* * Derived from Lua 5.3.1: - * $Id: patterns.c,v 1.3 2015/06/26 10:07:48 semarie Exp $ + * $Id: patterns.c,v 1.5 2016/02/14 18:20:59 semarie Exp $ * Standard library for string operations and pattern-matching */ @@ -38,6 +38,7 @@ #include #include "patterns.h" +#include "lwan.h" #define uchar(c) ((unsigned char)(c)) /* macro to 'unsign' a char */ #define CAP_UNFINISHED (-1) @@ -322,7 +323,7 @@ match(struct match_state *ms, const char *s, const char *p) const char *ep, *res; char previous; - if (ms->matchdepth-- == 0) { + if (UNLIKELY(ms->matchdepth-- == 0)) { match_error(ms, "pattern too complex"); return (NULL); } @@ -693,5 +694,6 @@ str_match_free(struct str_match *m) for (i = 0; i < m->sm_nmatch; i++) free(m->sm_match[i]); free(m->sm_match); + m->sm_match = NULL; m->sm_nmatch = 0; } diff --git a/common/patterns.h b/src/lib/patterns.h similarity index 97% rename from common/patterns.h rename to src/lib/patterns.h index fd1011d70..9517d3ea9 100644 --- a/common/patterns.h +++ b/src/lib/patterns.h @@ -20,7 +20,6 @@ #define PATTERNS_H #include -#include #define MAXCAPTURES 32 /* Max no. of allowed captures in pattern */ #define MAXCCALLS 200 /* Max recusion depth in pattern matching */ @@ -36,12 +35,10 @@ struct str_match { int sm_nmatch; /* number of elements in array */ }; -__BEGIN_DECLS int str_find(const char *, const char *, struct str_find *, size_t, const char **); int str_match(const char *, const char *, struct str_match *, const char **); void str_match_free(struct str_match *); -__END_DECLS #endif /* PATTERNS_H */ diff --git a/common/realpathat.c b/src/lib/realpathat.c similarity index 92% rename from common/realpathat.c rename to src/lib/realpathat.c index e0b3e5f2b..d3a2a5358 100644 --- a/common/realpathat.c +++ b/src/lib/realpathat.c @@ -1,5 +1,5 @@ /* -at() version of realpath() - Copyright (C) 2012 Leandro A. F. Pereira + Copyright (C) 2012 L. A. F. Pereira Based on: return the canonical absolute name of a given file. Copyright (C) 1996-2002,2004,2005,2006,2008 Free Software Foundation, Inc. @@ -32,7 +32,7 @@ #include #include -#include "lwan.h" +#include "lwan-private.h" char * realpathat2(int dirfd, char *dirfdpath, const char *name, char *resolved, @@ -44,12 +44,6 @@ realpathat2(int dirfd, char *dirfdpath, const char *name, char *resolved, ptrdiff_t dirfdlen; char *pathat; - /* If any of the additional parameters are null, or if the name to - resolve the real path is an absolute path, use the standard - realpath() routine. */ - if (UNLIKELY(dirfd < 0 || dirfdpath == NULL || name[0] == '/')) - return realpath(name, resolved); - if (UNLIKELY(name == NULL)) { /* As per Single Unix Specification V2 we must return an error if either parameter is a null pointer. We extend this to allow @@ -59,6 +53,12 @@ realpathat2(int dirfd, char *dirfdpath, const char *name, char *resolved, return NULL; } + /* If any of the additional parameters are null, or if the name to + resolve the real path is an absolute path, use the standard + realpath() routine. */ + if (UNLIKELY(dirfd < 0 || dirfdpath == NULL || name[0] == '/')) + return realpath(name, resolved); + if (name[0] == '\0') { if (UNLIKELY(fstat(dirfd, st) < 0)) return NULL; @@ -76,7 +76,7 @@ realpathat2(int dirfd, char *dirfdpath, const char *name, char *resolved, rpath_limit = rpath + PATH_MAX; strcpy(rpath, dirfdpath); - dest = rawmemchr(rpath, '\0'); + dest = rpath + strlen(rpath); dirfdlen = dest - rpath; for (start = end = name; *start; start = end) { @@ -130,16 +130,17 @@ realpathat2(int dirfd, char *dirfdpath, const char *name, char *resolved, dest = rpath + dest_offset; } - dest = mempcpy(dest, start, (size_t)(end - start)); + dest = mempmove(dest, start, (size_t)(end - start)); *dest = '\0'; - if (LIKELY(!strncmp(rpath, dirfdpath, (size_t)dirfdlen))) { - pathat = rpath + dirfdlen + 1; - if (*pathat == '\0') - pathat = rpath; - } else { + if ((dirfdlen == 1 && *dirfdpath == '/') || + strncmp(rpath, dirfdpath, (size_t)dirfdlen)) { pathat = rpath; + } else { + pathat = rpath + dirfdlen; } + if (UNLIKELY(*pathat == '\0')) + pathat = rpath; if (UNLIKELY(fstatat(dirfd, pathat, st, AT_SYMLINK_NOFOLLOW) < 0)) goto error; @@ -159,7 +160,7 @@ realpathat2(int dirfd, char *dirfdpath, const char *name, char *resolved, buf[n] = '\0'; len = strlen(end); - if (UNLIKELY((long int) (n + (long int)len) >= PATH_MAX)) { + if (UNLIKELY((size_t)(PATH_MAX - n) <= len)) { errno = ENAMETOOLONG; goto error; } diff --git a/common/realpathat.h b/src/lib/realpathat.h similarity index 96% rename from common/realpathat.h rename to src/lib/realpathat.h index 4fc1dd947..871c79896 100644 --- a/common/realpathat.h +++ b/src/lib/realpathat.h @@ -1,5 +1,5 @@ /* -at() version of realpath() - Copyright (C) 2012 Leandro A. F. Pereira + Copyright (C) 2012 L. A. F. Pereira Based on: return the canonical absolute name of a given file. Copyright (C) 1996-2002,2004,2005,2006,2008 Free Software Foundation, Inc. diff --git a/src/lib/response-template.html b/src/lib/response-template.html new file mode 100644 index 000000000..1490cd191 --- /dev/null +++ b/src/lib/response-template.html @@ -0,0 +1,24 @@ + + + +
+
+

{{short_message}}

+
+

{{long_message}}

+
+
+
+ + diff --git a/src/lib/ringbuffer.h b/src/lib/ringbuffer.h new file mode 100644 index 000000000..21086213c --- /dev/null +++ b/src/lib/ringbuffer.h @@ -0,0 +1,141 @@ +/* + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +/* + * Inspired by blog post by Juho Snellman + * https://www.snellman.net/blog/archive/2016-12-13-ring-buffers/ + */ + +#pragma once + +#include +#include +#include +#include + +#define DEFINE_RING_BUFFER_TYPE(type_name_, element_type_, size_) \ + static_assert((size_) && !((size_) & ((size_)-1)), \ + "size is a power of two"); \ + \ + struct type_name_ { \ + uint32_t read, write; \ + element_type_ array[size_]; \ + }; \ + \ + __attribute__((unused)) static inline uint32_t type_name_##_mask( \ + uint32_t value) \ + { \ + return value & ((size_)-1); \ + } \ + \ + __attribute__((unused)) static inline uint32_t type_name_##_size( \ + const struct type_name_ *rb) \ + { \ + return rb->write - rb->read; \ + } \ + \ + __attribute__((unused)) static inline bool type_name_##_full( \ + const struct type_name_ *rb) \ + { \ + return type_name_##_size(rb) == (size_); \ + } \ + \ + __attribute__((unused)) static inline bool type_name_##_empty( \ + const struct type_name_ *rb) \ + { \ + return rb->write == rb->read; \ + } \ + \ + __attribute__((unused)) static inline void type_name_##_init( \ + struct type_name_ *rb) \ + { \ + rb->write = rb->read = 0; \ + } \ + \ + __attribute__((unused)) static inline void type_name_##_put( \ + struct type_name_ *rb, const element_type_ *e) \ + { \ + assert(!type_name_##_full(rb)); \ + memcpy(&rb->array[type_name_##_mask(rb->write++)], e, sizeof(*e)); \ + } \ + \ + __attribute__((unused)) static inline void type_name_##_put_copy( \ + struct type_name_ *rb, element_type_ e) \ + { \ + assert(!type_name_##_full(rb)); \ + rb->array[type_name_##_mask(rb->write++)] = e; \ + } \ + \ + __attribute__((unused)) static inline bool type_name_##_try_put_copy( \ + struct type_name_ *rb, element_type_ e) \ + { \ + if (type_name_##_full(rb)) \ + return false; \ + \ + rb->array[type_name_##_mask(rb->write++)] = e; \ + return true; \ + } \ + \ + __attribute__((unused)) static inline bool type_name_##_try_put( \ + struct type_name_ *rb, const element_type_ *e) \ + { \ + if (type_name_##_full(rb)) \ + return false; \ + \ + memcpy(&rb->array[type_name_##_mask(rb->write++)], e, sizeof(*e)); \ + return true; \ + } \ + \ + __attribute__((unused)) static inline element_type_ type_name_##_get( \ + struct type_name_ *rb) \ + { \ + assert(!type_name_##_empty(rb)); \ + return rb->array[type_name_##_mask(rb->read++)]; \ + } \ + \ + __attribute__((unused)) static inline void type_name_##_consume( \ + struct type_name_ *rb, element_type_ *buffer, uint32_t entries) \ + { \ + assert(type_name_##_size(rb) >= entries); \ + const uint32_t mask = type_name_##_mask(rb->read); \ + if (mask && entries > mask) { \ + memcpy(buffer, &rb->array[rb->read], \ + sizeof(*buffer) * (entries - mask)); \ + memcpy(buffer + mask, &rb->array[0], sizeof(*buffer) * mask); \ + } else { \ + memcpy(buffer, &rb->array[rb->read], sizeof(*buffer) * entries); \ + } \ + rb->read += entries; \ + } \ + \ + __attribute__((unused)) static inline element_type_ *type_name_##_get_ptr( \ + struct type_name_ *rb) \ + { \ + assert(!type_name_##_empty(rb)); \ + return &rb->array[type_name_##_mask(rb->read++)]; \ + } \ + \ + __attribute__((unused)) static inline element_type_ \ + *type_name_##_get_ptr_or_null(struct type_name_ *rb) \ + { \ + return type_name_##_empty(rb) \ + ? NULL \ + : &rb->array[type_name_##_mask(rb->read++)]; \ + } diff --git a/common/sd-daemon.c b/src/lib/sd-daemon.c similarity index 55% rename from common/sd-daemon.c rename to src/lib/sd-daemon.c index 2f7c2789c..c9274dc0c 100644 --- a/common/sd-daemon.c +++ b/src/lib/sd-daemon.c @@ -21,20 +21,30 @@ ***/ #define _GNU_SOURCE +#include #include #include +#include #include #include +#include +#include #include +#include #include #include "sd-daemon.h" +#include "lwan-config.h" + +static void unsetenv_listen_vars(void) { + unsetenv("LISTEN_PID"); + unsetenv("LISTEN_FDS"); + unsetenv("LISTEN_FDNAMES"); +} int sd_listen_fds(int unset_environment) { - int r, fd; + int n, l, r, fd; const char *e; - char *p = NULL; - unsigned long l; e = getenv("LISTEN_PID"); if (!e) { @@ -42,21 +52,14 @@ int sd_listen_fds(int unset_environment) { goto finish; } - errno = 0; - l = strtoul(e, &p, 10); - - if (errno > 0) { - r = -errno; - goto finish; - } - - if (!p || p == e || *p || l == 0) { + l = parse_int(e, -1); + if (l <= 0) { r = -EINVAL; goto finish; } /* Is this for us? */ - if (getpid() != (pid_t) l) { + if (getpid() != (pid_t)l) { r = 0; goto finish; } @@ -67,20 +70,19 @@ int sd_listen_fds(int unset_environment) { goto finish; } - errno = 0; - l = strtoul(e, &p, 10); - - if (errno > 0) { - r = -errno; + n = parse_int(e, -1); + if (!n) { + r = 0; goto finish; } - if (!p || p == e || *p) { + static_assert(SD_LISTEN_FDS_START < INT_MAX, ""); + if (n < 0 || n > INT_MAX - SD_LISTEN_FDS_START) { r = -EINVAL; goto finish; } - for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + (int) l; fd ++) { + for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + (int)n; fd++) { int flags; flags = fcntl(fd, F_GETFD); @@ -98,21 +100,141 @@ int sd_listen_fds(int unset_environment) { } } - r = (int) l; + r = n; finish: - if (unset_environment) { - unsetenv("LISTEN_PID"); - unsetenv("LISTEN_FDS"); + if (unset_environment) + unsetenv_listen_vars(); + + return r; +} + +/* Both strv_extend_n() and strv_split() aren't simple copies of the + * same functions from systemd. These are simplified versions of those + * functions, used only in the sd_listen_fds_with_names() ported from + * newer versions of systemd. + */ +static int strv_extend_n(char ***p, const char *s, int n) { + if (!p) + return -EINVAL; + if (!s) + return -EINVAL; + + *p = calloc((size_t)n, sizeof(char *)); + if (!*p) + return -ENOMEM; + + size_t s_size = strlen(s) + 1; + char *copies = calloc((size_t)(n + 1), s_size); + if (!copies) { + free(*p); + return -ENOMEM; + } + for (int i = 0; i < n; i++) { + char *copy = &copies[(size_t)i * s_size]; + *p[i] = memcpy(copy, s, s_size); + } + + return 0; +} + +static int strv_split(char ***p, const char *value, const char separator) { + char *copy = strdup(value); + if (!copy) + return -ENOMEM; + + int n_split = 1; + for (char *c = copy; *c; ) { + char *sep_pos = strchr(c, separator); + if (!sep_pos) + break; + + n_split++; + c = sep_pos + 1; + } + + *p = calloc((size_t)(n_split + 1), sizeof(char *)); + if (!*p) { + free(copy); + return -ENOMEM; + } + + if (n_split == 1) { + *p[0] = copy; + return 1; } + int i = 0; + for (char *c = copy; *c; ) { + char *sep_pos = strchr(c, separator); + if (!sep_pos) + break; + + *sep_pos = '\0'; + *p[i++] = c; + c = sep_pos + 1; + } + + return n_split; +} + +int sd_listen_fds_with_names(int unset_environment, char ***names) { + char **l = NULL; + bool have_names; + int n_names = 0, n_fds; + const char *e; + int r; + + if (!names) + return sd_listen_fds(unset_environment); + + e = getenv("LISTEN_FDNAMES"); + if (e) { + n_names = strv_split(&l, e, ':'); + if (n_names < 0) { + if (unset_environment) + unsetenv_listen_vars(); + return n_names; + } + + have_names = true; + } else { + have_names = false; + } + + n_fds = sd_listen_fds(unset_environment); + if (n_fds <= 0) { + r = n_fds; + goto fail; + } + + if (have_names) { + if (n_names != n_fds) { + r = -EINVAL; + goto fail; + } + } else { + r = strv_extend_n(&l, "unknown", n_fds); + if (r < 0) + return r; + } + + *names = l; + + return n_fds; + +fail: + free(l); return r; } static int sd_is_socket_internal(int fd, int type, int listening) { struct stat st_fd; - if (fd < 0 || type < 0) + if (fd < 0) + return -EBADF; + + if (type < 0) return -EINVAL; if (fstat(fd, &st_fd) < 0) diff --git a/common/sd-daemon.h b/src/lib/sd-daemon.h similarity index 98% rename from common/sd-daemon.h rename to src/lib/sd-daemon.h index 31170e6b1..7a2277ff4 100644 --- a/common/sd-daemon.h +++ b/src/lib/sd-daemon.h @@ -70,6 +70,7 @@ See sd_listen_fds(3) for more information. */ int sd_listen_fds(int unset_environment); +int sd_listen_fds_with_names(int unset_environment, char ***names); /* Helper call for identifying a passed file descriptor. Returns 1 if @@ -255,3 +256,11 @@ int sd_booted(void); */ int sd_watchdog_enabled(int unset_environment, uint64_t *usec); + +static inline void strv_free(char **p) +{ + if (p) { + free(p[0]); + free(p); + } +} diff --git a/src/lib/servefile-template.html b/src/lib/servefile-template.html new file mode 100644 index 000000000..043173adc --- /dev/null +++ b/src/lib/servefile-template.html @@ -0,0 +1,41 @@ + + +{{rel_path?}} Index of {{rel_path}}{{/rel_path?}} +{{^rel_path?}} Index of /{{/rel_path?}} + + + +{{rel_path?}}

Index of {{rel_path}}

{{/rel_path?}} +{{^rel_path?}}

Index of /

{{/rel_path?}} +{{readme?}}
{{readme}}
{{/readme?}} + + + + + + + + + + + +{{#file_list}} + + + + + + +{{/file_list}} +{{^#file_list}} + + + +{{/file_list}} +
Parent directory
 File nameTypeSize
{{file_list.icon_alt}}{{{file_list.name}}}{{file_list.type}}{{file_list.size}}{{file_list.unit}}
Empty directory.
+ + diff --git a/src/lib/sha1.c b/src/lib/sha1.c new file mode 100644 index 000000000..7f7f275db --- /dev/null +++ b/src/lib/sha1.c @@ -0,0 +1,207 @@ +/* +SHA-1 in C +By Steve Reid +100% Public Domain + +Test Vectors (from FIPS PUB 180-1) +"abc" + A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D +"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1 +A million repetitions of "a" + 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F +*/ + +#include "sha1.h" +#include +#include +#include +#include + +#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) + +/* blk0() and blk() perform the initial expand. */ +/* I got the idea of expanding during the round function from SSLeay */ +#define blk0(i) (block[i] = htobe32(block[i])) +#define blk(i) \ + (block[i & 15] = rol(block[(i + 13) & 15] ^ block[(i + 8) & 15] ^ \ + block[(i + 2) & 15] ^ block[i & 15], \ + 1)) + +/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */ +#define R0(v, w, x, y, z, i) \ + z += ((w & (x ^ y)) ^ y) + blk0(i) + 0x5A827999 + rol(v, 5); \ + w = rol(w, 30); +#define R1(v, w, x, y, z, i) \ + z += ((w & (x ^ y)) ^ y) + blk(i) + 0x5A827999 + rol(v, 5); \ + w = rol(w, 30); +#define R2(v, w, x, y, z, i) \ + z += (w ^ x ^ y) + blk(i) + 0x6ED9EBA1 + rol(v, 5); \ + w = rol(w, 30); +#define R3(v, w, x, y, z, i) \ + z += (((w | x) & y) | (w & x)) + blk(i) + 0x8F1BBCDC + rol(v, 5); \ + w = rol(w, 30); +#define R4(v, w, x, y, z, i) \ + z += (w ^ x ^ y) + blk(i) + 0xCA62C1D6 + rol(v, 5); \ + w = rol(w, 30); + +/* Hash a single 512-bit block. This is the core of the algorithm. */ + +#define STATE(i) state[(index + (i)) % 5] + +static void sha1_transform(uint32_t orig_state[5], const unsigned char buffer[64]) +{ + uint32_t state[5]; + uint32_t block[16]; + int round = 0; + int index = 0; + + memcpy(block, buffer, sizeof(block)); + + /* Copy context->state[] to working vars */ + memcpy(state, orig_state, sizeof(state)); + + /* 4 rounds of 20 operations each. */ + for (; round < 16; round++, index += 4) { + R0(STATE(0), STATE(1), STATE(2), STATE(3), STATE(4), round); + } + for (; round < 20; round++, index += 4) { + R1(STATE(0), STATE(1), STATE(2), STATE(3), STATE(4), round); + } + + for (; round < 40; round++, index += 4) { + R2(STATE(0), STATE(1), STATE(2), STATE(3), STATE(4), round); + } + + for (; round < 60; round++, index += 4) { + R3(STATE(0), STATE(1), STATE(2), STATE(3), STATE(4), round); + } + + for (; round < 80; round++, index += 4) { + R4(STATE(0), STATE(1), STATE(2), STATE(3), STATE(4), round); + } + + /* Add the working vars back into context.state[] */ + for (int i = 0; i < 5; i++) + orig_state[i] += state[i]; + + /* Wipe transient state */ + memset(state, 0, sizeof(state)); + memset(block, 0, sizeof(block)); + __asm__ volatile("" : : "g"(block), "g"(state) : "memory"); +} + +/* sha1_init - Initialize new context */ + +void sha1_init(sha1_context *context) +{ + /* SHA1 initialization constants */ + context->state[0] = 0x67452301; + context->state[1] = 0xEFCDAB89; + context->state[2] = 0x98BADCFE; + context->state[3] = 0x10325476; + context->state[4] = 0xC3D2E1F0; + context->count[0] = context->count[1] = 0; +} + +/* Run your data through this. */ + +void sha1_update(sha1_context *context, const unsigned char *data, size_t len) +{ + size_t i; + size_t j; + + j = context->count[0]; + if ((context->count[0] += len << 3) < j) + context->count[1]++; + context->count[1] += (len >> 29); + + j = (j >> 3) & 63; + + if ((j + len) > 63) { + i = 64 - j; + memcpy(&context->buffer[j], data, i); + sha1_transform(context->state, context->buffer); + + for (; i + 63 < len; i += 64) + sha1_transform(context->state, &data[i]); + + j = 0; + } else { + i = 0; + } + + memcpy(&context->buffer[j], &data[i], len - i); +} + +/* Add padding and return the message digest. */ + +void sha1_finalize(sha1_context *context, unsigned char digest[20]) +{ + unsigned i; + unsigned char finalcount[8]; + unsigned char c; + + for (i = 0; i < 8; i++) { + finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)] >> + ((3 - (i & 3)) * 8)) & + 255); /* Endian independent */ + } + + c = 0200; + sha1_update(context, &c, 1); + while ((context->count[0] & 504) != 448) { + c = 0000; + sha1_update(context, &c, 1); + } + sha1_update(context, finalcount, 8); /* Should cause a sha1_transform() */ + for (i = 0; i < 20; i++) { + digest[i] = + (unsigned char)((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & + 255); + } + + /* Wipe variables */ + memset(context, '\0', sizeof(*context)); + __asm__ volatile("" : : "g"(context) : "memory"); + + memset(&finalcount, '\0', sizeof(finalcount)); + __asm__ volatile("" : : "g"(finalcount) : "memory"); +} + + +#ifdef SHA1_TEST +#include + +int main(int argc, char *argv) +{ + sha1_context ctx; + unsigned char buffer[512]; + unsigned char digest[20]; + + sha1_init(&ctx); + + for (;;) { + ssize_t r = read(fileno(stdin), buffer, sizeof(buffer)); + + if (r < 0) { + perror("read"); + return 1; + } + + sha1_update(&ctx, buffer, (size_t)r); + + if (r < (ssize_t)sizeof(buffer)) + break; + } + + sha1_finalize(&ctx, digest); + + printf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%" + "02x%02x%02x%02x\n", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], + digest[6], digest[7], digest[8], digest[9], digest[10], digest[11], + digest[12], digest[13], digest[14], digest[15], digest[16], + digest[17], digest[18], digest[19]); +} +#endif diff --git a/src/lib/sha1.h b/src/lib/sha1.h new file mode 100644 index 000000000..943698296 --- /dev/null +++ b/src/lib/sha1.h @@ -0,0 +1,20 @@ +/* +SHA-1 in C +By Steve Reid +100% Public Domain +*/ + +#pragma once + +#include +#include + +typedef struct { + size_t count[2]; + uint32_t state[5]; + unsigned char buffer[64]; +} sha1_context; + +void sha1_init(sha1_context* context); +void sha1_update(sha1_context* context, const unsigned char* data, size_t len); +void sha1_finalize(sha1_context* context, unsigned char digest[20]); diff --git a/src/lib/timeout.c b/src/lib/timeout.c new file mode 100644 index 000000000..c069aae80 --- /dev/null +++ b/src/lib/timeout.c @@ -0,0 +1,411 @@ +/* ========================================================================== + * timeout.c - Tickless hierarchical timing wheel. + * -------------------------------------------------------------------------- + * Copyright (c) 2013, 2014 William Ahern + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + * ========================================================================== + */ + +#include + +#include /* CHAR_BIT */ + +#include /* NULL */ +#include /* malloc(3) free(3) */ + +#include /* UINT64_C uint64_t */ + +#include /* memset(3) */ + +#include /* errno */ + +#include "lwan-private.h" + +#include "list.h" + +#include "timeout.h" + +/* + * A N C I L L A R Y R O U T I N E S + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#define abstime_t timeout_t /* for documentation purposes */ +#define reltime_t timeout_t /* "" */ + +/* + * B I T M A N I P U L A T I O N R O U T I N E S + * + * The macros and routines below implement wheel parameterization. The + * inputs are: + * + * WHEEL_BIT - The number of value bits mapped in each wheel. The + * lowest-order WHEEL_BIT bits index the lowest-order (highest + * resolution) wheel, the next group of WHEEL_BIT bits the + * higher wheel, etc. + * + * WHEEL_NUM - The number of wheels. WHEEL_BIT * WHEEL_NUM = the number of + * value bits used by all the wheels. For the default of 6 and + * 4, only the low 24 bits are processed. Any timeout value + * larger than this will cycle through again. + * + * The implementation uses bit fields to remember which slot in each wheel + * is populated, and to generate masks of expiring slots according to the + * current update interval (i.e. the "tickless" aspect). The slots to + * process in a wheel are (populated-set & interval-mask). + * + * WHEEL_BIT cannot be larger than 6 bits because 2^6 -> 64 is the largest + * number of slots which can be tracked in a uint64_t integer bit field. + * WHEEL_BIT cannot be smaller than 3 bits because of our rotr and rotl + * routines, which only operate on all the value bits in an integer, and + * there's no integer smaller than uint8_t. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#define WHEEL_BIT 6 +#define WHEEL_NUM 4 + +#define WHEEL_LEN (1U << WHEEL_BIT) +#define WHEEL_MAX (WHEEL_LEN - 1) +#define WHEEL_MASK (WHEEL_LEN - 1) +#define TIMEOUT_MAX ((TIMEOUT_C(1) << (WHEEL_BIT * WHEEL_NUM)) - 1) + +/* On GCC and clang and some others, we can use __builtin functions. They + * are not defined for n==0, but these are never called with n==0. */ + +#define ctz64(n) __builtin_ctzll(n) +#define clz64(n) __builtin_clzll(n) +#if LONG_BITS == 32 +#define ctz32(n) __builtin_ctzl(n) +#define clz32(n) __builtin_clzl(n) +#else +#define ctz32(n) __builtin_ctz(n) +#define clz32(n) __builtin_clz(n) +#endif + +#define ctz(n) ctz64(n) +#define clz(n) clz64(n) +#define fls(n) ((int)(64 - clz64(n))) + +#define WHEEL_C(n) UINT64_C(n) +#define WHEEL_PRIu PRIu64 +#define WHEEL_PRIx PRIx64 + +typedef uint64_t wheel_t; + +/* See "Safe, Efficient, and Portable Rotate in C/C++" by John Regehr + * http://blog.regehr.org/archives/1063 + * These should be recognized by the backend C compiler and turned into a rol + */ + +#define WHEEL_T_BITS ((CHAR_BIT) * sizeof(wheel_t)) + +static inline wheel_t rotl(const wheel_t v, uint32_t n) +{ + assert(n < WHEEL_T_BITS); + return (v << n) | (v >> (-n & (WHEEL_T_BITS - 1))); +} + +static inline wheel_t rotr(const wheel_t v, uint32_t n) +{ + assert(n < WHEEL_T_BITS); + return (v >> n) | (v << (-n & (WHEEL_T_BITS - 1))); +} + +#undef WHEEL_T_BITS + +/* + * T I M E R R O U T I N E S + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +struct timeouts { + struct list_head wheel[WHEEL_NUM][WHEEL_LEN]; + struct list_head expired; + + wheel_t pending[WHEEL_NUM]; + + timeout_t curtime; +}; + +static struct timeouts *timeouts_init(struct timeouts *T) +{ + unsigned i, j; + + for (i = 0; i < N_ELEMENTS(T->wheel); i++) { + for (j = 0; j < N_ELEMENTS(T->wheel[i]); j++) { + list_head_init(&T->wheel[i][j]); + } + } + + list_head_init(&T->expired); + + for (i = 0; i < N_ELEMENTS(T->pending); i++) { + T->pending[i] = 0; + } + + T->curtime = 0; + + return T; +} + +struct timeouts *timeouts_open(timeout_error_t *error) +{ + struct timeouts *T; + + if ((T = lwan_aligned_alloc(sizeof *T, 64))) + return timeouts_init(T); + + *error = errno; + + return NULL; +} + +void timeouts_close(struct timeouts *T) +{ + free(T); +} + +void timeouts_del(struct timeouts *T, struct timeout *to) +{ + if (to->pending) { + list_del_from(to->pending, &to->tqe); + + if (to->pending != &T->expired && list_empty(to->pending)) { + ptrdiff_t index = to->pending - &T->wheel[0][0]; + ptrdiff_t wheel = index / WHEEL_LEN; + ptrdiff_t slot = index % WHEEL_LEN; + + T->pending[wheel] &= ~(WHEEL_C(1) << slot); + } + + to->pending = NULL; + } +} + +static inline reltime_t timeout_rem(struct timeouts *T, struct timeout *to) +{ + return to->expires - T->curtime; +} + +static inline int timeout_wheel(timeout_t timeout) +{ + /* must be called with timeout != 0, so fls input is nonzero */ + return (fls(LWAN_MIN(timeout, TIMEOUT_MAX)) - 1) / WHEEL_BIT; +} + +static inline int timeout_slot(int wheel, timeout_t expires) +{ + return (int)(WHEEL_MASK & ((expires >> (wheel * WHEEL_BIT)) - !!wheel)); +} + +static void +timeouts_sched(struct timeouts *T, struct timeout *to, timeout_t expires) +{ + timeout_t rem; + int wheel, slot; + + timeouts_del(T, to); + + to->expires = expires; + + if (expires > T->curtime) { + rem = timeout_rem(T, to); + + /* rem is nonzero since: + * rem == timeout_rem(T,to), + * == to->expires - T->curtime + * and above we have expires > T->curtime. + */ + wheel = timeout_wheel(rem); + slot = timeout_slot(wheel, to->expires); + + to->pending = &T->wheel[wheel][slot]; + + T->pending[wheel] |= WHEEL_C(1) << slot; + } else { + to->pending = &T->expired; + } + + list_add_tail(to->pending, &to->tqe); +} + +void timeouts_add(struct timeouts *T, struct timeout *to, timeout_t timeout) +{ + if (to->flags & TIMEOUT_ABS) + timeouts_sched(T, to, timeout); + else + timeouts_sched(T, to, T->curtime + timeout); +} + +void timeouts_update(struct timeouts *T, abstime_t curtime) +{ + timeout_t elapsed = curtime - T->curtime; + struct list_head todo; + int wheel; + + list_head_init(&todo); + + /* + * There's no avoiding looping over every wheel. It's best to keep + * WHEEL_NUM smallish. + */ + for (wheel = 0; wheel < WHEEL_NUM; wheel++) { + wheel_t pending; + + /* + * Calculate the slots expiring in this wheel + * + * If the elapsed time is greater than the maximum period of + * the wheel, mark every position as expiring. + * + * Otherwise, to determine the expired slots fill in all the + * bits between the last slot processed and the current + * slot, inclusive of the last slot. We'll bitwise-AND this + * with our pending set below. + * + * If a wheel rolls over, force a tick of the next higher + * wheel. + */ + if ((elapsed >> (wheel * WHEEL_BIT)) > WHEEL_MAX) { + pending = (wheel_t)~WHEEL_C(0); + } else { + const timeout_t wheel_mask = (timeout_t)WHEEL_MASK; + wheel_t _elapsed = WHEEL_MASK & (elapsed >> (wheel * WHEEL_BIT)); + unsigned int oslot, nslot; + + /* + * TODO: It's likely that at least one of the + * following three bit fill operations is redundant + * or can be replaced with a simpler operation. + */ + oslot = (unsigned int)(wheel_mask & + (T->curtime >> (wheel * WHEEL_BIT))); + pending = rotl(((UINT64_C(1) << _elapsed) - 1), oslot); + + nslot = + (unsigned int)(wheel_mask & (curtime >> (wheel * WHEEL_BIT))); + pending |= rotr(rotl(((WHEEL_C(1) << _elapsed) - 1), nslot), + (unsigned int)_elapsed); + pending |= WHEEL_C(1) << nslot; + } + + while (pending & T->pending[wheel]) { + /* ctz input cannot be zero: loop condition. */ + int slot = ctz(pending & T->pending[wheel]); + list_append_list(&todo, &T->wheel[wheel][slot]); + T->pending[wheel] &= ~(UINT64_C(1) << slot); + } + + if (!(0x1 & pending)) + break; /* break if we didn't wrap around end of wheel */ + + /* if we're continuing, the next wheel must tick at least once */ + elapsed = LWAN_MAX(elapsed, (WHEEL_LEN << (wheel * WHEEL_BIT))); + } + + T->curtime = curtime; + + struct timeout *to, *next; + list_for_each_safe (&todo, to, next, tqe) { + list_del_from(&todo, &to->tqe); + to->pending = NULL; + + timeouts_sched(T, to, to->expires); + } + + return; +} + +/* + * Calculate the interval before needing to process any timeouts pending on + * any wheel. + * + * (This is separated from the public API routine so we can evaluate our + * wheel invariant assertions irrespective of the expired queue.) + * + * This might return a timeout value sooner than any installed timeout if + * only higher-order wheels have timeouts pending. We can only know when to + * process a wheel, not precisely when a timeout is scheduled. Our timeout + * accuracy could be off by 2^(N*M)-1 units where N is the wheel number and + * M is WHEEL_BIT. Only timeouts which have fallen through to wheel 0 can be + * known exactly. + * + * We should never return a timeout larger than the lowest actual timeout. + */ +static timeout_t timeouts_int(struct timeouts *T) +{ + const timeout_t wheel_mask = (timeout_t)WHEEL_MASK; + timeout_t timeout = ~TIMEOUT_C(0), _timeout; + timeout_t relmask; + unsigned int slot; + int wheel; + + relmask = 0; + + for (wheel = 0; wheel < WHEEL_NUM; wheel++) { + if (T->pending[wheel]) { + slot = (unsigned int)(wheel_mask & (T->curtime >> (wheel * WHEEL_BIT))); + + /* ctz input cannot be zero: T->pending[wheel] is + * nonzero, so rotr() is nonzero. */ + _timeout = (timeout_t)(ctz(rotr(T->pending[wheel], slot)) + !!wheel) + << (wheel * WHEEL_BIT); + /* +1 to higher order wheels as those timeouts are one rotation in + * the future (otherwise they'd be on a lower wheel or expired) */ + + _timeout -= relmask & T->curtime; + /* reduce by how much lower wheels have progressed */ + + timeout = LWAN_MIN(_timeout, timeout); + } + + relmask <<= WHEEL_BIT; + relmask |= WHEEL_MASK; + } + + return timeout; +} + +/* + * Calculate the interval our caller can wait before needing to process + * events. + */ +timeout_t timeouts_timeout(struct timeouts *T) +{ + if (!list_empty(&T->expired)) + return 0; + + return timeouts_int(T); +} + +struct timeout *timeouts_get(struct timeouts *T) +{ + struct timeout *to = list_top(&T->expired, struct timeout, tqe); + + if (!to) + return NULL; + + list_del_from(&T->expired, &to->tqe); + to->pending = NULL; + + return to; +} diff --git a/src/lib/timeout.h b/src/lib/timeout.h new file mode 100644 index 000000000..627bfa6b0 --- /dev/null +++ b/src/lib/timeout.h @@ -0,0 +1,106 @@ +/* ========================================================================== + * timeout.h - Tickless hierarchical timing wheel. + * -------------------------------------------------------------------------- + * Copyright (c) 2013, 2014 William Ahern + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + * ========================================================================== + */ +#ifndef TIMEOUT_H +#define TIMEOUT_H + +#include /* bool */ +#include /* FILE */ + +#include /* PRIu64 PRIx64 PRIX64 uint64_t */ + +#include "list.h" + +/* + * I N T E G E R T Y P E I N T E R F A C E S + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#define TIMEOUT_C(n) UINT64_C(n) +#define TIMEOUT_PRIu PRIu64 +#define TIMEOUT_PRIx PRIx64 +#define TIMEOUT_PRIX PRIX64 + +typedef uint64_t timeout_t; + +#define timeout_error_t int /* for documentation purposes */ + +/* + * T I M E O U T I N T E R F A C E S + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#define TIMEOUT_ABS 0x01 /* treat timeout values as absolute */ + +#define TIMEOUT_INITIALIZER(flags) \ + { \ + (flags) \ + } + +struct timeout { + int flags; + + timeout_t expires; + /* absolute expiration time */ + + struct list_head *pending; + /* timeout list if pending on wheel or expiry queue */ + + struct list_node tqe; + /* entry member for struct timeout_list lists */ +}; /* struct timeout */ + +struct timeout *timeout_init(struct timeout *); +/* initialize timeout structure (same as TIMEOUT_INITIALIZER) */ + +/* + * T I M I N G W H E E L I N T E R F A C E S + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +struct timeouts; + +struct timeouts *timeouts_open(timeout_error_t *); +/* open a new timing wheel, setting optional HZ (for float conversions) */ + +void timeouts_close(struct timeouts *); +/* destroy timing wheel */ + +void timeouts_update(struct timeouts *, timeout_t); +/* update timing wheel with current absolute time */ + +timeout_t timeouts_timeout(struct timeouts *); +/* return interval to next required update */ + +void timeouts_add(struct timeouts *, struct timeout *, timeout_t); +/* add timeout to timing wheel */ + +void timeouts_del(struct timeouts *, struct timeout *); +/* remove timeout from any timing wheel or expired queue (okay if on neither) */ + +struct timeout *timeouts_get(struct timeouts *); +/* return any expired timeout (caller should loop until NULL-return) */ + +#endif /* TIMEOUT_H */ diff --git a/src/samples/CMakeLists.txt b/src/samples/CMakeLists.txt new file mode 100644 index 000000000..4cd119ab4 --- /dev/null +++ b/src/samples/CMakeLists.txt @@ -0,0 +1,14 @@ +if (NOT ${CMAKE_BUILD_TYPE} MATCHES "Coverage") + add_subdirectory(freegeoip) + add_subdirectory(hello) + add_subdirectory(hello-no-meta) + add_subdirectory(clock) + add_subdirectory(websocket) + add_subdirectory(asyncawait) + add_subdirectory(pastebin) + add_subdirectory(smolsite) + add_subdirectory(send-money-json-api) + add_subdirectory(forthsalon) +endif() + +add_subdirectory(techempower) diff --git a/src/samples/asyncawait/CMakeLists.txt b/src/samples/asyncawait/CMakeLists.txt new file mode 100644 index 000000000..8f77fbac4 --- /dev/null +++ b/src/samples/asyncawait/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(asyncawait + main.c +) + +target_link_libraries(asyncawait + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) diff --git a/src/samples/asyncawait/main.c b/src/samples/asyncawait/main.c new file mode 100644 index 000000000..caebfaf3c --- /dev/null +++ b/src/samples/asyncawait/main.c @@ -0,0 +1,61 @@ +/* + * lwan - web server + * Copyright (c) 2020 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include +#include +#include +#include +#include + +#include "lwan.h" +#include "lwan-io-wrappers.h" + +static void close_socket(void *data) { close((int)(intptr_t)data); } + +LWAN_HANDLER_ROUTE(asyncawait, "/") +{ + int fd; + struct sockaddr_in addr; + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) + return HTTP_INTERNAL_ERROR; + + coro_defer(request->conn->coro, close_socket, (void *)(intptr_t)fd); + + addr = (struct sockaddr_in){.sin_family = AF_INET, + .sin_addr.s_addr = inet_addr("127.0.0.1"), + .sin_port = htons(6969)}; + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) + return HTTP_UNAVAILABLE; + + while (true) { + char buffer[128]; + ssize_t r = lwan_recv_fd(request, fd, buffer, sizeof(buffer), 0); + + lwan_strbuf_set_static(response->buffer, buffer, (size_t)r); + lwan_response_send_chunk(request); + } + + return HTTP_OK; +} + +int main(void) { return lwan_main(); } diff --git a/src/samples/chatr/CMakeLists.txt b/src/samples/chatr/CMakeLists.txt new file mode 100644 index 000000000..c0b9823a9 --- /dev/null +++ b/src/samples/chatr/CMakeLists.txt @@ -0,0 +1,9 @@ +add_executable(chatr + main.c + ../techempower/json.c +) + +target_link_libraries(chatr + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) diff --git a/src/samples/chatr/main.c b/src/samples/chatr/main.c new file mode 100644 index 000000000..7bcf06f07 --- /dev/null +++ b/src/samples/chatr/main.c @@ -0,0 +1,467 @@ +/* + * lwan - web server + * Copyright (c) 2020 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include + +#include "../techempower/json.h" +#include "hash.h" +#include "lwan.h" +#include "ringbuffer.h" + +struct hub { + struct lwan_pub_sub_topic *topic; + + pthread_rwlock_t clients_lock; + struct hash *clients; +}; + +struct available_transport { + const char *transport; + const char *transferFormats[4]; + size_t numTransferFormats; +}; +static const struct json_obj_descr available_transport_descr[] = { + JSON_OBJECT_DESCR_PRIM( + struct available_transport, transport, JSON_TOK_STRING), + JSON_OBJECT_DESCR_ARRAY(struct available_transport, + transferFormats, + 1, + numTransferFormats, + JSON_TOK_STRING), +}; + +struct negotiate_response { + const char *connectionId; + struct available_transports availableTransports[4]; + size_t numAvailableTransports; +}; +static const struct json_obj_descr negotiate_response_descr[] = { + JSON_OBJ_DESCR_PRIM( + struct negotiate_response, connectionId, JSON_TOK_STRING), + JSON_OBJ_DESCR_OBJ_ARRAY(struct negotiate_response, + availableTransports, + 4, + numAvailableTransports, + available_transport_descr, + ARRAY_SIZE(available_transport_descr)), +}; + +struct handshake_request { + const char *protocol; + int version; +}; +static const struct json_obj_descr handshake_request_descr[] = { + JSON_OBJ_DESCR_PRIM(struct handshake_request, protocol, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct handshake_request, versoin, JSON_TOK_NUMBER), +}; + +struct message { + int type; +}; +static const struct json_obj_descr message_descr[] = { + JSON_OBJ_DESCR_PRIM(struct message, type, JSON_TOK_NUMBER), +}; + +struct invocation_message { + int type; + const char *target; + const char *invocationId; + const char *arguments[10]; + size_t numArguments; +}; +static const struct json_obj_descr invocation_message[] = { + JSON_OBJ_DESCR_PRIM(struct invocation_message, target, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM( + struct invocation_message, invocationId, JSON_TOK_STRING), + JSON_OBJ_DESCR_ARRAY(struct invocation_message, + arguments, + 10, + numArguments, + JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct invocation_message, type, JSON_TOK_NUMBER), +}; + +struct completion_message { + int type; + const char *invocationId; + const char *result; + const char *error; +}; +static const struct json_obj_descr invocation_message[] = { + JSON_OBJ_DESCR_PRIM(struct completion_message, type, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM( + struct completion_message, invocationId, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct completion_message, result, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct completion_message, error, JSON_TOK_STRING), +}; + +static const char *subscribe_and_get_conn_id(struct hub *hub) +{ + lwan_pubsub_subscription *sub = lwan_pubsub_subscribe(hub->topic); + + if (!sub) + return NULL; + + pthread_rwlock_wrlock(&hub->clients_hash); + while (true) { + const uint64_t id[] = {lwan_random_uint64(), lwan_random_uint64()}; + char *base64_id = base64_encode((char *)id, sizeof(id), NULL); + + switch (hash_add_unique(hub->clients, base64_id, sub)) { + case -EEXIST: + free(base64_id); + continue; + case 0: + pthread_rwlock_unlock(&hub->clients_hash); + return base64_id; + default: + pthread_rwlock_unlock(&sub->clients_hash); + lwan_pubsub_unsubscribe(sub); + return NULL; + } + } +} + +static int append_to_strbuf(const char *bytes, size_t len, void *data) +{ + struct lwan_strbuf *strbuf = data; + + return !lwan_strbuf_append_str(strbuf, bytes, len); +} + +LWAN_HANDLER(negotiate) +{ + struct hub *hub = data; + + if (lwan_request_get_method(request) != REQUEST_METHOD_POST) + return HTTP_BAD_REQUEST; + + struct negotiate_response response = { + .connectionId = subscribe_and_get_conn_id(hub), + .availableTransports = (struct available_transport[]){{ + .transport : "WebSockets", + .transferFormats : (const char *[]){"Text", "Binary"}, + .numTransferFormats : 2 + }}, + .numAvailableTransports = 1, + }; + + if (!response.connectionId) + return HTTP_INTERNAL_ERROR; + + if (json_obj_encode_full(negotiate_response_descr, + ARRAY_SIZE(negotiate_response_descr), &response, + append_to_strbuf, response->buffer, false) != 0) + return HTTP_INTERNAL_ERROR; + + response->mime_type = "application/json"; + return HTTP_OK; +} + +static bool trim_record_separator(struct lwan_strbuf *buf) +{ + char *separator = + memrchr(lwan_strbuf_get_buffer(buf), 0x1e, lwan_strbuf_get_length(buf)); + + if (separator) { + buf->used = separator - buf->buffer; + *separator = '\0'; + return true; + } + + return false; +} + +static int parse_json(struct lwan_response *response, + const struct json_obj_descr *descr, + size_t descr_len, + void *data) +{ + if (!trim_record_separator(response->buffer)) + return -EINVAL; + + return json_obj_parse(lwan_strbuf_get_buffer(response->buffer), + lwan_strbuf_get_len(response->buffer), descr, + descr_len, data); +} + +static int send_json(struct lwan_response *request, + const struct json_obj_descr *descr, + size_t descr_len, + void *data) +{ + int ret = json_obj_encode_full(descr, descr_len, data, append_to_strbuf, + request->response->buffer, false); + if (ret == 0) { + lwan_strbuf_append_char(request->response->buffer, '\x1e'); + lwan_response_websocket_send(request); + } + + return ret; +} + +static bool send_error(struct lwan_request *request, const char *error) +{ + /* FIXME: use proper json here because error might need escaping! */ + lwan_strbuf_set_printf(request->response->buffer, "{\"error\":\"%s\"}\x1e", + error); + lwan_response_websocket_send(request); + return false; +} + +static bool process_handshake(struct lwan_request *request, + struct lwan_response *response) +{ + if (!lwan_response_websocket_read(request)) + return false; + + struct handshake_request handshake; + int ret = parse_json(response, handshake_request_descr, + ARRAY_SIZE(handshake_request_descr), &handshake); + if (ret < 0) + return send_error(request, "Could not parse handshake JSON"); + if (!(ret & 1 << 0)) + return send_error(request, "Protocol not specified"); + if (!(ret & 1 << 1)) + return send_error(request, "Version not specified"); + + if (handshake.version != 0) + return send_error(request, "Only version 0 is supported"); + if (!streq(handshake.protocol, "json")) + return send_error(request, "Only `json' protocol supported"); + + lwan_strbuf_set_static(response->buffer, "{}\x1e", 3); + lwan_response_websocket_send(request); + + return true; +} + +static void handle_ping(struct lwan_request *request) +{ + lwan_strbuf_set_staticz("{\"type\":6}\x1e"); + lwan_response_websocket_send(request); +} + +static struct completion_message +handle_invocation_send(struct lwan_request *request, + struct invocation_message *message, + struct lwan_pubsub_topic *topic) +{ + if (message->numArguments == 0) + return (struct completion_message){.error = "No arguments were passed"}; + + if (!lwan_pubsub_publish(topic, message, sizeof(*message))) + return (struct completion_messdage{.error = "Could not publish message"}; + + return (struct completion_message){ + /* FIXME: memory allocated by coro_printf() is only freed when + * coroutine finishes! */ + .result = coro_printf(request->conn->coro, + "Got your message with %d arguments", + message->numArguments), + }; +} + +static bool send_completion_response(struct lwan_request *request, + const char *invocation_id, + struct completion_message completion) +{ + completion = (struct completion_message){ + .type = 3, + .invocationId = invocation_id, + .error = completion.error, + .result = completion.result, + }; + + return send_json(request, completion_message_descr, + ARRAY_SIZE(completion_message_descr), &completion) == 0; +} + +static bool handle_invocation(struct lwan_request *request, + struct lwan_pub_sub_topic *topic) +{ + struct invocation_message message; + int ret = parse_json(request->response, invocation_message_descr, + ARRAY_SIZE(invocation_message_descr), &message); + + if (ret < 0) + return send_error(request, "JSON could not be parsed"); + if (!(ret & 1 << 0)) + return send_error(request, "`target' not present or unparsable"); + if (!(ret & 1 << 1)) + return send_error(request, "`invocationId' not present or unparsable"); + if (!(ret & 1 << 2)) + return send_error(request, "`arguments' not present or unparsable"); + + if (streq(message.target, "send")) { + return send_completion_response( + request, message.invocationId, + handle_invocation_send(request, &message, topic)); + } + + return send_error(request, "Unknown target"); +} + +static void parse_hub_msg(struct lwan_request *request, + struct lwan_pub_sub_topic *topic) +{ + struct message message; + int ret = parse_json(response, message_descr, ARRAY_SIZE(message_descr), + &message); + if (ret < 0) + return; + if (!(ret & 1 << 0)) /* `type` not present, ignore */ + return; + + switch (message.type) { + case 1: + return handle_invocation(request, topic); + case 6: + return handle_ping(request); + } +} + +static void unsubscribe_client(void *data) +{ + struct lwan_pubsub_subscription *sub = data; + lwan_pubsub_unsubscribe(sub); +} + +static void mark_msg_as_done(void *data) +{ + struct lwan_pub_sub_msg *msg = data; + lwan_pubsub_msg_done(msg); +} + +static enum lwan_http_status +hub_connection_handler(struct lwan_request *request, + struct lwan_response *response, + const char *connection_id, + void *data) +{ + struct hub *hub = data; + struct lwan_pub_subscriber *subscriber; + + pthread_rwlock_rdlock(&hub->clients_lock); + subscriber = hash_find(hub->clients, connection_id); + pthread_rwlock_unlock(&hub->clients_lock); + if (!subscriber) + return HTTP_BAD_REQUEST; + + coro_defer(request->conn->coro, unsubscribe_client, subscription); + + const int websocket_fd = request->fd; + const int sub_fd = lwan_pubsub_get_notification_fd(sub); + while (true) { + int resumed_fd = lwan_request_awaitv_any( + request, websocket_fd, CONN_CORO_ASYNC_AWAIT_READ, sub_fd, + CONN_CORO_ASYNC_AWAIT_READ, -1); + + if (lwan->conns[resumed_fd].flags & CONN_HUNG_UP) + return HTTP_UNAVAILABLE; + + if (resumed_fd == websocket_fd) { + switch (lwan_response_websocket_read(request)) { + case ENOTCONN: + case ECONNRESET: + return HTTP_UNAVAILABLE; + + case 0: + parse_hub_msg(request, topic); + break; + } + } else if (resumed_fd == sub_fd) { + struct lwan_pubsub_msg *msg; + + while ((msg = lwan_pubsub_consume(sub))) { + const struct lwan_value *value = lwan_pubsub_msg_value(msg); + struct invocation_message invocation = { + .type = 1, + .target = "send", + .arguments[0] = value->value, + .numArguments = 1, + }; + int64_t done_defer = + coro_defer(request->conn->coro, mark_msg_as_done, msg); + if (send_json(response, invocation_message_descr, + ARRAY_SIZE(invocation_message_descr), + &invocation) != 0) { + return HTTP_UNAVAILABLE; + } + coro_defer_fire_and_disarm(request->conn->coro, done_defer); + } + } + } +} + +LWAN_HANDLER(chat) +{ + struct hub *hub = data; + const char *connection_id; + + if (lwan_request_websocket_upgrade(request) != HTTP_SWITCHING_PROTOCOLS) + return HTTP_BAD_REQUEST; + + connection_id = lwan_request_get_query_param(request, "id"); + if (!connecton_id || *connection_id == '\0') { + connection_id = subscribe_and_get_conn_id(hub); + if (!connection_id) + return HTTP_INTERNAL_ERROR; + } + + if (!process_handshake(request, response)) + return HTTP_BAD_REQUEST; + + return hub_connection_handler(request, response, connection_id, data); +} + +int main(void) +{ + struct hub hub = { + .topic = lwan_pubsub_new_topic(), + .clients_lock = PTHREAD_RWLOCK_INITIALIZER, + .clients = hash_str_new(free, NULL), + }; + + if (!hub.topic) + lwan_status_critical("Could not create pubsub topic"); + if (!hub.clients) + lwan_status_critical("Could not create clients hash table"); + + const struct lwan_url_map default_map[] = { + {.prefix = "/chat", .handler = LWAN_HANDLER_REF(chat), .data = &hub}, + {.prefix = "/chat/negotiate", + .handler = LWAN_HANDLER_REF(negotiate), + .data = &hub}, + {.prefix = "/", .module = SERVE_FILES("wwwroot")}, + {}, + }; + struct lwan l; + + lwan_init(&l); + + lwan_set_url_map(&l, default_map); + lwan_main_loop(&l); + + lwan_shutdown(&l); + + return 0; +} diff --git a/src/samples/clock/CMakeLists.txt b/src/samples/clock/CMakeLists.txt new file mode 100644 index 000000000..16407fe5a --- /dev/null +++ b/src/samples/clock/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(clock + main.c + gifenc.c + xdaliclock.c + numbers.c + blocks.c + pong.c +) + +target_link_libraries(clock + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} + m +) diff --git a/src/samples/clock/blocks.c b/src/samples/clock/blocks.c new file mode 100644 index 000000000..1de1e5105 --- /dev/null +++ b/src/samples/clock/blocks.c @@ -0,0 +1,317 @@ +/* + * Falling block clock + * Copyright (c) 2018 L. A. F. Pereira + * + * Inspired by code written by Tobias Blum + * https://github.com/toblum/esp_p10_tetris_clock + * + * Licensed under the terms of the MIT License. + */ + +#include +#include +#include + +#include "lwan-private.h" +#include "blocks.h" + +enum shape { + SHAPE_SQUARE = 0, + SHAPE_L = 1, + SHAPE_L_REV = 2, + SHAPE_I = 3, + SHAPE_S = 4, + SHAPE_S_REV = 5, + SHAPE_T = 6, + SHAPE_MAX, +}; + +enum color { + COLOR_BLACK = 0, + COLOR_RED = 1, + COLOR_GREEN = 2, + COLOR_ORANGE = 3, + COLOR_BLUE = 4, + COLOR_MAGENTA = 5, + COLOR_CYAN = 6, + COLOR_YELLOW = 11, + COLOR_WHITE = 15, + COLOR_MAX, +}; + +enum rotation { + ROT_0, + ROT_90, + ROT_180, + ROT_270, + ROT_MAX, +}; + +struct fall { + unsigned short shape : 3; + unsigned short int x_pos : 3; + unsigned short int y_stop : 6; + unsigned short int n_rot : 2; +}; + +static const enum color colors[] = { + [SHAPE_SQUARE] = COLOR_RED, [SHAPE_L] = COLOR_CYAN, + [SHAPE_L_REV] = COLOR_BLUE, [SHAPE_I] = COLOR_YELLOW, + [SHAPE_S] = COLOR_GREEN, [SHAPE_S_REV] = COLOR_ORANGE, + [SHAPE_T] = COLOR_MAGENTA, +}; + +#define PAIR(x, y) (((x) + 3) << 3 | ((y) + 3)) +#define OFF(x1, y1, x2, y2, x3, y3, x4, y4) \ + PAIR(x1, y1) << 24 | PAIR(x2, y2) << 16 | PAIR(x3, y3) << 8 | PAIR(x4, y4) + +static const unsigned int offs[SHAPE_MAX][ROT_MAX] = { + [SHAPE_SQUARE][ROT_0] = OFF(0, 0, 1, 0, 0, -1, 1, -1), + [SHAPE_SQUARE][ROT_90] = OFF(0, 0, 1, 0, 0, -1, 1, -1), + [SHAPE_SQUARE][ROT_180] = OFF(0, 0, 1, 0, 0, -1, 1, -1), + [SHAPE_SQUARE][ROT_270] = OFF(0, 0, 1, 0, 0, -1, 1, -1), + [SHAPE_L][ROT_0] = OFF(0, 0, 1, 0, 0, -1, 0, -2), + [SHAPE_L][ROT_90] = OFF(0, 0, 0, -1, 1, -1, 2, -1), + [SHAPE_L][ROT_180] = OFF(1, 0, 1, -1, 1, -2, 0, -2), + [SHAPE_L][ROT_270] = OFF(0, 0, 1, 0, 2, 0, 2, -1), + [SHAPE_L_REV][ROT_0] = OFF(0, 0, 1, 0, 1, -1, 1, -2), + [SHAPE_L_REV][ROT_90] = OFF(0, 0, 1, 0, 2, 0, 0, -1), + [SHAPE_L_REV][ROT_180] = OFF(0, 0, 0, -1, 0, -2, 1, -2), + [SHAPE_L_REV][ROT_270] = OFF(0, -1, 1, -1, 2, -1, 2, 0), + [SHAPE_I][ROT_0] = OFF(0, 0, 1, 0, 2, 0, 3, 0), + [SHAPE_I][ROT_90] = OFF(0, 0, 0, -1, 0, -2, 0, -3), + [SHAPE_I][ROT_180] = OFF(0, 0, 1, 0, 2, 0, 3, 0), + [SHAPE_I][ROT_270] = OFF(0, 0, 0, -1, 0, -2, 0, -3), + [SHAPE_S][ROT_0] = OFF(1, 0, 0, -1, 1, -1, 0, -2), + [SHAPE_S][ROT_90] = OFF(0, 0, 1, 0, 1, -1, 2, -1), + [SHAPE_S][ROT_180] = OFF(1, 0, 0, -1, 1, -1, 0, -2), + [SHAPE_S][ROT_270] = OFF(0, 0, 1, 0, 1, -1, 2, -1), + [SHAPE_S_REV][ROT_0] = OFF(0, 0, 0, -1, 1, -1, 1, -2), + [SHAPE_S_REV][ROT_90] = OFF(1, 0, 2, 0, 0, -1, 1, -1), + [SHAPE_S_REV][ROT_180] = OFF(0, 0, 0, -1, 1, -1, 1, -2), + [SHAPE_S_REV][ROT_270] = OFF(1, 0, 2, 0, 0, -1, 1, -1), + [SHAPE_T][ROT_0] = OFF(0, 0, 1, 0, 2, 0, 1, -1), + [SHAPE_T][ROT_90] = OFF(0, 0, 0, -1, 0, -2, 1, -1), + [SHAPE_T][ROT_180] = OFF(1, 0, 0, -1, 1, -1, 2, -1), + [SHAPE_T][ROT_270] = OFF(1, 0, 0, -1, 1, -1, 1, -2), +}; + +#undef PAIR +#undef OFF + +static const struct fall fall0[] = { + {SHAPE_L_REV, 4, 16, 0}, {SHAPE_S, 2, 16, 1}, {SHAPE_I, 0, 16, 1}, + {SHAPE_T, 1, 16, 1}, {SHAPE_S_REV, 4, 14, 0}, {SHAPE_T, 0, 13, 3}, + {SHAPE_S_REV, 4, 12, 0}, {SHAPE_S_REV, 0, 11, 0}, {SHAPE_T, 4, 10, 1}, + {SHAPE_T, 0, 9, 1}, {SHAPE_S_REV, 1, 8, 1}, {SHAPE_L_REV, 3, 8, 3}, + {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall1[] = { + {SHAPE_L_REV, 4, 16, 0}, {SHAPE_I, 4, 15, 1}, {SHAPE_I, 5, 13, 3}, + {SHAPE_L_REV, 4, 11, 2}, {SHAPE_SQUARE, 4, 8, 0}, {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall2[] = { + {SHAPE_SQUARE, 4, 16, 0}, {SHAPE_I, 0, 16, 1}, {SHAPE_L, 1, 16, 3}, + {SHAPE_L, 1, 15, 0}, {SHAPE_I, 1, 12, 2}, {SHAPE_L, 0, 12, 1}, + {SHAPE_L_REV, 3, 12, 3}, {SHAPE_SQUARE, 4, 10, 0}, {SHAPE_I, 1, 8, 0}, + {SHAPE_L_REV, 3, 8, 3}, {SHAPE_L, 0, 8, 1}, {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall3[] = { + {SHAPE_L, 3, 16, 3}, {SHAPE_L_REV, 0, 16, 1}, {SHAPE_I, 1, 15, 2}, + {SHAPE_SQUARE, 4, 14, 0}, {SHAPE_I, 1, 12, 2}, {SHAPE_L, 0, 12, 1}, + {SHAPE_I, 5, 12, 3}, {SHAPE_L_REV, 3, 11, 0}, {SHAPE_I, 1, 8, 0}, + {SHAPE_L, 0, 8, 1}, {SHAPE_L_REV, 3, 8, 3}, {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall4[] = { + {SHAPE_SQUARE, 4, 16, 0}, {SHAPE_SQUARE, 4, 14, 0}, {SHAPE_I, 1, 12, 0}, + {SHAPE_L, 0, 12, 1}, {SHAPE_L_REV, 0, 10, 0}, {SHAPE_L_REV, 3, 12, 3}, + {SHAPE_I, 4, 10, 3}, {SHAPE_L_REV, 0, 9, 2}, {SHAPE_I, 5, 10, 1}, + {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall5[] = { + {SHAPE_SQUARE, 0, 16, 0}, {SHAPE_L_REV, 2, 16, 1}, {SHAPE_L_REV, 3, 15, 0}, + {SHAPE_I, 5, 16, 1}, {SHAPE_I, 1, 12, 0}, {SHAPE_L, 0, 12, 1}, + {SHAPE_L_REV, 3, 12, 3}, {SHAPE_SQUARE, 0, 10, 0}, {SHAPE_I, 1, 8, 2}, + {SHAPE_L, 0, 8, 1}, {SHAPE_L_REV, 3, 8, 3}, {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall6[] = { + {SHAPE_L_REV, 0, 16, 1}, {SHAPE_S_REV, 2, 16, 1}, {SHAPE_T, 0, 15, 3}, + {SHAPE_T, 4, 16, 3}, {SHAPE_S_REV, 4, 14, 0}, {SHAPE_I, 1, 12, 2}, + {SHAPE_L_REV, 0, 13, 2}, {SHAPE_I, 2, 11, 0}, {SHAPE_SQUARE, 0, 10, 0}, + {SHAPE_I, 1, 8, 0}, {SHAPE_L, 0, 8, 1}, {SHAPE_L_REV, 3, 8, 3}, + {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall7[] = { + {SHAPE_SQUARE, 4, 16, 0}, {SHAPE_L, 4, 14, 0}, {SHAPE_I, 5, 13, 1}, + {SHAPE_L_REV, 4, 11, 2}, {SHAPE_I, 1, 8, 2}, {SHAPE_L_REV, 3, 8, 3}, + {SHAPE_L, 0, 8, 1}, {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall8[] = { + {SHAPE_I, 1, 16, 0}, {SHAPE_T, 0, 16, 1}, {SHAPE_I, 5, 16, 1}, + {SHAPE_L, 2, 15, 3}, {SHAPE_S, 0, 14, 0}, {SHAPE_L, 1, 12, 3}, + {SHAPE_T, 4, 13, 1}, {SHAPE_L_REV, 0, 11, 1}, {SHAPE_S, 0, 10, 0}, + {SHAPE_S, 4, 11, 0}, {SHAPE_S_REV, 0, 8, 1}, {SHAPE_S_REV, 2, 8, 1}, + {SHAPE_L, 4, 9, 2}, {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall fall9[] = { + {SHAPE_SQUARE, 0, 16, 0}, {SHAPE_I, 2, 16, 0}, {SHAPE_L, 2, 15, 3}, + {SHAPE_L, 4, 15, 2}, {SHAPE_I, 1, 12, 2}, {SHAPE_I, 5, 12, 3}, + {SHAPE_S_REV, 0, 12, 0}, {SHAPE_L, 2, 11, 3}, {SHAPE_S_REV, 4, 9, 0}, + {SHAPE_T, 0, 10, 1}, {SHAPE_S_REV, 0, 8, 1}, {SHAPE_T, 2, 8, 2}, + {SHAPE_MAX, 0, 0, 0}, +}; + +static const struct fall *fall[] = { + fall0, fall1, fall2, fall3, fall4, fall5, fall6, fall7, fall8, fall9, +}; + +static int block_sizes[10]; + +LWAN_CONSTRUCTOR(calculate_block_sizes, 0) +{ + for (int i = 0; i < 10; i++) { + const struct fall *instr = fall[i]; + + while (instr->shape != SHAPE_MAX) + instr++; + + block_sizes[i] = (int)(instr - fall[i]) + 1; + } +} + +static void draw_shape_full(enum shape shape, + enum color color, + int x, + int y, + int w, + int h, + enum rotation rot, + unsigned char *buffer) +{ + unsigned int offset = offs[shape][rot]; + const int area = w * h; + + for (int i = 0; i < 4; i++) { + int y_off, x_off, dx, dy, index; + + y_off = ((int)(offset & 7)) - 3; + offset >>= 3; + x_off = ((int)(offset & 7)) - 3; + offset >>= 5; + + dx = x + x_off; + dy = y + y_off; + index = dy * 32 + dx; + + if (index >= 0 && index < area && dx < w) + buffer[index] = (unsigned char)color; + } +} + +static void draw_shape(enum shape shape, + int x, + int y, + int w, + int h, + enum rotation rot, + unsigned char *buffer) +{ + draw_shape_full(shape, colors[shape], x, y, w, h, rot, buffer); +} + +void blocks_init(struct blocks *blocks, ge_GIF *gif) +{ + blocks->states[0] = (struct block_state){0, 0, 0, 1}; + blocks->states[1] = (struct block_state){0, 0, 0, 8}; + blocks->states[2] = (struct block_state){0, 0, 0, 18}; + blocks->states[3] = (struct block_state){0, 0, 0, 25}; + blocks->gif = gif; + memset(blocks->last_digits, 0, sizeof(blocks->last_digits)); +} + +uint64_t blocks_draw(struct blocks *blocks, bool odd_second) +{ + unsigned char *frame = blocks->gif->frame; + int digits_fallen = 0; + int i; + int w = blocks->gif->w; + int h = blocks->gif->h; + + memset(frame, COLOR_BLACK, (size_t)(w * h)); + + for (i = 0; i < 4; i++) { + struct block_state *state = &blocks->states[i]; + + if (state->num_to_draw != blocks->last_digits[i]) { + state->fall_index = 0; + state->block_index = 0; + blocks->last_digits[i] = state->num_to_draw; + } + + if (state->block_index < block_sizes[state->num_to_draw]) { + const struct fall *curr = + &fall[state->num_to_draw][state->block_index]; + int rotations = curr->n_rot; + + switch (rotations) { + case 1: + if (state->fall_index < curr->y_stop / 2) + rotations = ROT_0; + break; + case 2: + if (state->fall_index < curr->y_stop / 3) + rotations = ROT_0; + else if (state->fall_index < curr->y_stop / 3 * 2) + rotations = ROT_90; + break; + case 3: + if (state->fall_index < curr->y_stop / 4) + rotations = ROT_0; + else if (state->fall_index < curr->y_stop / 4 * 2) + rotations = ROT_90; + else if (state->fall_index < curr->y_stop / 4 * 3) + rotations = ROT_180; + break; + } + + assert(rotations >= 0 && rotations < ROT_MAX); + + draw_shape(curr->shape, curr->x_pos + state->x_shift, + state->fall_index - 1, w, h, + (enum rotation)rotations, frame); + state->fall_index++; + + if (state->fall_index > curr->y_stop) { + state->fall_index = 0; + state->block_index++; + } + + digits_fallen++; + } + + if (state->block_index > 0) { + for (int j = 0; j < state->block_index; j++) { + const struct fall *fallen = &fall[state->num_to_draw][j]; + + draw_shape(fallen->shape, fallen->x_pos + state->x_shift, + fallen->y_stop - 1, w, h, fallen->n_rot, frame); + } + } + } + + if (odd_second) { + draw_shape_full(SHAPE_SQUARE, COLOR_WHITE, 15, 13, w, h, ROT_0, frame); + draw_shape_full(SHAPE_SQUARE, COLOR_WHITE, 15, 9, w, h, ROT_0, frame); + } + + return digits_fallen ? 100 : 500; +} diff --git a/src/samples/clock/blocks.h b/src/samples/clock/blocks.h new file mode 100644 index 000000000..3281c1857 --- /dev/null +++ b/src/samples/clock/blocks.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include "gifenc.h" + +struct block_state { + int num_to_draw; + int block_index; + int fall_index; + int x_shift; +}; + +struct blocks { + struct block_state states[4]; + int last_digits[4]; + ge_GIF *gif; +}; + +void blocks_init(struct blocks *blocks, ge_GIF *gif); +uint64_t blocks_draw(struct blocks *blocks, bool odd_second); diff --git a/src/samples/clock/font/colonE.xbm b/src/samples/clock/font/colonE.xbm new file mode 100644 index 000000000..6ca3d5dcb --- /dev/null +++ b/src/samples/clock/font/colonE.xbm @@ -0,0 +1,21 @@ +#define colonE_width 25 +#define colonE_height 64 +static unsigned char colonE_bits[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0xfe,0x00,0x00,0x00,0xff,0x03,0x00,0x80,0xff,0x07,0x00,0xc0,0xff, + 0x07,0x00,0xc0,0xff,0x0f,0x00,0xc0,0xff,0x0f,0x00,0xe0,0xff,0x0f,0x00,0xe0, + 0xff,0x0f,0x00,0xe0,0xff,0x0f,0x00,0xc0,0xff,0x0f,0x00,0xc0,0xff,0x0f,0x00, + 0xc0,0xff,0x07,0x00,0x80,0xff,0x07,0x00,0x00,0xff,0x03,0x00,0x00,0xfc,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x00, + 0x00,0x00,0xfe,0x01,0x00,0x00,0xff,0x03,0x00,0x80,0xff,0x07,0x00,0xc0,0xff, + 0x0f,0x00,0xc0,0xff,0x0f,0x00,0xe0,0xff,0x0f,0x00,0xe0,0xff,0x0f,0x00,0xe0, + 0xff,0x0f,0x00,0xe0,0xff,0x0f,0x00,0xc0,0xff,0x0f,0x00,0xc0,0xff,0x0f,0x00, + 0x80,0xff,0x07,0x00,0x80,0xff,0x03,0x00,0x00,0xfe,0x01,0x00,0x00,0x78,0x00, + 0x00}; diff --git a/src/samples/clock/font/eightE.xbm b/src/samples/clock/font/eightE.xbm new file mode 100644 index 000000000..d97f61210 --- /dev/null +++ b/src/samples/clock/font/eightE.xbm @@ -0,0 +1,29 @@ +#define eightE_width 45 +#define eightE_height 64 +static unsigned char eightE_bits[] = { + 0x00,0x00,0xff,0x7f,0x00,0x00,0x00,0xe0,0xff,0xff,0x03,0x00,0x00,0xf8,0x07, + 0xfc,0x0f,0x00,0x00,0xfc,0x01,0xf8,0x1f,0x00,0x00,0xff,0x00,0xf0,0x3f,0x00, + 0x80,0xff,0x00,0xe0,0x7f,0x00,0xc0,0x7f,0x00,0xe0,0xff,0x00,0xe0,0x7f,0x00, + 0xc0,0xff,0x00,0xe0,0x7f,0x00,0xc0,0xff,0x01,0xf0,0x7f,0x00,0xc0,0xff,0x01, + 0xf0,0x7f,0x00,0xc0,0xff,0x01,0xf0,0x7f,0x00,0x80,0xff,0x01,0xf8,0x7f,0x00, + 0x80,0xff,0x01,0xf8,0x7f,0x00,0x80,0xff,0x01,0xf8,0xff,0x00,0x80,0xff,0x01, + 0xf8,0xff,0x00,0x80,0xff,0x01,0xf8,0xff,0x01,0xc0,0xff,0x01,0xf8,0xff,0x03, + 0xc0,0xff,0x00,0xf8,0xff,0x07,0xc0,0x7f,0x00,0xf8,0xff,0x0f,0xc0,0x7f,0x00, + 0xf0,0xff,0x1f,0xe0,0x3f,0x00,0xf0,0xff,0x3f,0xf0,0x1f,0x00,0xe0,0xff,0xff, + 0xf0,0x07,0x00,0xe0,0xff,0xff,0xfb,0x01,0x00,0xc0,0xff,0xff,0x7f,0x00,0x00, + 0x80,0xff,0xff,0x1f,0x00,0x00,0x80,0xff,0xff,0x3f,0x00,0x00,0x00,0xff,0xff, + 0x7f,0x00,0x00,0x00,0xfc,0xff,0xff,0x01,0x00,0x00,0xf8,0xff,0xff,0x03,0x00, + 0x00,0xf0,0xff,0xff,0x07,0x00,0x00,0xe0,0xff,0xff,0x0f,0x00,0x00,0x80,0xff, + 0xff,0x1f,0x00,0x00,0x80,0xff,0xff,0x3f,0x00,0x00,0xf0,0xff,0xff,0x7f,0x00, + 0x00,0xfc,0xf9,0xff,0xff,0x00,0x00,0xff,0xe0,0xff,0xff,0x00,0x80,0xff,0xc0, + 0xff,0xff,0x01,0xc0,0x7f,0x80,0xff,0xff,0x01,0xe0,0x3f,0x00,0xfe,0xff,0x03, + 0xf0,0x3f,0x00,0xfc,0xff,0x03,0xf0,0x3f,0x00,0xf8,0xff,0x03,0xf8,0x1f,0x00, + 0xf8,0xff,0x03,0xf8,0x1f,0x00,0xf0,0xff,0x03,0xfc,0x1f,0x00,0xe0,0xff,0x03, + 0xfc,0x1f,0x00,0xe0,0xff,0x03,0xfc,0x1f,0x00,0xe0,0xff,0x03,0xfc,0x1f,0x00, + 0xc0,0xff,0x03,0xfc,0x1f,0x00,0xc0,0xff,0x03,0xfc,0x1f,0x00,0xc0,0xff,0x03, + 0xfc,0x1f,0x00,0xc0,0xff,0x03,0xfc,0x1f,0x00,0xc0,0xff,0x03,0xf8,0x1f,0x00, + 0xc0,0xff,0x01,0xf8,0x1f,0x00,0xc0,0xff,0x01,0xf8,0x3f,0x00,0xc0,0xff,0x00, + 0xf0,0x3f,0x00,0xe0,0xff,0x00,0xf0,0x3f,0x00,0xe0,0x7f,0x00,0xe0,0x7f,0x00, + 0xe0,0x3f,0x00,0xc0,0xff,0x00,0xf0,0x1f,0x00,0x80,0xff,0x01,0xf8,0x0f,0x00, + 0x00,0xfe,0x03,0xfe,0x03,0x00,0x00,0xf8,0xff,0xff,0x00,0x00,0x00,0xe0,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xfe,0x03,0x00,0x00}; diff --git a/src/samples/clock/font/fiveE.xbm b/src/samples/clock/font/fiveE.xbm new file mode 100644 index 000000000..1c083bafb --- /dev/null +++ b/src/samples/clock/font/fiveE.xbm @@ -0,0 +1,29 @@ +#define fiveE_width 45 +#define fiveE_height 64 +static unsigned char fiveE_bits[] = { + 0x00,0xe0,0xff,0xff,0xff,0x07,0x00,0xe0,0xff,0xff,0xff,0x07,0x00,0xe0,0xff, + 0xff,0xff,0x07,0x00,0xe0,0xff,0xff,0xff,0x03,0x00,0xf0,0xff,0xff,0xff,0x03, + 0x00,0xf0,0xff,0xff,0xff,0x03,0x00,0xf0,0xff,0xff,0xff,0x01,0x00,0xf8,0xff, + 0xff,0xff,0x01,0x00,0xf8,0xff,0xff,0xff,0x01,0x00,0xf8,0xff,0xff,0xff,0x00, + 0x00,0xf8,0xff,0xff,0xff,0x00,0x00,0x3c,0x00,0x00,0x00,0x00,0x00,0x1c,0x00, + 0x00,0x00,0x00,0x00,0x1c,0x00,0x00,0x00,0x00,0x00,0x1c,0x00,0x00,0x00,0x00, + 0x00,0x0e,0x00,0x00,0x00,0x00,0x00,0x0e,0x00,0x00,0x00,0x00,0x00,0x0e,0x00, + 0x00,0x00,0x00,0x00,0x0f,0x00,0x00,0x00,0x00,0x00,0x7f,0x00,0x00,0x00,0x00, + 0x00,0xff,0x7f,0x00,0x00,0x00,0x00,0xff,0xff,0x07,0x00,0x00,0x80,0xff,0xff, + 0x1f,0x00,0x00,0x80,0xff,0xff,0xff,0x00,0x00,0x80,0xff,0xff,0xff,0x01,0x00, + 0xc0,0xff,0xff,0xff,0x07,0x00,0xc0,0xff,0xff,0xff,0x0f,0x00,0xc0,0xff,0xff, + 0xff,0x1f,0x00,0xc0,0xff,0xff,0xff,0x3f,0x00,0xe0,0xff,0xff,0xff,0x7f,0x00, + 0xe0,0xff,0xff,0xff,0x7f,0x00,0xe0,0xff,0xff,0xff,0xff,0x00,0x00,0xe0,0xff, + 0xff,0xff,0x00,0x00,0x00,0xf8,0xff,0xff,0x01,0x00,0x00,0xc0,0xff,0xff,0x01, + 0x00,0x00,0x00,0xff,0xff,0x01,0x00,0x00,0x00,0xfc,0xff,0x03,0x00,0x00,0x00, + 0xf0,0xff,0x03,0x00,0x00,0x00,0xe0,0xff,0x03,0x00,0x00,0x00,0x80,0xff,0x03, + 0x00,0x00,0x00,0x00,0xff,0x03,0x00,0x00,0x00,0x00,0xfe,0x03,0x00,0x00,0x00, + 0x00,0xfe,0x03,0x00,0x00,0x00,0x00,0xfc,0x03,0x00,0x00,0x00,0x00,0xfc,0x03, + 0x00,0x00,0x00,0x00,0xf8,0x03,0x00,0x00,0x00,0x00,0xf8,0x01,0x00,0x00,0x00, + 0x00,0xf8,0x01,0x00,0x00,0x00,0x00,0xf8,0x01,0x00,0x00,0x00,0x00,0xf8,0x00, + 0xe0,0x03,0x00,0x00,0xf8,0x00,0xf0,0x0f,0x00,0x00,0x78,0x00,0xf8,0x3f,0x00, + 0x00,0x7c,0x00,0xfc,0xff,0x00,0x00,0x3c,0x00,0xfc,0xff,0x01,0x00,0x1e,0x00, + 0xfc,0xff,0x03,0x00,0x1f,0x00,0xfc,0xff,0x0f,0x80,0x0f,0x00,0xfc,0xff,0x3f, + 0xc0,0x03,0x00,0xf8,0xff,0xff,0xff,0x01,0x00,0xf8,0xff,0xff,0xff,0x00,0x00, + 0xf0,0xff,0xff,0x3f,0x00,0x00,0xc0,0xff,0xff,0x0f,0x00,0x00,0x00,0xff,0xff, + 0x01,0x00,0x00,0x00,0xf8,0x0f,0x00,0x00,0x00}; diff --git a/src/samples/clock/font/fourE.xbm b/src/samples/clock/font/fourE.xbm new file mode 100644 index 000000000..552b82050 --- /dev/null +++ b/src/samples/clock/font/fourE.xbm @@ -0,0 +1,29 @@ +#define fourE_width 45 +#define fourE_height 64 +static unsigned char fourE_bits[] = { + 0x00,0x00,0x00,0xc0,0x3f,0x00,0x00,0x00,0x00,0xe0,0x3f,0x00,0x00,0x00,0x00, + 0xf0,0x3f,0x00,0x00,0x00,0x00,0xf8,0x3f,0x00,0x00,0x00,0x00,0xf8,0x3f,0x00, + 0x00,0x00,0x00,0xfc,0x3f,0x00,0x00,0x00,0x00,0xfe,0x3f,0x00,0x00,0x00,0x00, + 0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x80,0xff,0x3f,0x00, + 0x00,0x00,0xc0,0xff,0x3f,0x00,0x00,0x00,0xe0,0xff,0x3f,0x00,0x00,0x00,0xf0, + 0xff,0x3f,0x00,0x00,0x00,0xf0,0xff,0x3f,0x00,0x00,0x00,0xf8,0xff,0x3f,0x00, + 0x00,0x00,0x7c,0xff,0x3f,0x00,0x00,0x00,0x3e,0xff,0x3f,0x00,0x00,0x00,0x1e, + 0xff,0x3f,0x00,0x00,0x00,0x0f,0xff,0x3f,0x00,0x00,0x80,0x0f,0xff,0x3f,0x00, + 0x00,0xc0,0x07,0xff,0x3f,0x00,0x00,0xc0,0x03,0xff,0x3f,0x00,0x00,0xe0,0x03, + 0xff,0x3f,0x00,0x00,0xf0,0x01,0xff,0x3f,0x00,0x00,0xf0,0x00,0xff,0x3f,0x00, + 0x00,0x78,0x00,0xff,0x3f,0x00,0x00,0x7c,0x00,0xff,0x3f,0x00,0x00,0x3e,0x00, + 0xff,0x3f,0x00,0x00,0x1e,0x00,0xff,0x3f,0x00,0x00,0x0f,0x00,0xff,0x3f,0x00, + 0x80,0x0f,0x00,0xff,0x3f,0x00,0x80,0x07,0x00,0xff,0x3f,0x00,0xc0,0x03,0x00, + 0xff,0x3f,0x00,0xe0,0x03,0x00,0xff,0x3f,0x00,0xe0,0x01,0x00,0xff,0x3f,0x00, + 0xf0,0x00,0x00,0xff,0x3f,0x00,0x78,0x00,0x00,0xff,0x3f,0x00,0x7c,0x00,0x00, + 0xff,0x3f,0x00,0x3c,0x00,0x00,0xff,0x3f,0x00,0x1e,0x00,0x00,0xff,0x3f,0x00, + 0xfe,0xff,0xff,0xff,0xff,0x07,0xfe,0xff,0xff,0xff,0xff,0x07,0xfe,0xff,0xff, + 0xff,0xff,0x07,0xfe,0xff,0xff,0xff,0xff,0x07,0xfe,0xff,0xff,0xff,0xff,0x07, + 0xfe,0xff,0xff,0xff,0xff,0x07,0xfe,0xff,0xff,0xff,0xff,0x07,0xfe,0xff,0xff, + 0xff,0xff,0x07,0xfe,0xff,0xff,0xff,0xff,0x07,0xfe,0xff,0xff,0xff,0xff,0x07, + 0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00, + 0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00, + 0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00, + 0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00, + 0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00, + 0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00}; diff --git a/src/samples/clock/font/nineE.xbm b/src/samples/clock/font/nineE.xbm new file mode 100644 index 000000000..0e40a1202 --- /dev/null +++ b/src/samples/clock/font/nineE.xbm @@ -0,0 +1,29 @@ +#define nineE_width 45 +#define nineE_height 64 +static unsigned char nineE_bits[] = { + 0x00,0x00,0xff,0x0f,0x00,0x00,0x00,0xe0,0xff,0x3f,0x00,0x00,0x00,0xf8,0x07, + 0xff,0x00,0x00,0x00,0xfc,0x03,0xfe,0x03,0x00,0x00,0xfe,0x01,0xfc,0x07,0x00, + 0x00,0xff,0x01,0xf8,0x0f,0x00,0x80,0xff,0x00,0xf8,0x1f,0x00,0xc0,0xff,0x00, + 0xf8,0x1f,0x00,0xe0,0xff,0x00,0xf0,0x3f,0x00,0xe0,0xff,0x00,0xf0,0x7f,0x00, + 0xf0,0x7f,0x00,0xf0,0x7f,0x00,0xf0,0x7f,0x00,0xf0,0xff,0x00,0xf8,0x7f,0x00, + 0xf0,0xff,0x00,0xf8,0x7f,0x00,0xf0,0xff,0x01,0xf8,0x7f,0x00,0xe0,0xff,0x01, + 0xf8,0x7f,0x00,0xe0,0xff,0x01,0xfc,0x7f,0x00,0xe0,0xff,0x03,0xfc,0x7f,0x00, + 0xe0,0xff,0x03,0xfc,0x7f,0x00,0xe0,0xff,0x03,0xfc,0x7f,0x00,0xe0,0xff,0x03, + 0xfc,0x7f,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x07,0xfc,0xff,0x00, + 0xe0,0xff,0x07,0xfc,0xff,0x00,0xe0,0xff,0x07,0xfc,0xff,0x00,0xe0,0xff,0x07, + 0xf8,0xff,0x00,0xe0,0xff,0x07,0xf8,0xff,0x00,0xe0,0xff,0x07,0xf8,0xff,0x00, + 0xe0,0xff,0x07,0xf8,0xff,0x01,0xe0,0xff,0x07,0xf0,0xff,0x01,0xe0,0xff,0x07, + 0xf0,0xff,0x01,0xe0,0xff,0x03,0xe0,0xff,0x03,0xf0,0xff,0x03,0xe0,0xff,0x03, + 0xf0,0xff,0x03,0xc0,0xff,0x07,0xf0,0xff,0x03,0x80,0xff,0x0f,0xf0,0xff,0x03, + 0x00,0xff,0x7f,0xff,0xff,0x01,0x00,0xfe,0xff,0xff,0xff,0x01,0x00,0xf8,0xff, + 0xff,0xff,0x01,0x00,0xe0,0xff,0xf8,0xff,0x01,0x00,0x00,0x00,0xf8,0xff,0x00, + 0x00,0x00,0x00,0xfc,0xff,0x00,0x00,0x00,0x00,0xfc,0x7f,0x00,0x00,0x00,0x00, + 0xfc,0x7f,0x00,0x00,0x00,0x00,0xfe,0x3f,0x00,0x00,0x00,0x00,0xfe,0x3f,0x00, + 0x00,0x00,0x00,0xfe,0x1f,0x00,0x00,0x00,0x00,0xff,0x0f,0x00,0x00,0x00,0x00, + 0xff,0x0f,0x00,0x00,0x00,0x80,0xff,0x07,0x00,0x00,0x00,0xc0,0xff,0x03,0x00, + 0x00,0x00,0xc0,0xff,0x01,0x00,0x00,0x00,0xe0,0xff,0x00,0x00,0x00,0x00,0xf0, + 0x7f,0x00,0x00,0x00,0x00,0xf8,0x3f,0x00,0x00,0x00,0x00,0xfc,0x0f,0x00,0x00, + 0x00,0x00,0xfe,0x07,0x00,0x00,0x00,0x00,0xff,0x03,0x00,0x00,0x00,0xc0,0xff, + 0x00,0x00,0x00,0x00,0xf0,0x3f,0x00,0x00,0x00,0x00,0xfc,0x0f,0x00,0x00,0x00, + 0x00,0xff,0x03,0x00,0x00,0x00,0xf0,0x7f,0x00,0x00,0x00,0x00,0xf8,0x07,0x00, + 0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x00,0x00}; diff --git a/src/samples/clock/font/oneE.xbm b/src/samples/clock/font/oneE.xbm new file mode 100644 index 000000000..06821513b --- /dev/null +++ b/src/samples/clock/font/oneE.xbm @@ -0,0 +1,29 @@ +#define oneE_width 45 +#define oneE_height 64 +static unsigned char oneE_bits[] = { + 0x00,0x00,0x00,0x3c,0x00,0x00,0x00,0x00,0x00,0x3f,0x00,0x00,0x00,0x00,0xc0, + 0x3f,0x00,0x00,0x00,0x00,0xf8,0x3f,0x00,0x00,0x00,0x00,0xfe,0x3f,0x00,0x00, + 0x00,0x80,0xff,0x3f,0x00,0x00,0x00,0xf0,0xff,0x3f,0x00,0x00,0x00,0xfc,0xff, + 0x3f,0x00,0x00,0x00,0xff,0xff,0x3f,0x00,0x00,0xe0,0xff,0xff,0x3f,0x00,0x00, + 0xe0,0x80,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00, + 0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x80,0xff,0x7f,0x00,0x00,0x00,0x80,0xff,0x7f,0x00,0x00, + 0x00,0xe0,0xff,0xff,0x00,0x00,0x00,0xf8,0xff,0xff,0x03,0x00,0xe0,0xff,0xff, + 0xff,0xff,0x00,0xe0,0xff,0xff,0xff,0xff,0x00}; diff --git a/src/samples/clock/font/sevenE.xbm b/src/samples/clock/font/sevenE.xbm new file mode 100644 index 000000000..e6f2a45c1 --- /dev/null +++ b/src/samples/clock/font/sevenE.xbm @@ -0,0 +1,29 @@ +#define sevenE_width 45 +#define sevenE_height 64 +static unsigned char sevenE_bits[] = { + 0xe0,0xff,0xff,0xff,0xff,0x0f,0xf0,0xff,0xff,0xff,0xff,0x07,0xf0,0xff,0xff, + 0xff,0xff,0x07,0xf0,0xff,0xff,0xff,0xff,0x07,0xf0,0xff,0xff,0xff,0xff,0x03, + 0xf0,0xff,0xff,0xff,0xff,0x03,0xf8,0xff,0xff,0xff,0xff,0x03,0xf8,0xff,0xff, + 0xff,0xff,0x01,0xf8,0xff,0xff,0xff,0xff,0x01,0xf8,0xff,0xff,0xff,0xff,0x01, + 0xf8,0xff,0xff,0xff,0xff,0x00,0xfc,0xff,0xff,0xff,0xff,0x00,0xfc,0x03,0x00, + 0x80,0x7f,0x00,0x7c,0x00,0x00,0x80,0x7f,0x00,0x1c,0x00,0x00,0x80,0x7f,0x00, + 0x1c,0x00,0x00,0x80,0x3f,0x00,0x0e,0x00,0x00,0xc0,0x3f,0x00,0x0e,0x00,0x00, + 0xc0,0x3f,0x00,0x06,0x00,0x00,0xc0,0x1f,0x00,0x06,0x00,0x00,0xe0,0x1f,0x00, + 0x00,0x00,0x00,0xe0,0x1f,0x00,0x00,0x00,0x00,0xe0,0x0f,0x00,0x00,0x00,0x00, + 0xf0,0x0f,0x00,0x00,0x00,0x00,0xf0,0x0f,0x00,0x00,0x00,0x00,0xf8,0x07,0x00, + 0x00,0x00,0x00,0xf8,0x07,0x00,0x00,0x00,0x00,0xf8,0x07,0x00,0x00,0x00,0x00, + 0xfc,0x03,0x00,0x00,0x00,0x00,0xfc,0x03,0x00,0x00,0x00,0x00,0xfc,0x03,0x00, + 0x00,0x00,0x00,0xfe,0x01,0x00,0x00,0x00,0x00,0xfe,0x01,0x00,0x00,0x00,0x00, + 0xfe,0x01,0x00,0x00,0x00,0x00,0xff,0x00,0x00,0x00,0x00,0x00,0xff,0x00,0x00, + 0x00,0x00,0x80,0x7f,0x00,0x00,0x00,0x00,0x80,0x7f,0x00,0x00,0x00,0x00,0x80, + 0x7f,0x00,0x00,0x00,0x00,0xc0,0x3f,0x00,0x00,0x00,0x00,0xc0,0x3f,0x00,0x00, + 0x00,0x00,0xc0,0x3f,0x00,0x00,0x00,0x00,0xe0,0x1f,0x00,0x00,0x00,0x00,0xe0, + 0x1f,0x00,0x00,0x00,0x00,0xf0,0x1f,0x00,0x00,0x00,0x00,0xf0,0x0f,0x00,0x00, + 0x00,0x00,0xf0,0x0f,0x00,0x00,0x00,0x00,0xf8,0x0f,0x00,0x00,0x00,0x00,0xf8, + 0x07,0x00,0x00,0x00,0x00,0xf8,0x07,0x00,0x00,0x00,0x00,0xfc,0x07,0x00,0x00, + 0x00,0x00,0xfc,0x03,0x00,0x00,0x00,0x00,0xfc,0x03,0x00,0x00,0x00,0x00,0xfe, + 0x03,0x00,0x00,0x00,0x00,0xfe,0x01,0x00,0x00,0x00,0x00,0xff,0x01,0x00,0x00, + 0x00,0x00,0xff,0x01,0x00,0x00,0x00,0x00,0xff,0x00,0x00,0x00,0x00,0x80,0xff, + 0x00,0x00,0x00,0x00,0x80,0xff,0x00,0x00,0x00,0x00,0x80,0x7f,0x00,0x00,0x00, + 0x00,0xc0,0x7f,0x00,0x00,0x00,0x00,0xc0,0x3f,0x00,0x00,0x00,0x00,0xc0,0x3f, + 0x00,0x00,0x00,0x00,0xe0,0x3f,0x00,0x00,0x00}; diff --git a/src/samples/clock/font/sixE.xbm b/src/samples/clock/font/sixE.xbm new file mode 100644 index 000000000..7cad94409 --- /dev/null +++ b/src/samples/clock/font/sixE.xbm @@ -0,0 +1,29 @@ +#define sixE_width 45 +#define sixE_height 64 +static unsigned char sixE_bits[] = { + 0x00,0x00,0x00,0x00,0xf0,0x07,0x00,0x00,0x00,0x00,0xff,0x03,0x00,0x00,0x00, + 0xe0,0x7f,0x00,0x00,0x00,0x00,0xfc,0x0f,0x00,0x00,0x00,0x00,0xff,0x03,0x00, + 0x00,0x00,0xc0,0xff,0x00,0x00,0x00,0x00,0xf0,0x3f,0x00,0x00,0x00,0x00,0xf8, + 0x1f,0x00,0x00,0x00,0x00,0xfe,0x0f,0x00,0x00,0x00,0x00,0xff,0x07,0x00,0x00, + 0x00,0x80,0xff,0x03,0x00,0x00,0x00,0xe0,0xff,0x01,0x00,0x00,0x00,0xf0,0xff, + 0x00,0x00,0x00,0x00,0xf8,0x7f,0x00,0x00,0x00,0x00,0xf8,0x3f,0x00,0x00,0x00, + 0x00,0xfc,0x3f,0x00,0x00,0x00,0x00,0xfe,0x1f,0x00,0x00,0x00,0x00,0xff,0x1f, + 0x00,0x00,0x00,0x00,0xff,0x0f,0x00,0x00,0x00,0x80,0xff,0x0f,0x00,0x00,0x00, + 0xc0,0xff,0x07,0x00,0x00,0x00,0xc0,0xff,0x07,0x00,0x00,0x00,0xe0,0xff,0x07, + 0x00,0x00,0x00,0xe0,0xff,0x03,0x00,0x00,0x00,0xe0,0xff,0xc3,0x3f,0x00,0x00, + 0xf0,0xff,0xff,0xff,0x01,0x00,0xf0,0xff,0xff,0xff,0x07,0x00,0xf0,0xff,0xff, + 0xff,0x1f,0x00,0xf8,0xff,0x03,0xff,0x3f,0x00,0xf8,0xff,0x01,0xfc,0x7f,0x00, + 0xf8,0xff,0x01,0xfc,0xff,0x00,0xf8,0xff,0x01,0xf8,0xff,0x00,0xf8,0xff,0x01, + 0xf0,0xff,0x01,0xfc,0xff,0x00,0xf0,0xff,0x01,0xfc,0xff,0x00,0xf0,0xff,0x03, + 0xfc,0xff,0x00,0xf0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00, + 0xe0,0xff,0x07,0xfc,0xff,0x00,0xe0,0xff,0x07,0xfc,0xff,0x00,0xe0,0xff,0x07, + 0xfc,0xff,0x00,0xe0,0xff,0x07,0xfc,0xff,0x00,0xe0,0xff,0x07,0xfc,0xff,0x00, + 0xe0,0xff,0x07,0xf8,0xff,0x00,0xe0,0xff,0x07,0xf8,0xff,0x00,0xe0,0xff,0x07, + 0xf8,0xff,0x00,0xe0,0xff,0x07,0xf8,0xff,0x00,0xe0,0xff,0x07,0xf8,0xff,0x00, + 0xe0,0xff,0x07,0xf0,0xff,0x00,0xe0,0xff,0x03,0xf0,0xff,0x01,0xe0,0xff,0x03, + 0xf0,0xff,0x01,0xe0,0xff,0x03,0xe0,0xff,0x01,0xe0,0xff,0x01,0xe0,0xff,0x01, + 0xe0,0xff,0x01,0xc0,0xff,0x01,0xe0,0xff,0x01,0xc0,0xff,0x01,0xe0,0xff,0x00, + 0x80,0xff,0x01,0xe0,0x7f,0x00,0x00,0xff,0x03,0xe0,0x7f,0x00,0x00,0xfe,0x03, + 0xe0,0x3f,0x00,0x00,0xfc,0x07,0xf0,0x1f,0x00,0x00,0xf8,0x07,0xf0,0x0f,0x00, + 0x00,0xf0,0x0f,0xf8,0x03,0x00,0x00,0xc0,0x3f,0xfe,0x01,0x00,0x00,0x00,0xff, + 0x3f,0x00,0x00,0x00,0x00,0xf8,0x07,0x00,0x00}; diff --git a/src/samples/clock/font/slashE.xbm b/src/samples/clock/font/slashE.xbm new file mode 100644 index 000000000..570a58c6b --- /dev/null +++ b/src/samples/clock/font/slashE.xbm @@ -0,0 +1,21 @@ +#define slashE_width 25 +#define slashE_height 64 +static unsigned char slashE_bits[] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xfc,0xff,0x7f,0x00,0xfc,0xff, + 0x7f,0x00,0xfc,0xff,0x7f,0x00,0xfc,0xff,0x7f,0x00,0xfc,0xff,0x7f,0x00,0xfc, + 0xff,0x7f,0x00,0xfc,0xff,0x7f,0x00,0xfc,0xff,0x7f,0x00,0xfc,0xff,0x7f,0x00, + 0xfc,0xff,0x7f,0x00,0xfc,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00}; diff --git a/src/samples/clock/font/threeE.xbm b/src/samples/clock/font/threeE.xbm new file mode 100644 index 000000000..07f648fc9 --- /dev/null +++ b/src/samples/clock/font/threeE.xbm @@ -0,0 +1,29 @@ +#define threeE_width 45 +#define threeE_height 64 +static unsigned char threeE_bits[] = { + 0x00,0x00,0xfc,0x3f,0x00,0x00,0x00,0x80,0xff,0xff,0x00,0x00,0x00,0xe0,0xff, + 0xff,0x03,0x00,0x00,0xf0,0xff,0xff,0x07,0x00,0x00,0xfc,0xff,0xff,0x0f,0x00, + 0x00,0xfe,0xff,0xff,0x1f,0x00,0x00,0xff,0xff,0xff,0x3f,0x00,0x80,0x0f,0xe0, + 0xff,0x3f,0x00,0xc0,0x03,0xc0,0xff,0x3f,0x00,0xc0,0x01,0x00,0xff,0x7f,0x00, + 0xe0,0x00,0x00,0xff,0x7f,0x00,0x70,0x00,0x00,0xfe,0x7f,0x00,0x30,0x00,0x00, + 0xfe,0x7f,0x00,0x30,0x00,0x00,0xfc,0x7f,0x00,0x00,0x00,0x00,0xfc,0x7f,0x00, + 0x00,0x00,0x00,0xfc,0x7f,0x00,0x00,0x00,0x00,0xfc,0x3f,0x00,0x00,0x00,0x00, + 0xfc,0x3f,0x00,0x00,0x00,0x00,0xfc,0x1f,0x00,0x00,0x00,0x00,0xfc,0x1f,0x00, + 0x00,0x00,0x00,0xfe,0x0f,0x00,0x00,0x00,0x00,0xfe,0x07,0x00,0x00,0x00,0x00, + 0xff,0x01,0x00,0x00,0x00,0x80,0xff,0x00,0x00,0x00,0x00,0xc0,0xff,0x01,0x00, + 0x00,0x00,0xe0,0xff,0x07,0x00,0x00,0x00,0xf8,0xff,0x0f,0x00,0x00,0x00,0xfe, + 0xff,0x3f,0x00,0x00,0xc0,0xff,0xff,0x7f,0x00,0x00,0xc0,0xff,0xff,0x7f,0x00, + 0x00,0x00,0xff,0xff,0xff,0x00,0x00,0x00,0xf8,0xff,0xff,0x01,0x00,0x00,0xe0, + 0xff,0xff,0x01,0x00,0x00,0x80,0xff,0xff,0x01,0x00,0x00,0x00,0xff,0xff,0x03, + 0x00,0x00,0x00,0xfc,0xff,0x03,0x00,0x00,0x00,0xf8,0xff,0x03,0x00,0x00,0x00, + 0xf0,0xff,0x03,0x00,0x00,0x00,0xe0,0xff,0x07,0x00,0x00,0x00,0xc0,0xff,0x07, + 0x00,0x00,0x00,0xc0,0xff,0x07,0x00,0x00,0x00,0x80,0xff,0x07,0x00,0x00,0x00, + 0x80,0xff,0x03,0x00,0x00,0x00,0x00,0xff,0x03,0x00,0x00,0x00,0x00,0xff,0x03, + 0x00,0x00,0x00,0x00,0xff,0x03,0x00,0x00,0x00,0x00,0xff,0x03,0x00,0x00,0x00, + 0x00,0xfe,0x01,0x00,0x00,0x00,0x00,0xfe,0x01,0x00,0x00,0x00,0x00,0xfe,0x01, + 0xe0,0x01,0x00,0x00,0xff,0x00,0xf8,0x07,0x00,0x00,0x7f,0x00,0xfc,0x1f,0x00, + 0x00,0x7f,0x00,0xfc,0x3f,0x00,0x00,0x3f,0x00,0xfe,0x7f,0x00,0x80,0x1f,0x00, + 0xfe,0xff,0x00,0x80,0x0f,0x00,0xfe,0xff,0x01,0xc0,0x07,0x00,0xfc,0xff,0x03, + 0xe0,0x03,0x00,0xfc,0xff,0x0f,0xf0,0x01,0x00,0xf8,0xff,0x3f,0x7e,0x00,0x00, + 0xf8,0xff,0xff,0x3f,0x00,0x00,0xe0,0xff,0xff,0x0f,0x00,0x00,0x80,0xff,0xff, + 0x01,0x00,0x00,0x00,0xf8,0x0f,0x00,0x00,0x00}; diff --git a/src/samples/clock/font/twoE.xbm b/src/samples/clock/font/twoE.xbm new file mode 100644 index 000000000..9feb410cc --- /dev/null +++ b/src/samples/clock/font/twoE.xbm @@ -0,0 +1,29 @@ +#define twoE_width 45 +#define twoE_height 64 +static unsigned char twoE_bits[] = { + 0x00,0x00,0xfe,0x1f,0x00,0x00,0x00,0x80,0xff,0x7f,0x00,0x00,0x00,0xe0,0xff, + 0xff,0x01,0x00,0x00,0xf0,0xff,0xff,0x03,0x00,0x00,0xf8,0xff,0xff,0x07,0x00, + 0x00,0xfe,0xff,0xff,0x0f,0x00,0x00,0xfe,0xff,0xff,0x1f,0x00,0x00,0xff,0xff, + 0xff,0x3f,0x00,0x80,0xff,0xff,0xff,0x3f,0x00,0xc0,0xff,0xff,0xff,0x7f,0x00, + 0xc0,0x0f,0xf0,0xff,0x7f,0x00,0xe0,0x03,0xc0,0xff,0x7f,0x00,0xe0,0x01,0x00, + 0xff,0xff,0x00,0xf0,0x00,0x00,0xfe,0xff,0x00,0x70,0x00,0x00,0xfe,0xff,0x00, + 0x38,0x00,0x00,0xfc,0xff,0x00,0x38,0x00,0x00,0xfc,0xff,0x00,0x18,0x00,0x00, + 0xf8,0xff,0x00,0x00,0x00,0x00,0xf8,0xff,0x00,0x00,0x00,0x00,0xf8,0xff,0x00, + 0x00,0x00,0x00,0xf8,0xff,0x00,0x00,0x00,0x00,0xf8,0xff,0x00,0x00,0x00,0x00, + 0xf0,0x7f,0x00,0x00,0x00,0x00,0xf8,0x7f,0x00,0x00,0x00,0x00,0xf8,0x7f,0x00, + 0x00,0x00,0x00,0xf8,0x3f,0x00,0x00,0x00,0x00,0xf8,0x3f,0x00,0x00,0x00,0x00, + 0xf8,0x1f,0x00,0x00,0x00,0x00,0xfc,0x0f,0x00,0x00,0x00,0x00,0xfc,0x0f,0x00, + 0x00,0x00,0x00,0xfc,0x07,0x00,0x00,0x00,0x00,0xfe,0x03,0x00,0x00,0x00,0x00, + 0xfe,0x03,0x00,0x00,0x00,0x00,0xff,0x01,0x00,0x00,0x00,0x00,0xff,0x00,0x00, + 0x00,0x00,0x80,0x7f,0x00,0x00,0x00,0x00,0xc0,0x3f,0x00,0x00,0x00,0x00,0xc0, + 0x1f,0x00,0x00,0x00,0x00,0xe0,0x0f,0x00,0x00,0x00,0x00,0xf0,0x07,0x00,0x00, + 0x00,0x00,0xf0,0x03,0x00,0x00,0x00,0x00,0xf8,0x01,0x00,0x00,0x00,0x00,0xfc, + 0x00,0x00,0x00,0x00,0x00,0x3e,0x00,0x00,0x00,0x00,0x00,0x1f,0x00,0x00,0x00, + 0x00,0x00,0x0f,0x00,0x00,0x0e,0x00,0x80,0x07,0x00,0x00,0x06,0x00,0xc0,0x03, + 0x00,0x00,0x07,0x00,0xe0,0x01,0x00,0x00,0x07,0x00,0xf0,0x00,0x00,0x80,0x07, + 0x00,0x78,0x00,0x00,0xc0,0x03,0x00,0x7c,0x00,0x00,0xfc,0x03,0x00,0xfc,0xff, + 0xff,0xff,0x03,0x00,0xfe,0xff,0xff,0xff,0x03,0x00,0xff,0xff,0xff,0xff,0x03, + 0x80,0xff,0xff,0xff,0xff,0x01,0xc0,0xff,0xff,0xff,0xff,0x01,0xe0,0xff,0xff, + 0xff,0xff,0x01,0xf0,0xff,0xff,0xff,0xff,0x01,0xf8,0xff,0xff,0xff,0xff,0x01, + 0xfc,0xff,0xff,0xff,0xff,0x00,0xfe,0xff,0xff,0xff,0xff,0x00,0xfe,0xff,0xff, + 0xff,0xff,0x00,0xfe,0xff,0xff,0xff,0xff,0x00}; diff --git a/src/samples/clock/font/zeroE.xbm b/src/samples/clock/font/zeroE.xbm new file mode 100644 index 000000000..2d828f4e0 --- /dev/null +++ b/src/samples/clock/font/zeroE.xbm @@ -0,0 +1,29 @@ +#define zeroE_width 45 +#define zeroE_height 64 +static unsigned char zeroE_bits[] = { + 0x00,0x00,0xfe,0x07,0x00,0x00,0x00,0x80,0xff,0x1f,0x00,0x00,0x00,0xe0,0x0f, + 0x7f,0x00,0x00,0x00,0xf0,0x07,0xfe,0x00,0x00,0x00,0xf8,0x03,0xfc,0x01,0x00, + 0x00,0xfc,0x01,0xf8,0x03,0x00,0x00,0xfe,0x01,0xf8,0x07,0x00,0x00,0xff,0x01, + 0xf8,0x0f,0x00,0x00,0xff,0x00,0xf0,0x1f,0x00,0x80,0xff,0x00,0xf0,0x1f,0x00, + 0xc0,0xff,0x00,0xf0,0x3f,0x00,0xc0,0xff,0x00,0xf0,0x3f,0x00,0xe0,0xff,0x00, + 0xf0,0x7f,0x00,0xe0,0xff,0x00,0xf0,0x7f,0x00,0xf0,0xff,0x00,0xf0,0xff,0x00, + 0xf0,0xff,0x00,0xf0,0xff,0x00,0xf0,0xff,0x00,0xf0,0xff,0x00,0xf8,0xff,0x00, + 0xf0,0xff,0x01,0xf8,0xff,0x00,0xe0,0xff,0x01,0xf8,0xff,0x00,0xe0,0xff,0x01, + 0xf8,0xff,0x00,0xe0,0xff,0x01,0xf8,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00, + 0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03, + 0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00, + 0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03, + 0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00, + 0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03, + 0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00, + 0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03, + 0xfc,0xff,0x00,0xe0,0xff,0x03,0xfc,0xff,0x00,0xe0,0xff,0x03,0xf8,0xff,0x00, + 0xe0,0xff,0x01,0xf8,0xff,0x00,0xe0,0xff,0x01,0xf8,0xff,0x00,0xe0,0xff,0x01, + 0xf8,0xff,0x00,0xf0,0xff,0x01,0xf0,0xff,0x00,0xf0,0xff,0x00,0xf0,0xff,0x00, + 0xf0,0xff,0x00,0xf0,0xff,0x00,0xf0,0xff,0x00,0xe0,0xff,0x00,0xf0,0x7f,0x00, + 0xe0,0xff,0x00,0xf0,0x7f,0x00,0xc0,0xff,0x00,0xf0,0x7f,0x00,0xc0,0xff,0x00, + 0xf0,0x3f,0x00,0x80,0xff,0x00,0xf0,0x3f,0x00,0x80,0xff,0x00,0xf0,0x1f,0x00, + 0x00,0xff,0x01,0xf8,0x0f,0x00,0x00,0xfe,0x01,0xf8,0x0f,0x00,0x00,0xfc,0x01, + 0xf8,0x07,0x00,0x00,0xfc,0x03,0xfc,0x03,0x00,0x00,0xf8,0x03,0xfc,0x01,0x00, + 0x00,0xe0,0x07,0xfe,0x00,0x00,0x00,0xc0,0x9f,0x3f,0x00,0x00,0x00,0x00,0xff, + 0x0f,0x00,0x00,0x00,0x00,0xf8,0x01,0x00,0x00}; diff --git a/src/samples/clock/gifenc.c b/src/samples/clock/gifenc.c new file mode 100644 index 000000000..efd2d9c03 --- /dev/null +++ b/src/samples/clock/gifenc.c @@ -0,0 +1,303 @@ +/* + * All of the source code and documentation for gifenc is released into the + * public domain and provided without warranty of any kind. + * + * Author: Marcel Rodrigues + */ +#include "gifenc.h" + +#include +#include +#include +#include +#include +#include +#include + +static uint8_t vga[0x30] = { + 0x00, 0x00, 0x00, 0xAA, 0x00, 0x00, 0x00, 0xAA, 0x00, 0xAA, 0x55, 0x00, + 0x00, 0x00, 0xAA, 0xAA, 0x00, 0xAA, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0x55, 0x55, 0x55, 0xFF, 0x55, 0x55, 0x55, 0xFF, 0x55, 0xFF, 0xFF, 0x55, + 0x55, 0x55, 0xFF, 0xFF, 0x55, 0xFF, 0x55, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, +}; + +#pragma GCC diagnostic ignored "-Wconversion" +#pragma GCC diagnostic ignored "-Wsign-conversion" +#pragma GCC diagnostic ignored "-Wpointer-sign" + +struct Node { + uint16_t key; + struct Node *children[]; +}; +typedef struct Node Node; + +static Node *new_node(uint16_t key, int degree) +{ + Node *node = calloc(1, sizeof(*node) + degree * sizeof(Node *)); + if (node) + node->key = key; + return node; +} + +static Node *new_trie(int degree, int *nkeys) +{ + Node *root = new_node(0, degree); + /* Create nodes for single pixels. */ + for (*nkeys = 0; *nkeys < degree; (*nkeys)++) + root->children[*nkeys] = new_node(*nkeys, degree); + *nkeys += 2; /* skip clear code and stop code */ + return root; +} + +static void del_trie(Node *root, int degree) +{ + if (!root) + return; + for (int i = 0; i < degree; i++) + del_trie(root->children[i], degree); + free(root); +} + +static void put_loop(ge_GIF *gif, uint16_t loop); + +static void write_num(struct lwan_strbuf *buf, int16_t num) +{ + lwan_strbuf_append_char(buf, num & 0xff); + lwan_strbuf_append_char(buf, num >> 8); +} + +ge_GIF *ge_new_gif(struct lwan_strbuf *buf, + uint16_t width, + uint16_t height, + uint8_t *palette, + int depth, + int loop) +{ + int i, r, g, b, v; + ge_GIF *gif = calloc(1, sizeof(*gif) + 2 * width * height); + + if (!gif) + goto no_gif; + + gif->w = width; + gif->h = height; + gif->depth = depth > 1 ? depth : 2; + gif->frame = (uint8_t *)&gif[1]; + gif->back = &gif->frame[width * height]; + gif->buf = buf; + + lwan_strbuf_append_str(buf, "GIF89a", 6); + write_num(buf, width); + write_num(buf, height); + lwan_strbuf_append_str(buf, (const char[]){0xF0 | (depth - 1), 0x00, 0x00}, + 3); + + if (palette) { + lwan_strbuf_append_str(buf, palette, 3ull << depth); + } else if (depth <= 4) { + lwan_strbuf_append_str(buf, vga, 3ull << depth); + } else { + lwan_strbuf_append_str(buf, vga, sizeof(vga)); + + i = 0x10; + for (r = 0; r < 6; r++) { + for (g = 0; g < 6; g++) { + for (b = 0; b < 6; b++) { + lwan_strbuf_append_str( + buf, (const char[]){r * 51, g * 51, b * 51}, 3); + if (++i == 1 << depth) + goto done_gct; + } + } + } + for (i = 1; i <= 24; i++) { + v = i * 0xFF / 25; + lwan_strbuf_append_str(buf, (const char[]){v, v, v}, 3); + } + } + +done_gct: + if (loop >= 0 && loop <= 0xFFFF) + put_loop(gif, (uint16_t)loop); + return gif; + +no_gif: + return NULL; +} + +static void put_loop(ge_GIF *gif, uint16_t loop) +{ + lwan_strbuf_append_str(gif->buf, (const char[]){'!', 0xFF, 0x0B}, 3); + lwan_strbuf_append_str(gif->buf, "NETSCAPE2.0", 11); + lwan_strbuf_append_str(gif->buf, (const char[]){0x03, 0x01}, 2); + write_num(gif->buf, loop); + lwan_strbuf_append_char(gif->buf, '\0'); +} + +/* Add packed key to buffer, updating offset and partial. + * gif->offset holds position to put next *bit* + * gif->partial holds bits to include in next byte */ +static void put_key(ge_GIF *gif, uint16_t key, int key_size) +{ + int byte_offset, bit_offset, bits_to_write; + + byte_offset = gif->offset / 8; + bit_offset = gif->offset % 8; + gif->partial |= ((uint32_t)key) << bit_offset; + bits_to_write = bit_offset + key_size; + + while (bits_to_write >= 8) { + gif->buffer[byte_offset++] = gif->partial & 0xFF; + + if (byte_offset == 0xFF) { + lwan_strbuf_append_char(gif->buf, 0xff); + lwan_strbuf_append_str(gif->buf, gif->buffer, 0xff); + + byte_offset = 0; + } + + gif->partial >>= 8; + bits_to_write -= 8; + } + gif->offset = (gif->offset + key_size) % (0xFF * 8); +} + +static void end_key(ge_GIF *gif) +{ + int byte_offset; + + byte_offset = gif->offset / 8; + if (gif->offset % 8) + gif->buffer[byte_offset++] = gif->partial & 0xFF; + + lwan_strbuf_append_char(gif->buf, byte_offset); + lwan_strbuf_append_str(gif->buf, gif->buffer, byte_offset); + lwan_strbuf_append_char(gif->buf, '\0'); + + gif->offset = gif->partial = 0; +} + +static void +put_image(ge_GIF *gif, uint16_t w, uint16_t h, uint16_t x, uint16_t y) +{ + int nkeys, key_size, i, j; + Node *node, *child, *root; + int degree = 1 << gif->depth; + + lwan_strbuf_append_char(gif->buf, ','); + write_num(gif->buf, x); + write_num(gif->buf, y); + write_num(gif->buf, w); + write_num(gif->buf, h); + lwan_strbuf_append_str(gif->buf, (const char[]){0x00, gif->depth}, 2); + + root = node = new_trie(degree, &nkeys); + key_size = gif->depth + 1; + put_key(gif, degree, key_size); /* clear code */ + + for (i = y; i < y + h; i++) { + for (j = x; j < x + w; j++) { + uint8_t pixel = gif->frame[i * gif->w + j] & (degree - 1); + child = node->children[pixel]; + + if (child) { + node = child; + } else { + put_key(gif, node->key, key_size); + if (nkeys < 0x1000) { + if (nkeys == (1 << key_size)) + key_size++; + node->children[pixel] = new_node(nkeys++, degree); + } else { + put_key(gif, degree, key_size); /* clear code */ + del_trie(root, degree); + root = node = new_trie(degree, &nkeys); + key_size = gif->depth + 1; + } + node = root->children[pixel]; + } + } + } + + put_key(gif, node->key, key_size); + put_key(gif, degree + 1, key_size); /* stop code */ + end_key(gif); + del_trie(root, degree); +} + +static int +get_bbox(ge_GIF *gif, uint16_t *w, uint16_t *h, uint16_t *x, uint16_t *y) +{ + int i, j, k; + int left, right, top, bottom; + + left = gif->w; + right = 0; + top = gif->h; + bottom = 0; + k = 0; + for (i = 0; i < gif->h; i++) { + for (j = 0; j < gif->w; j++, k++) { + if (gif->frame[k] != gif->back[k]) { + if (j < left) + left = j; + if (j > right) + right = j; + if (i < top) + top = i; + if (i > bottom) + bottom = i; + } + } + } + + if (left != gif->w && top != gif->h) { + *x = left; + *y = top; + *w = right - left + 1; + *h = bottom - top + 1; + return 1; + } else { + return 0; + } +} + +static void set_delay(ge_GIF *gif, uint16_t d) +{ + lwan_strbuf_append_str(gif->buf, (const char[]){'!', 0xF9, 0x04, 0x04}, 4); + write_num(gif->buf, d); + lwan_strbuf_append_str(gif->buf, "\0\0", 2); +} + +void ge_add_frame(ge_GIF *gif, uint16_t delay) +{ + uint16_t w, h, x, y; + uint8_t *tmp; + + if (delay) + set_delay(gif, delay); + if (gif->nframes == 0) { + w = gif->w; + h = gif->h; + x = y = 0; + } else if (!get_bbox(gif, &w, &h, &x, &y)) { + /* image's not changed; save one pixel just to add delay */ + w = h = 1; + x = y = 0; + } + put_image(gif, w, h, x, y); + gif->nframes++; + tmp = gif->back; + gif->back = gif->frame; + gif->frame = tmp; +} + +struct lwan_strbuf *ge_close_gif(ge_GIF *gif) +{ + struct lwan_strbuf *buf = gif->buf; + + lwan_strbuf_append_char(gif->buf, ';'); + free(gif); + + return buf; +} diff --git a/src/samples/clock/gifenc.h b/src/samples/clock/gifenc.h new file mode 100644 index 000000000..d6a8fb30d --- /dev/null +++ b/src/samples/clock/gifenc.h @@ -0,0 +1,34 @@ +/* + * All of the source code and documentation for gifenc is released into the + * public domain and provided without warranty of any kind. + * + * Author: Marcel Rodrigues + */ + +#ifndef GIFENC_H +#define GIFENC_H + +#include +#include + +typedef struct ge_GIF { + struct lwan_strbuf *buf; + uint16_t w, h; + int depth; + int offset; + int nframes; + uint8_t *frame, *back; + uint32_t partial; + uint8_t buffer[0xFF]; +} ge_GIF; + +ge_GIF *ge_new_gif(struct lwan_strbuf *buf, + uint16_t width, + uint16_t height, + uint8_t *palette, + int depth, + int loop); +void ge_add_frame(ge_GIF *gif, uint16_t delay); +struct lwan_strbuf *ge_close_gif(ge_GIF *gif); + +#endif /* GIFENC_H */ diff --git a/src/samples/clock/main.c b/src/samples/clock/main.c new file mode 100644 index 000000000..3ef4df4b2 --- /dev/null +++ b/src/samples/clock/main.c @@ -0,0 +1,420 @@ +/* + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include + +#include "lwan-private.h" +#include "lwan-template.h" +#include "lwan-mod-redirect.h" +#include "gifenc.h" +#include "xdaliclock.h" +#include "blocks.h" +#include "pong.h" + +/* Font stolen from https://github.com/def-/time.gif */ +const uint8_t digital_clock_font[10][5] = { + [0] = {7, 5, 5, 5, 7}, [1] = {2, 2, 2, 2, 2}, [2] = {7, 1, 7, 4, 7}, + [3] = {7, 1, 3, 1, 7}, [4] = {5, 5, 7, 1, 1}, [5] = {7, 4, 7, 1, 7}, + [6] = {7, 4, 7, 5, 7}, [7] = {7, 1, 1, 1, 1}, [8] = {7, 5, 7, 5, 7}, + [9] = {7, 5, 7, 1, 7}, +}; + +static const struct lwan_key_value seriously_do_not_cache[] = { + {.key = "Content-Transfer-Encoding", .value = "binary"}, + {.key = "Cache-Control", .value = "no-cache"}, + {.key = "Cache-Control", .value = "no-store"}, + {.key = "Cache-Control", .value = "no-transform"}, + {}, +}; + +static const uint16_t width = 3 * 6 /* 6*3px wide digits */ + + 3 * 1 /* 3*1px wide decimal digit space */ + + 3 * 2 /* 2*3px wide minutes+seconds dots */; +static const uint16_t height = 5; + +static void destroy_gif(void *data) +{ + ge_GIF *gif = data; + + ge_close_gif(gif); +} + +struct tm* my_localtime(const time_t *t) +{ + static __thread struct tm result; + return localtime_r(t, &result); +} + +LWAN_HANDLER(clock) +{ + static const uint8_t base_offsets[] = {0, 0, 2, 2, 4, 4}; + ge_GIF *gif = ge_new_gif(response->buffer, width, height, NULL, 2, -1); + uint8_t dot_visible = 0; + + if (!gif) + return HTTP_INTERNAL_ERROR; + + coro_defer(request->conn->coro, destroy_gif, gif); + + response->mime_type = "image/gif"; + response->headers = (struct lwan_key_value *)seriously_do_not_cache; + + memset(gif->frame, 0, (size_t)(width * height)); + + for (int frame = 0; frame < 3600 * 2; frame++) { + time_t curtime; + char digits[8]; + int digit, line, base; + + curtime = time(NULL); + strftime(digits, sizeof(digits), "%H%M%S", my_localtime(&curtime)); + + for (digit = 0; digit < 6; digit++) { + int dig = digits[digit] - '0'; + uint8_t off = base_offsets[digit]; + + for (line = 0, base = digit * 4; line < 5; line++, base += width) { + gif->frame[base + 0 + off] = !!(digital_clock_font[dig][line] & 1<<2); + gif->frame[base + 1 + off] = !!(digital_clock_font[dig][line] & 1<<1); + gif->frame[base + 2 + off] = !!(digital_clock_font[dig][line] & 1<<0); + } + } + + gif->frame[8 + width] = dot_visible; + gif->frame[18 + width] = dot_visible; + gif->frame[8 + width * 3] = dot_visible; + gif->frame[18 + width * 3] = dot_visible; + dot_visible = dot_visible ? 0 : 3; + + ge_add_frame(gif, 0); + lwan_response_send_chunk(request); + lwan_request_sleep(request, 500); + } + + return HTTP_OK; +} + +static void destroy_xdaliclock(void *data) +{ + struct xdaliclock *xdc = data; + + xdaliclock_free(xdc); +} + +LWAN_HANDLER(dali) +{ + ge_GIF *gif = ge_new_gif(response->buffer, 320, 64, NULL, 2, -1); + struct xdaliclock *xdc; + uint32_t one_hour; + + if (!gif) + return HTTP_INTERNAL_ERROR; + + coro_defer(request->conn->coro, destroy_gif, gif); + + xdc = xdaliclock_new(gif); + if (!xdc) + return HTTP_INTERNAL_ERROR; + + coro_defer(request->conn->coro, destroy_xdaliclock, xdc); + + response->mime_type = "image/gif"; + response->headers = seriously_do_not_cache; + + memset(gif->frame, 0, (size_t)(width * height)); + + one_hour = 3600 * 1000 / xdaliclock_get_frame_time(xdc); + for (uint32_t frame = 0; frame < one_hour; frame++) { + xdaliclock_update(xdc); + + ge_add_frame(gif, 0); + lwan_response_send_chunk(request); + lwan_request_sleep(request, xdaliclock_get_frame_time(xdc)); + } + + return HTTP_OK; +} + +LWAN_HANDLER(blocks) +{ + ge_GIF *gif = ge_new_gif(response->buffer, 32, 16, NULL, 4, -1); + struct blocks blocks; + uint64_t total_waited = 0; + time_t last = 0; + bool odd_second = false; + + if (!gif) + return HTTP_INTERNAL_ERROR; + + coro_defer(request->conn->coro, destroy_gif, gif); + + blocks_init(&blocks, gif); + + response->mime_type = "image/gif"; + response->headers = seriously_do_not_cache; + + while (total_waited <= 3600000) { + uint64_t timeout; + time_t curtime; + + curtime = time(NULL); + if (curtime != last) { + char digits[5]; + + strftime(digits, sizeof(digits), "%H%M", my_localtime(&curtime)); + last = curtime; + odd_second = last & 1; + + for (int i = 0; i < 4; i++) + blocks.states[i].num_to_draw = digits[i] - '0'; + } + + timeout = blocks_draw(&blocks, odd_second); + total_waited += timeout; + + ge_add_frame(gif, 0); + lwan_response_send_chunk(request); + lwan_request_sleep(request, timeout); + } + + return HTTP_OK; +} + +LWAN_HANDLER(pong) +{ + ge_GIF *gif = ge_new_gif(response->buffer, 64, 32, NULL, 4, -1); + struct pong pong; + uint64_t total_waited = 0; + + if (!gif) + return HTTP_INTERNAL_ERROR; + + coro_defer(request->conn->coro, destroy_gif, gif); + + pong_init(&pong, gif); + + response->mime_type = "image/gif"; + response->headers = seriously_do_not_cache; + + while (total_waited <= 3600000) { + uint64_t timeout; + + timeout = pong_draw(&pong); + total_waited += timeout; + + ge_add_frame(gif, (uint16_t)timeout); + lwan_response_send_chunk(request); + lwan_request_sleep(request, timeout); + } + + return HTTP_OK; +} + +struct index { + const char *title; + const char *variant; + int width; +}; + +#undef TPL_STRUCT +#define TPL_STRUCT struct index +static const struct lwan_var_descriptor index_desc[] = { + TPL_VAR_STR_ESCAPE(title), + TPL_VAR_STR_ESCAPE(variant), + TPL_VAR_INT(width), + TPL_VAR_SENTINEL, +}; + +static struct lwan_tpl *index_tpl; + +LWAN_CONSTRUCTOR(initialize_template, 0) +{ + static const char index[] = + "\n" + "\n" + "\n" + "\n" + "{{title}}\n" + "\n" + "\n" + "
\n" + " Powered by the Lwan web server.\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + "
\n" + "
\n" + "
\n" + "
\n" + " Styles: " + "Digital · " + "Dali · " + "Pong · " + "Blocks\n" + "
\n" + "\n" + ""; + + index_tpl = lwan_tpl_compile_string_full(index, index_desc, + LWAN_TPL_FLAG_CONST_TEMPLATE); + if (!index_tpl) + lwan_status_critical("Could not compile template"); +} + +LWAN_HANDLER(templated_index) +{ + if (lwan_tpl_apply_with_buffer(index_tpl, response->buffer, data)) { + response->mime_type = "text/html"; + return HTTP_OK; + } + + return HTTP_INTERNAL_ERROR; +} + +static void setup_timezone(void) +{ + char tzpath_buf[PATH_MAX], *tzpath; + + tzpath = realpath("/etc/localtime", tzpath_buf); + if (!tzpath) + return; + + char *last_slash = strrchr(tzpath, '/'); + if (last_slash && !strcmp(last_slash, "/UTC")) { + /* If this system is set up to use UTC, there's no need to + * stat(/etc/localtime) every time my_localtime() is called like + * Glibc likes to do. */ + setenv("TZ", ":/etc/localtime", 1); + } +} + +int main(void) +{ + struct index sample_clock = { + .title = "Lwan Sample Clock", + .variant = "clock", + .width = 200, + }; + struct index dali_clock = { + .title = "Lwan Dali Clock", + .variant = "dali", + .width = 320, + }; + struct index blocks_clock = { + .title = "Lwan Blocks Clock", + .variant = "blocks", + .width = 320, + }; + struct index pong_clock = { + .title = "Lwan Pong Clock", + .variant = "pong", + .width = 320, + }; + const struct lwan_url_map default_map[] = { + { + .prefix = "/clock.gif", + .handler = LWAN_HANDLER_REF(clock), + }, + { + .prefix = "/dali.gif", + .handler = LWAN_HANDLER_REF(dali), + }, + { + .prefix = "/blocks.gif", + .handler = LWAN_HANDLER_REF(blocks), + }, + { + .prefix = "/pong.gif", + .handler = LWAN_HANDLER_REF(pong), + }, + { + .prefix = "/clock", + .handler = LWAN_HANDLER_REF(templated_index), + .data = &sample_clock, + }, + { + .prefix = "/dali", + .handler = LWAN_HANDLER_REF(templated_index), + .data = &dali_clock, + }, + { + .prefix = "/blocks", + .handler = LWAN_HANDLER_REF(templated_index), + .data = &blocks_clock, + }, + { + .prefix = "/pong", + .handler = LWAN_HANDLER_REF(templated_index), + .data = &pong_clock, + }, + { + .prefix = "/", + REDIRECT("/clock"), + }, + {}, + }; + struct lwan l; + + setup_timezone(); + + lwan_init(&l); + + lwan_set_url_map(&l, default_map); + lwan_main_loop(&l); + + lwan_shutdown(&l); + + return 0; +} diff --git a/src/samples/clock/numbers.c b/src/samples/clock/numbers.c new file mode 100644 index 000000000..4e39f577e --- /dev/null +++ b/src/samples/clock/numbers.c @@ -0,0 +1,33 @@ +/* Dali Clock - a melting digital clock for PalmOS. + * Copyright (c) 1991-2010 Jamie Zawinski + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + */ + +#ifdef LWAN_HAVE_CONFIG_H +#include "config.h" +#endif + +#include "numbers.h" + +#include "font/colonE.xbm" +#include "font/eightE.xbm" +#include "font/fiveE.xbm" +#include "font/fourE.xbm" +#include "font/nineE.xbm" +#include "font/oneE.xbm" +#include "font/sevenE.xbm" +#include "font/sixE.xbm" +#include "font/slashE.xbm" +#include "font/threeE.xbm" +#include "font/twoE.xbm" +#include "font/zeroE.xbm" +FONT(E); + +const struct raw_number *get_raw_numbers(void) { return numbers_E; } diff --git a/src/samples/clock/numbers.h b/src/samples/clock/numbers.h new file mode 100644 index 000000000..4819e54e5 --- /dev/null +++ b/src/samples/clock/numbers.h @@ -0,0 +1,42 @@ +/* Dali Clock - a melting digital clock for PalmOS. + * Copyright (c) 1991-2010 Jamie Zawinski + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + */ + +#ifndef __NUMBERS_H__ +#define __NUMBERS_H__ + +typedef unsigned char POS; + +struct raw_number { + const unsigned char *bits; + POS width, height; +}; + +#define FONT(X) \ + static const struct raw_number numbers_ ## X [] = { \ + { zero ## X ## _bits, zero ## X ## _width, zero ## X ## _height }, \ + { one ## X ## _bits, one ## X ## _width, one ## X ## _height }, \ + { two ## X ## _bits, two ## X ## _width, two ## X ## _height }, \ + { three ## X ## _bits, three ## X ## _width, three ## X ## _height }, \ + { four ## X ## _bits, four ## X ## _width, four ## X ## _height }, \ + { five ## X ## _bits, five ## X ## _width, five ## X ## _height }, \ + { six ## X ## _bits, six ## X ## _width, six ## X ## _height }, \ + { seven ## X ## _bits, seven ## X ## _width, seven ## X ## _height }, \ + { eight ## X ## _bits, eight ## X ## _width, eight ## X ## _height }, \ + { nine ## X ## _bits, nine ## X ## _width, nine ## X ## _height }, \ + { colon ## X ## _bits, colon ## X ## _width, colon ## X ## _height }, \ + { slash ## X ## _bits, slash ## X ## _width, slash ## X ## _height }, \ + { 0, }, \ +} + +const struct raw_number *get_raw_numbers (void); + +#endif /* __NUMBERS_H__ */ diff --git a/src/samples/clock/pong.c b/src/samples/clock/pong.c new file mode 100644 index 000000000..06d16a7e6 --- /dev/null +++ b/src/samples/clock/pong.c @@ -0,0 +1,288 @@ +/* + * C port of Daniel Esteban's Pong Clock for Lwan + * Copyright (C) 2019 Daniel Esteban + * Copyright (C) 2020 L. A. F. Pereira + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include +#include +#include + +#include "pong.h" + +extern const uint8_t digital_clock_font[10][5]; + +struct tm* my_localtime(const time_t *t); + +static double rand_double(double scale) +{ + return ((double)rand() / (double)(RAND_MAX)) * scale; +} + +static void pong_time_update(struct pong_time *pong_time) +{ + time_t cur_time = time(NULL); + + if (cur_time != pong_time->last_time) { + char digits[5]; + + strftime(digits, sizeof(digits), "%H%M", my_localtime(&cur_time)); + + for (int i = 0; i < 4; i++) + pong_time->time[i] = (char)(digits[i] - '0'); + + pong_time->hour = pong_time->time[0] * 10 + pong_time->time[1]; + pong_time->minute = pong_time->time[2] * 10 + pong_time->time[3]; + + pong_time->last_time = cur_time; + } +} + +static void pong_init_internal(struct pong *pong, ge_GIF *gif, bool reset) +{ + double ball_y = rand_double(16.0) + 8.0; + struct { + double y, target_y; + } left, right; + + if (reset) { + memcpy(&left, &pong->player_left, sizeof(left)); + memcpy(&right, &pong->player_right, sizeof(right)); + } else { + left.y = right.y = 8; + left.target_y = right.target_y = ball_y; + } + + *pong = (struct pong){ + .gif = gif, + .ball_x = {.pos = 31.0, .vel = 1.0}, + .ball_y = {.pos = ball_y, .vel = rand() % 2 ? 0.5 : -0.5}, + .player_left = {.y = 8, .target_y = ball_y}, + .player_right = {.y = 18, .target_y = ball_y}, + .player_loss = 0, + .game_stopped = 0, + }; + + pong_time_update(&pong->time); +} + +void pong_init(struct pong *pong, ge_GIF *gif) +{ + return pong_init_internal(pong, gif, false); +} + +static void draw_pixel(unsigned char *frame, int x, int y, unsigned char color) +{ + if (x < 64 && y < 32) + frame[y * 64 + x] = color; +} + +static void pong_draw_net(const struct pong *pong) +{ + for (int i = 1; i < 32; i += 2) + draw_pixel(pong->gif->frame, 31, i, 6); +} + +static void pong_draw_player(const struct pong *pong, int x, int y) +{ + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 8; j++) + draw_pixel(pong->gif->frame, x + i, y + j, 3); + } +} + +static void pong_draw_ball(const struct pong *pong) +{ + int x = (int)pong->ball_x.pos; + int y = (int)pong->ball_y.pos; + + draw_pixel(pong->gif->frame, x, y, 1); + draw_pixel(pong->gif->frame, x + 1, y, 1); + draw_pixel(pong->gif->frame, x, y + 1, 1); + draw_pixel(pong->gif->frame, x + 1, y + 1, 1); +} + +static void pong_draw_time(const struct pong *pong) +{ + static const uint8_t base_offsets[] = {23, 23, 25, 25}; + static const uint8_t colors[] = {0, 6}; + unsigned char *frame = pong->gif->frame; + + for (int digit = 0; digit < 4; digit++) { + int dig = pong->time.time[digit]; + uint8_t off = base_offsets[digit]; + + for (int line = 0, base = digit * 4; line < 5; line++, base += 64) { + frame[base + 0 + off] = colors[!!(digital_clock_font[dig][line] & 1<<2)]; + frame[base + 1 + off] = colors[!!(digital_clock_font[dig][line] & 1<<1)]; + frame[base + 2 + off] = colors[!!(digital_clock_font[dig][line] & 1<<0)]; + } + } +} + +static double pong_calculate_end_point(const struct pong *pong, bool hit) +{ + double x = pong->ball_x.pos; + double y = pong->ball_y.pos; + double vel_x = pong->ball_x.vel; + double vel_y = pong->ball_y.vel; + + for (;;) { + x += vel_x; + y += vel_y; + if (hit) { + if (x >= 60.0 || x <= 2.0) + return y; + } else { + if (x >= 62.0 || x <= 0.0) + return y; + } + if (y >= 30.0 || y <= 0.0) + vel_y = -vel_y; + } +} + +static void clamp(double *val, double min, double max) +{ + if (*val < min) { + *val = min; + } else if (*val > max) { + *val = max; + } +} + +uint64_t pong_draw(struct pong *pong) +{ + if (pong->game_stopped < 20) { + pong->game_stopped++; + goto draw; + } + + pong->ball_x.pos += pong->ball_x.vel; + pong->ball_y.pos += pong->ball_y.vel; + + if ((pong->ball_x.pos >= 60.0 && pong->player_loss != 1) || + (pong->ball_x.pos <= 2.0 && pong->player_loss != -1)) { + pong->ball_x.vel = -pong->ball_x.vel; + if (rand() % 4 > 0) { + if (rand() % 2 == 0) { + if (pong->ball_y.vel > 0.0 && pong->ball_y.vel < 2.5) + pong->ball_y.vel += 0.2; + else if (pong->ball_y.vel < 0.0 && pong->ball_y.vel > -2.5) + pong->ball_y.vel -= 0.2; + + if (pong->ball_x.pos >= 60.0) + pong->player_right.target_y += 1.0 + rand_double(3); + else + pong->player_left.target_y += 1.0 + rand_double(3); + } else { + if (pong->ball_y.vel > 0.5) + pong->ball_y.vel -= 0.2; + else if (pong->ball_y.vel < -0.5) + pong->ball_y.vel += 0.2; + + if (pong->ball_x.pos >= 60.0) + pong->player_right.target_y -= 1.0 + rand_double(3); + else + pong->player_left.target_y -= 1.0 + rand_double(3); + } + + clamp(&pong->player_left.target_y, 0.0, 24.0); + clamp(&pong->player_right.target_y, 0.0, 24.0); + } + } else if ((pong->ball_x.pos > 62.0 && pong->player_loss == 1) || + (pong->ball_x.pos < 1.0 && pong->player_loss == -1)) { + pong_init_internal(pong, pong->gif, true); + } + + if (pong->ball_y.pos >= 30.0 || pong->ball_y.pos <= 0.0) + pong->ball_y.vel = -pong->ball_y.vel; + + if (round(pong->ball_x.pos) == 40.0 + rand_double(13)) { + pong->player_left.target_y = pong->ball_y.pos - 3.0; + clamp(&pong->player_left.target_y, 0.0, 24.0); + } else if (round(pong->ball_x.pos) == 8 + rand_double(13)) { + pong->player_right.target_y = pong->ball_y.pos - 3.0; + clamp(&pong->player_right.target_y, 0.0, 24.0); + } + + if (pong->player_left.target_y > pong->player_left.y) + pong->player_left.y++; + if (pong->player_left.target_y < pong->player_left.y) + pong->player_left.y--; + + if (pong->player_right.target_y > pong->player_right.y) + pong->player_right.y++; + if (pong->player_right.target_y < pong->player_right.y) + pong->player_right.y--; + + /* If the ball is in the middle, check if we need to lose and calculate + * the endpoint to avoid/hit the ball */ + if (round(pong->ball_x.pos) == 32.0) { + struct pong_time cur_time = {}; + + pong_time_update(&cur_time); + + if (cur_time.minute != pong->time.minute && pong->player_loss == 0) { + /* Need to change one or the other */ + if (cur_time.minute == 0) /* Need to change the hour */ + pong->player_loss = 1; + else /* Need to change the minute */ + pong->player_loss = -1; + } + + if (pong->ball_x.vel < 0) { /* Moving to the left */ + pong->player_left.target_y = + pong_calculate_end_point(pong, pong->player_loss != -1) - 3; + if (pong->player_loss == -1) { /* We need to lose */ + if (pong->player_left.target_y < 16) + pong->player_left.target_y = 19 + rand_double(5); + else + pong->player_left.target_y = rand_double(5); + } + + clamp(&pong->player_left.target_y, 0.0, 24.0); + } else if (pong->ball_x.vel > 0) { /* Moving to the right */ + pong->player_right.target_y = + pong_calculate_end_point(pong, pong->player_loss != 1) - 3; + if (pong->player_loss == 1) { /* We need to lose */ + if (pong->player_right.target_y < 16) + pong->player_right.target_y = 19 + rand_double(5); + else + pong->player_right.target_y = rand_double(5); + } + + clamp(&pong->player_right.target_y, 0.0, 24.0); + } + + clamp(&pong->ball_y.pos, 0.0, 30.0); + } + +draw: + memset(pong->gif->frame, 0, 64 * 32); + pong_draw_net(pong); + pong_draw_time(pong); + pong_draw_player(pong, 0, (int)pong->player_left.y); + pong_draw_player(pong, 62, (int)pong->player_right.y); + pong_draw_ball(pong); + + return 8; +} diff --git a/src/samples/clock/pong.h b/src/samples/clock/pong.h new file mode 100644 index 000000000..6da18b9d1 --- /dev/null +++ b/src/samples/clock/pong.h @@ -0,0 +1,53 @@ +/* + * C port of Daniel Esteban's Pong Clock for Lwan + * Copyright (C) 2019 Daniel Esteban + * Copyright (C) 2020 L. A. F. Pereira + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#pragma once + +#include +#include +#include "gifenc.h" + +struct pong_time { + time_t last_time; + char time[4]; + int hour, minute; +}; + +struct pong { + ge_GIF *gif; + struct { + double pos; + double vel; + } ball_x, ball_y; + struct { + double y; + double target_y; + } player_left, player_right; + int player_loss; + int game_stopped; + struct pong_time time; +}; + +void pong_init(struct pong *pong, ge_GIF *gif); +uint64_t pong_draw(struct pong *pong); diff --git a/src/samples/clock/xdaliclock.c b/src/samples/clock/xdaliclock.c new file mode 100644 index 000000000..31d2a25ca --- /dev/null +++ b/src/samples/clock/xdaliclock.c @@ -0,0 +1,367 @@ +/* + * Lwan port of Dali Clock + * Copyright (c) 2018 L. A. F. Pereira + * + * Based on: + * Dali Clock - a melting digital clock for Pebble. + * Copyright (c) 2014 Joshua Wise + * Copyright (c) 1991-2010 Jamie Zawinski + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + */ + +#include +#include + +#include "gifenc.h" +#include "numbers.h" +#include "lwan-private.h" + +#define FRAMES_PER_SECOND 10 + +#if (FRAMES_PER_SECOND + 1) > 15 +#error Animation easing routine needs to be updated for this framerate +#endif + +/**************************************************************************/ +/* Scanline parsing. + * + * (Largely stolen from the PalmOS original). + */ + +#define MAX_SEGS_PER_LINE 2 + +struct scanline { + POS left[MAX_SEGS_PER_LINE], right[MAX_SEGS_PER_LINE]; +}; + +struct frame { + struct scanline scanlines[1]; +}; + +struct xdaliclock { + ge_GIF *gif_enc; + + int current_digits[8]; + int target_digits[8]; + + struct frame *temp_frame; + struct frame *clear_frame; + + uint32_t frame; +}; + +enum paint_color { BACKGROUND = 0, FOREGROUND = 3 }; + +static struct frame *base_frames[12]; +static POS char_height, char_width, colon_width; +static int digit_widths[8]; +static unsigned int easing[FRAMES_PER_SECOND]; + +struct tm* my_localtime(const time_t *t); + +static struct frame *frame_mk(int width, int height) +{ + struct frame *fr = malloc(sizeof(struct frame) + + (sizeof(struct scanline) * ((size_t)height - 1))); + POS half_width = (POS)(width / 2); + int x, y; + + if (!fr) + return NULL; + + for (y = 0; y < height; y++) { + for (x = 0; x < MAX_SEGS_PER_LINE; x++) { + fr->scanlines[y].left[x] = half_width; + fr->scanlines[y].right[x] = half_width; + } + } + + return fr; +} + +static inline bool get_bit(const unsigned char *bits, int x, int y, int width) +{ + return bits[(y * ((width + 7) >> 3)) + (x >> 3)] & 1 << (x & 7); +} + +static struct frame * +frame_from_pixmap(const unsigned char *bits, int width, int height) +{ + int x, y; + struct frame *frame; + POS *left, *right; + POS half_width = (POS)(width / 2); + + frame = frame_mk(width, height); + if (!frame) + return NULL; + + for (y = 0; y < height; y++) { + int seg, end; + x = 0; + + left = frame->scanlines[y].left; + right = frame->scanlines[y].right; + + for (seg = 0; seg < MAX_SEGS_PER_LINE; seg++) { + left[seg] = half_width; + right[seg] = half_width; + } + + for (seg = 0; seg < MAX_SEGS_PER_LINE; seg++) { + for (; x < width; x++) { + if (get_bit(bits, x, y, width)) + break; + } + if (x == width) + break; + left[seg] = (POS)x; + for (; x < width; x++) { + if (!get_bit(bits, x, y, width)) + break; + } + right[seg] = (POS)x; + } + + for (; x < width; x++) { + if (get_bit(bits, x, y, width)) { + lwan_status_critical( + "Font too curvy. Increase MAX_SEGS_PER_LINE " + "and recompile"); + return NULL; + } + } + + /* If there were any segments on this line, then replicate the last + one out to the end of the line. If it's blank, leave it alone, + meaning it will be a 0-pixel-wide line down the middle. + */ + end = seg; + if (end > 0) { + for (; seg < MAX_SEGS_PER_LINE; seg++) { + left[seg] = left[end - 1]; + right[seg] = right[end - 1]; + } + } + } + + return frame; +} + +LWAN_CONSTRUCTOR(initialize_numbers, 0) +{ + const struct raw_number *raw = get_raw_numbers(); + + char_width = raw[0].width; + char_height = raw[0].height; + colon_width = raw[10].width; + + for (unsigned int i = 0; i < N_ELEMENTS(base_frames); i++) { + struct frame *frame; + + frame = frame_from_pixmap(raw[i].bits, raw[i].width, raw[i].height); + if (!frame) + lwan_status_critical("Could not allocate frame"); + + /* The base frames leak, but it's only one per program instance */ + base_frames[i] = frame; + } + + const int widths[] = { + [0] = char_width, [1] = char_width, [2] = colon_width, + [3] = char_width, [4] = char_width, [5] = colon_width, + [6] = char_width, [7] = char_width, [8] = 0 /* avoid UB */, + }; + memcpy(digit_widths, widths, sizeof(digit_widths)); + + /* Pre-compute easing function. */ + for (unsigned int i = 0; i < FRAMES_PER_SECOND - 1; i++) + easing[i] = 65535u - 65535u / (1u << (i + 1)); + easing[FRAMES_PER_SECOND - 1] = 65535u; +} + +static inline POS lerp(POS a, POS b, unsigned int anim) +{ + uint32_t part_a = a * (65536 - anim); + uint32_t part_b = b * (anim + 1); + + return (POS)((part_a + part_b) / 65536); +} + +static const struct frame *frame_lerp(struct xdaliclock *xdc, int digit, unsigned int anim) +{ + const int from = xdc->current_digits[digit]; + const int to = xdc->target_digits[digit]; + const struct frame *tof = (to >= 0) ? base_frames[to] : xdc->clear_frame; + const struct frame *fromf; + int x, y; + + if (from == to) + return tof; + + fromf = (from >= 0) ? base_frames[from] : xdc->clear_frame; + for (y = 0; y < char_height; y++) { + struct scanline *line = &xdc->temp_frame->scanlines[y]; + const struct scanline *to_line = &tof->scanlines[y]; + const struct scanline *from_line = &fromf->scanlines[y]; + + for (x = 0; x < MAX_SEGS_PER_LINE; x++) { + line->left[x] = lerp(from_line->left[x], to_line->left[x], anim); + line->right[x] = lerp(from_line->right[x], to_line->right[x], anim); + } + } + + return xdc->temp_frame; +} + +static void draw_horizontal_line(struct xdaliclock *xdc, + int x1, + int x2, + int y, + int screen_width, + enum paint_color color) +{ + /* These unlikely checks won't happen with the default font. */ + + if (UNLIKELY(x1 > screen_width)) + x1 = screen_width; + else if (UNLIKELY(x1 < 0)) + x1 = 0; + + if (UNLIKELY(x2 > screen_width)) + x2 = screen_width; + else if (UNLIKELY(x2 < 0)) + x2 = 0; + + if (x1 == x2) + return; + + if (x1 > x2) { + int swap = x1; + x1 = x2; + x2 = swap; + } + + memset(xdc->gif_enc->frame + y * screen_width + x1, (uint8_t)color, + (size_t)(x2 - x1)); +} + +static void +frame_render(struct xdaliclock *xdc, const struct frame *frame, int x) +{ + for (int py = 0; py < char_height; py++) { + const struct scanline *line = &frame->scanlines[py]; + int last_right = 0; + int px = 0; + + goto first_iter; + + for (; px < MAX_SEGS_PER_LINE; px++) { + if ((line->left[px] == line->right[px] || + (line->left[px] == line->left[px - 1] && + line->right[px] == line->right[px - 1]))) { + continue; + } + + first_iter: + /* Erase the line between the last segment and this segment. + */ + draw_horizontal_line(xdc, x + last_right, x + line->left[px], py, + xdc->gif_enc->w, BACKGROUND); + + /* Draw the line of this segment. + */ + draw_horizontal_line(xdc, x + line->left[px], x + line->right[px], + py, xdc->gif_enc->w, FOREGROUND); + + last_right = line->right[px]; + } + + /* Erase the line between the last segment and the right edge. + */ + draw_horizontal_line(xdc, x + last_right, x + char_width, py, + xdc->gif_enc->w, BACKGROUND); + } +} + +void xdaliclock_update(struct xdaliclock *xdc) +{ + if (xdc->frame >= FRAMES_PER_SECOND) { + const time_t now = time(NULL); + const struct tm *tm = my_localtime(&now); + + memcpy(xdc->current_digits, xdc->target_digits, + sizeof(xdc->current_digits)); + + xdc->target_digits[0] = tm->tm_hour / 10; + xdc->target_digits[1] = tm->tm_hour % 10; + xdc->target_digits[2] = 10; + xdc->target_digits[3] = tm->tm_min / 10; + xdc->target_digits[4] = tm->tm_min % 10; + xdc->target_digits[5] = 10; + xdc->target_digits[6] = tm->tm_sec / 10; + xdc->target_digits[7] = tm->tm_sec % 10; + + xdc->frame = 0; + } + + for (int digit = 0, x = 0; digit < 8; x += digit_widths[digit++]) { + const struct frame *frame = frame_lerp(xdc, digit, easing[xdc->frame]); + + frame_render(xdc, frame, x); + } + + xdc->frame++; +} + +struct xdaliclock *xdaliclock_new(ge_GIF *ge) +{ + struct xdaliclock *xdc = malloc(sizeof(*xdc)); + + if (!xdc) + return NULL; + + xdc->temp_frame = frame_mk(char_width, char_height); + if (!xdc->temp_frame) + goto out; + + xdc->clear_frame = frame_mk(char_width, char_height); + if (!xdc->clear_frame) + goto out; + + for (unsigned int i = 0; i < N_ELEMENTS(xdc->target_digits); i++) + xdc->target_digits[i] = xdc->current_digits[i] = -1; + + /* Ensure time() is called the first time xdaliclock_update() is called */ + xdc->frame = FRAMES_PER_SECOND; + xdc->gif_enc = ge; + + return xdc; + +out: + free(xdc); + return NULL; +} + +void xdaliclock_free(struct xdaliclock *xdc) +{ + if (!xdc) + return; + + free(xdc->temp_frame); + free(xdc->clear_frame); + free(xdc); +} + +uint32_t xdaliclock_get_frame_time(const struct xdaliclock *xdc + __attribute__((unused))) +{ + return 1000 / FRAMES_PER_SECOND; +} diff --git a/src/samples/clock/xdaliclock.h b/src/samples/clock/xdaliclock.h new file mode 100644 index 000000000..6e6da0e0b --- /dev/null +++ b/src/samples/clock/xdaliclock.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include "gifenc.h" + +struct xdaliclock; + +struct xdaliclock *xdaliclock_new(ge_GIF *gif); +void xdaliclock_free(struct xdaliclock *xdaliclock); + +void xdaliclock_update(struct xdaliclock *xdaliclock); +uint32_t xdaliclock_get_frame_time(const struct xdaliclock *xdc); + + diff --git a/src/samples/forthsalon/CMakeLists.txt b/src/samples/forthsalon/CMakeLists.txt new file mode 100644 index 000000000..1d710e45d --- /dev/null +++ b/src/samples/forthsalon/CMakeLists.txt @@ -0,0 +1,24 @@ +add_executable(forth + forth.c +) + +target_compile_options(forth PRIVATE -DMAIN -DDUMP_CODE) + +target_link_libraries(forth + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} + m +) + +add_executable(forthsalon + forth.c + main.c +) + +target_compile_options(forthsalon PRIVATE -DDUMP_CODE) + +target_link_libraries(forthsalon + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} + m +) diff --git a/src/samples/forthsalon/forth.c b/src/samples/forthsalon/forth.c new file mode 100644 index 000000000..61c743131 --- /dev/null +++ b/src/samples/forthsalon/forth.c @@ -0,0 +1,1647 @@ +/* + * lwan - web server + * Copyright (c) 2025 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +/* + * This is a FORTH dialect compatible with the Forth Salon[1] dialect, + * to be used as a pixel shader in art projects. + * [1] https://forthsalon.appspot.com + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hash.h" +#include "lwan-array.h" +#include "lwan-private.h" + +#include "forth.h" + +#define TAIL_CALL return +#if defined __has_attribute +# if __has_attribute (musttail) +# undef TAIL_CALL +# define TAIL_CALL __attribute__((musttail)) return +# endif +#endif + +#define MAX_WORD_LEN 64 + +union forth_inst { + void (*callback)(union forth_inst *, + double *d_stack, + double *r_stack, + struct forth_vars *vars); + struct forth_code *code; + double number; + size_t pc; +}; + +DEFINE_ARRAY_TYPE(forth_code, union forth_inst) + +struct forth_builtin { + const char *name; + + union { + void (*callback)(union forth_inst *, + double *d_stack, + double *r_stack, + struct forth_vars *vars); + const char *(*callback_compiler)(struct forth_ctx *, const char *); + }; + + int d_pushes; + int d_pops; + int r_pushes; + int r_pops; +}; + +struct forth_word { + union { + struct forth_code code; + const struct forth_builtin *builtin; + }; + + /* name[0] is '\0' for built-in words; to get the name for those, go through + * the builtin pointer. */ + char name[]; +}; + +struct forth_ctx { + union { + struct { + double d_stack[32]; + double r_stack[32]; + }; + struct { + size_t j_stack[63]; + size_t *j; + }; + }; + + struct forth_word *defining_word; + struct forth_word *main; + struct hash *words; +}; + +static inline bool is_inside_word_def(const struct forth_ctx *ctx) +{ + return !ctx->defining_word || ctx->defining_word != ctx->main; +} + +static inline bool is_word_builtin(const struct forth_word *w) +{ + return w->name[0] == '\0'; +} + +static inline bool is_word_compiler(const struct forth_word *w) +{ + const struct forth_builtin *b = w->builtin; + return is_word_builtin(w) && + b >= SECTION_START_SYMBOL(forth_compiler_builtin, b) && + b < SECTION_STOP_SYMBOL(forth_compiler_builtin, b); +} + +static const struct forth_builtin *find_builtin_by_callback(void *callback) +{ + const struct forth_builtin *iter; + + LWAN_SECTION_FOREACH(forth_builtin, iter) { + if (iter->callback == callback) + return iter; + } + LWAN_SECTION_FOREACH(forth_compiler_builtin, iter) { + if (iter->callback_compiler == callback) + return iter; + } + + return NULL; +} + +static void op_number(union forth_inst *inst, + double *d_stack, + double *r_stack, + struct forth_vars *vars) +{ + *d_stack++ = inst[1].number; + TAIL_CALL inst[2].callback(&inst[2], d_stack, r_stack, vars); +} + +static void op_jump_if(union forth_inst *inst, + double *d_stack, + double *r_stack, + struct forth_vars *vars) +{ + size_t pc = (*--d_stack == 0.0) ? inst[1].pc : 2; + TAIL_CALL inst[pc].callback(&inst[pc], d_stack, r_stack, vars); +} + +static void op_jump(union forth_inst *inst, + double *d_stack, + double *r_stack, + struct forth_vars *vars) +{ + size_t pc = inst[1].pc; + TAIL_CALL inst[pc].callback(&inst[pc], d_stack, r_stack, vars); +} + +static void op_nop(union forth_inst *inst, + double *d_stack, + double *r_stack, + struct forth_vars *vars) +{ + TAIL_CALL inst[1].callback(&inst[1], d_stack, r_stack, vars); +} + +static void op_halt(union forth_inst *inst __attribute__((unused)), + double *d_stack, + double *r_stack, + struct forth_vars *vars) +{ + vars->final_d_stack_ptr = d_stack; + vars->final_r_stack_ptr = r_stack; +} + +static void op_eval_code(union forth_inst *inst __attribute__((unused)), + double *d_stack, + double *r_stack, + struct forth_vars *vars) +{ + lwan_status_critical("eval_code instruction executed after inlining"); + __builtin_unreachable(); +} + +static bool check_stack_effects(const struct forth_ctx *ctx, + struct forth_word *w) +{ + /* FIXME: this isn't correct when we have JUMP_IF and JUMP + * instructions: the number of items in the stacks isn't reset + * to the beginning of either if/else block. */ + const union forth_inst *inst; + int items_in_d_stack = 0; + int items_in_r_stack = 0; + + assert(!is_word_builtin(w)); + + LWAN_ARRAY_FOREACH(&w->code, inst) { + if (inst->callback == op_number) { + items_in_d_stack++; + inst++; /* skip number immediate */ + continue; + } + if (inst->callback == op_jump_if) { + if (UNLIKELY(!items_in_d_stack)) { + lwan_status_error("Word `if' requires 1 item(s) in the D stack"); + return false; + } + items_in_d_stack--; + inst++; /* skip pc immediate */ + continue; + } + if (inst->callback == op_jump) { + inst++; /* skip pc immediate */ + continue; + } + if (inst->callback == op_halt) { + continue; /* no immediate for halt */ + } + if (inst->callback == op_nop) { + continue; /* no immediate for nop */ + } + if (UNLIKELY(inst->callback == op_eval_code)) { + lwan_status_critical("eval_code after inlining"); + __builtin_unreachable(); + } + + /* all other built-ins */ + const struct forth_builtin *b = find_builtin_by_callback(inst->callback); + if (UNLIKELY(!b)) { + lwan_status_critical("Can't find builtin word by callback"); + __builtin_unreachable(); + } + + if (UNLIKELY(items_in_d_stack < b->d_pops)) { + lwan_status_error("Word `%s' requires %d item(s) in the D stack", + b->name, b->d_pops); + return false; + } + if (UNLIKELY(items_in_r_stack < b->r_pops)) { + lwan_status_error("Word `%s' requires %d item(s) in the R stack", + b->name, b->r_pops); + return false; + } + + items_in_d_stack -= b->d_pops; + items_in_d_stack += b->d_pushes; + items_in_r_stack -= b->r_pops; + items_in_r_stack += b->r_pushes; + } + + if (UNLIKELY(items_in_d_stack >= (int)N_ELEMENTS(ctx->d_stack))) { + lwan_status_error("Program would cause a stack overflow in the D stack"); + return false; + } + if (UNLIKELY(items_in_r_stack >= (int)N_ELEMENTS(ctx->r_stack))) { + lwan_status_error("Program would cause a stack overflow in the R stack"); + return false; + } + + if (UNLIKELY(items_in_d_stack >= (int)N_ELEMENTS(ctx->d_stack))) { + lwan_status_error("Program would cause a stack overflow in the D stack"); + return false; + } + if (UNLIKELY(items_in_r_stack >= (int)N_ELEMENTS(ctx->r_stack))) { + lwan_status_error("Program would cause a stack overflow in the R stack"); + return false; + } + + if (UNLIKELY(items_in_d_stack < 0)) { + lwan_status_error("Program would underflow the D stack"); + return false; + } + if (UNLIKELY(items_in_r_stack < 0)) { + lwan_status_error("Program would underflow the R stack"); + return false; + } + + return true; +} + +#define JS_PUSH(val_) \ + ({ \ + if (j > (jump_stack + 64)) \ + return false; \ + *j++ = (val_); \ + }) +#define JS_POP(val_) \ + ({ \ + if (j <= jump_stack) \ + return false; \ + *--j; \ + }) + +#if defined(DUMP_CODE) +static void dump_code(const struct forth_code *code) +{ + const union forth_inst *inst; + + printf("dumping code @ %p\n", code); + + LWAN_ARRAY_FOREACH (code, inst) { + printf("%08zu ", + forth_code_get_elem_index(code, (union forth_inst *)inst)); + + if (inst->callback == op_number) { + inst++; + printf("number %lf\n", inst->number); + } else if (inst->callback == op_jump_if) { + printf("if [next +%zu, abs %zu]\n", inst[1].pc, + forth_code_get_elem_index(code, (union forth_inst *)inst) + + inst[1].pc); + inst++; + } else if (inst->callback == op_jump) { + printf("jump to +%zu, abs %zu\n", inst[1].pc, + forth_code_get_elem_index(code, (union forth_inst *)inst) + + inst[1].pc); + inst++; + } else if (inst->callback == op_halt) { + printf("halt\n"); + } else if (inst->callback == op_nop) { + printf("nop\n"); + } else if (UNLIKELY(inst->callback == op_eval_code)) { + lwan_status_critical("eval_code shouldn't exist here"); + __builtin_unreachable(); + } else { + const struct forth_builtin *b = + find_builtin_by_callback(inst->callback); + if (b) { + if (b->name[0] == ' ') { + printf("call private builtin '%s'\n", b->name + 1); + } else { + printf("call builtin '%s'\n", b->name); + } + } else { + printf("*** inconsistency; value = %zu ***\n", inst->pc); + } + } + } +} +#endif + +bool forth_run(struct forth_ctx *ctx, struct forth_vars *vars) +{ + union forth_inst *instr = forth_code_get_elem(&ctx->main->code, 0); + instr->callback(instr, ctx->d_stack, ctx->r_stack, vars); + return true; +} + +static bool parse_number(const char *ptr, double *number) +{ + char *endptr; + + errno = 0; + *number = strtod(ptr, &endptr); + + if (errno != 0) + return false; + + if (*endptr != '\0') + return false; + + return true; +} + +static struct forth_word *new_builtin_word(struct forth_ctx *ctx, + const struct forth_builtin *builtin) +{ + struct forth_word *word = malloc(sizeof(*word) + 1); + if (UNLIKELY(!word)) + return NULL; + + word->builtin = builtin; + word->name[0] = '\0'; + + if (!hash_add(ctx->words, builtin->name, word)) + return word; + + free(word); + return NULL; +} + +static struct forth_word * +new_user_word(struct forth_ctx *ctx, const char *name) +{ + const size_t len = strlen(name); + if (UNLIKELY(len > MAX_WORD_LEN)) + return NULL; + + struct forth_word *word = malloc(sizeof(*word) + len + 1); + if (UNLIKELY(!word)) + return NULL; + + memcpy(word->name, name, len); + word->name[len] = '\0'; + + forth_code_init(&word->code); + + if (!hash_add(ctx->words, word->name, word)) + return word; + + free(word); + return NULL; +} + +static struct forth_word *new_word(struct forth_ctx *ctx, + const char *name, + const struct forth_builtin *builtin) +{ + return builtin ? new_builtin_word(ctx, builtin) + : new_user_word(ctx, name); +} + +#define EMIT(arg) \ + ({ \ + union forth_inst *emitted = \ + forth_code_append(&ctx->defining_word->code); \ + if (UNLIKELY(!emitted)) \ + return NULL; \ + *emitted = (union forth_inst){arg}; \ + forth_code_get_elem_index(&ctx->defining_word->code, emitted); \ + }) + +static bool peephole_1(struct forth_ctx *ctx, + struct forth_code *code, + const struct forth_builtin *b) +{ + union forth_inst *last = + forth_code_get_elem(code, forth_code_len(code) - 1); + + if (streq(b->name, "+")) { + const struct forth_word *w = hash_find(ctx->words, "*"); + assert(w != NULL); + assert(is_word_builtin(w)); + if (last[0].callback == w->builtin->callback) { + w = hash_find(ctx->words, " fma"); + assert(w != NULL); + assert(is_word_builtin(w)); + last[0].callback = w->builtin->callback; + return true; + } + } else if (streq(b->name, "*")) { + const struct forth_builtin *prev_b = + find_builtin_by_callback(last[0].callback); + if (prev_b && streq(prev_b->name, "pi")) { + const struct forth_word *w = hash_find(ctx->words, " multpi"); + assert(w != NULL); + assert(is_word_builtin(w)); + last[0].callback = w->builtin->callback; + return true; + } + } else if (streq(b->name, "dup")) { + if (last[0].callback == b->callback) { + const struct forth_word *w = hash_find(ctx->words, " dupdup"); + assert(w != NULL); + assert(is_word_builtin(w)); + last[0].callback = w->builtin->callback; + return true; + } + } else if (streq(b->name, "swap")) { + const struct forth_word *w = hash_find(ctx->words, "-rot"); + assert(w != NULL); + assert(is_word_builtin(w)); + if (last[0].callback == w->builtin->callback) { + w = hash_find(ctx->words, " -rotswap"); + last[0].callback = w->builtin->callback; + return true; + } + + w = hash_find(ctx->words, ">="); + assert(w != NULL); + assert(is_word_builtin(w)); + if (last[0].callback == w->builtin->callback) { + w = hash_find(ctx->words, " >=swap"); + last[0].callback = w->builtin->callback; + return true; + } + } else if (streq(b->name, " div2")) { + const struct forth_word *w = hash_find(ctx->words, " multpi"); + assert(w != NULL); + assert(is_word_builtin(w)); + if (last[0].callback == w->builtin->callback) { + w = hash_find(ctx->words, " multhalfpi"); + last[0].callback = w->builtin->callback; + return true; + } + } + + return false; +} + +static bool peephole_n(struct forth_ctx *ctx, + struct forth_code *code, + const struct forth_builtin *b) +{ + union forth_inst *last = + forth_code_get_elem(code, forth_code_len(code) - 1); + + if (streq(b->name, "/")) { + if (last[-1].callback == op_number && last[0].number == 2.0) { + const struct forth_word *w = hash_find(ctx->words, " div2"); + assert(w != NULL); + assert(is_word_builtin(w)); + last[-1].callback = w->builtin->callback; + code->base.elements--; + return true; + } + } else if (streq(b->name, "+")) { + if (forth_code_len(code) >= 4) { + if (last[-1].callback == op_number && + last[-3].callback == op_number) { + last[-2].number += last[0].number; + code->base.elements -= 2; + return true; + } + } + } else if (streq(b->name, "-")) { + if (forth_code_len(code) >= 4) { + if (last[-1].callback == op_number && + last[-3].callback == op_number) { + last[-2].number -= last[0].number; + code->base.elements -= 2; + return true; + } + } + } else if (streq(b->name, "/")) { + if (forth_code_len(code) >= 4) { + if (last[-1].callback == op_number && + last[-3].callback == op_number) { + if (last[0].number == 0.0) { + last[-2].number = __builtin_inf(); + } else { + last[-2].number /= last[0].number; + } + code->base.elements -= 2; + return true; + } + } + } else if (streq(b->name, "*")) { + if (last[-1].callback == op_number && last[0].number == 2.0) { + const struct forth_word *w = hash_find(ctx->words, " mult2"); + assert(w != NULL); + assert(is_word_builtin(w)); + last[-1].callback = w->builtin->callback; + code->base.elements--; + return true; + } + + if (forth_code_len(code) >= 4) { + if (last[-1].callback == op_number && + last[-3].callback == op_number) { + last[-2].number *= last[0].number; + code->base.elements -= 2; + return true; + } + } + } else if (streq(b->name, "**")) { + if (last[-1].callback == op_number && last[0].number == 2.0) { + const struct forth_word *w = hash_find(ctx->words, " pow2"); + assert(w != NULL); + assert(is_word_builtin(w)); + last[-1].callback = w->builtin->callback; + code->base.elements--; + return true; + } + } else if (streq(b->name, " mult2")) { + if (last[-1].callback == op_number) { + last[0].number *= 2; + return true; + } + } + + return false; +} + +static bool peephole(struct forth_ctx *ctx) +{ + /* FIXME: constprop? */ + /* FIXME: This is small enough that the current implementation is + * fine, but if we end up adding quite a bit more rules, we should + * look into making the rule declaration a bit better. */ + /* FIXME: This should look at the generated code after inlining to + * match code between words. This isn't currently possible because + * of branching with if/else/then, but we can make it work by + * peepholing the op_nop instruction instead of handling it inside + * the inliner. */ + /* FIXME: Other optimizations are possible -- for instance, a + * constant folding step seems trivial to do. However, things like + * strength reduction are a bit tricky considering all numbers are + * floating pointing numbers (even if they're double precision). + * More sophisticated techniques could be employed but with the + * simplistic implementation of this compiler, it might not be + * worth the trouble. */ + struct forth_code *code = &ctx->defining_word->code; + struct forth_code orig_code = *code; + union forth_inst *inst; + bool modified = false; + union forth_inst *new_inst; + size_t jump_stack[64]; + size_t *j = jump_stack; + + forth_code_init(code); + + LWAN_ARRAY_FOREACH (&orig_code, inst) { + const struct forth_builtin *b = + find_builtin_by_callback(inst->callback); + if (b) { + if (forth_code_len(code) > 1) { + if (peephole_1(ctx, code, b)) { + modified = true; + continue; + } + } + if (forth_code_len(code) > 2) { + if (peephole_n(ctx, code, b)) { + modified = true; + continue; + } + } + } + + new_inst = forth_code_append(code); + if (!new_inst) + goto out; + + *new_inst = *inst; + + bool has_imm = false; + if (inst->callback == op_jump_if) { + JS_PUSH(forth_code_len(code)); + has_imm = true; + } else if (inst->callback == op_jump) { + union forth_inst *if_inst = forth_code_get_elem(code, JS_POP()); + if_inst->pc = forth_code_len(code) + + forth_code_get_elem_index(code, if_inst) - 2; + + JS_PUSH(forth_code_len(code)); + has_imm = true; + } else if (inst->callback == op_nop) { + union forth_inst *else_inst = + forth_code_get_elem(code, JS_POP()); + else_inst->pc = forth_code_len(code) - + forth_code_get_elem_index(code, else_inst) + 1; + } else if (inst->callback == op_number) { + has_imm = true; + } + + if (has_imm) { + new_inst = forth_code_append(code); + if (!new_inst) + goto out; + + inst++; + *new_inst = *inst; + } + } + + forth_code_reset(&orig_code); + return modified; + +out: + forth_code_reset(code); + lwan_status_error("Could not run peephole optimizer"); + return false; +} + +static const char *found_word(struct forth_ctx *ctx, + const char *code, + const char *word, + size_t word_len) +{ + if (UNLIKELY(word_len > MAX_WORD_LEN)) { + lwan_status_error("Word too long: %zu characters, expecting at most 64", + word_len); + return NULL; + } + + const char *word_copy = strndupa(word, word_len); + + double number; + if (parse_number(word_copy, &number)) { + if (UNLIKELY(!ctx->defining_word)) { + lwan_status_error("Can't redefine number %lf", number); + return NULL; + } + + EMIT(.callback = op_number); + EMIT(.number = number); + + return code; + } + + struct forth_word *w = hash_find(ctx->words, word_copy); + if (ctx->defining_word) { + if (UNLIKELY(!w)) { + lwan_status_error("Undefined word: \"%.*s\"", (int)word_len, word); + return NULL; + } + + if (is_word_builtin(w)) { + if (is_word_compiler(w)) + return w->builtin->callback_compiler(ctx, code); + + EMIT(.callback = w->builtin->callback); + } else { + EMIT(.callback = op_eval_code); + EMIT(.code = &w->code); + } + + return code; + } + + if (UNLIKELY(w != NULL)) { + lwan_status_error("Can't redefine %sword \"%.*s\"", + is_word_builtin(w) ? "built-in " : "", (int)word_len, + word); + return NULL; + } + + w = new_word(ctx, word_copy, NULL); + if (UNLIKELY(!w)) { + lwan_status_error("Can't create new word"); + return NULL; + } + + ctx->defining_word = w; + return code; +} + +static bool inline_calls_code(const struct forth_code *orig_code, + struct forth_code *new_code, + int nested) +{ + const union forth_inst *inst; + size_t jump_stack[64]; + size_t *j = jump_stack; + + if (!nested) { + lwan_status_error("Recursion limit reached while inlining"); + return false; + } + + LWAN_ARRAY_FOREACH (orig_code, inst) { + if (inst->callback == op_eval_code) { + inst++; + if (!inline_calls_code(inst->code, new_code, nested - 1)) + return false; + } else { + union forth_inst *new_inst = forth_code_append(new_code); + if (!new_inst) + return false; + + *new_inst = *inst; + + bool has_imm = false; + if (inst->callback == op_jump_if) { + JS_PUSH(forth_code_len(new_code)); + has_imm = true; + } else if (inst->callback == op_jump) { + union forth_inst *if_inst = + forth_code_get_elem(new_code, JS_POP()); + if_inst->pc = forth_code_len(new_code) + + forth_code_get_elem_index(new_code, if_inst) - 2; + + JS_PUSH(forth_code_len(new_code)); + has_imm = true; + } else if (inst->callback == op_nop) { + union forth_inst *else_inst = + forth_code_get_elem(new_code, JS_POP()); + else_inst->pc = forth_code_len(new_code) - + forth_code_get_elem_index(new_code, else_inst) + 1; + } else if (inst->callback == op_number) { + has_imm = true; + } + + if (has_imm) { + new_inst = forth_code_append(new_code); + if (!new_inst) + return false; + + inst++; + *new_inst = *inst; + } + } + } + + return true; +} + +static bool inline_calls(struct forth_ctx *ctx) +{ + struct forth_code new_main; + + forth_code_init(&new_main); + if (!inline_calls_code(&ctx->main->code, &new_main, 100)) { + forth_code_reset(&new_main); + return false; + } + + forth_code_reset(&ctx->main->code); + ctx->main->code = new_main; + + return true; +} + +bool forth_parse_string(struct forth_ctx *ctx, const char *code) +{ + assert(ctx); + + ctx->j = ctx->j_stack; + + while (*code) { + while (isspace(*code)) + code++; + + const char *word_ptr = code; + + while (true) { + if (*code == '\0') { + if (word_ptr == code) + goto finish; + break; + } + if (isspace(*code)) + break; + if (!isprint(*code)) + return false; + code++; + } + + code = found_word(ctx, code, word_ptr, (size_t)(code - word_ptr)); + if (!code) + return false; + + if (*code == '\0') + break; + + code++; + } + +finish: + if (is_inside_word_def(ctx)) { + lwan_status_error("Word definition not finished"); + return false; + } + + EMIT(.callback = op_halt); + + if (!inline_calls(ctx)) + return false; + + if (peephole(ctx)) + peephole(ctx); + +#if defined(DUMP_CODE) + dump_code(&ctx->main->code); +#endif + + if (!check_stack_effects(ctx, ctx->main)) + return false; + + return true; +} + +/* FIXME: mark a builtin as "constant" for constprop? */ +#define BUILTIN_DETAIL(name_, id_, struct_id_, d_pushes_, d_pops_, r_pushes_, \ + r_pops_) \ + static void id_(union forth_inst *inst, double *d_stack, double *r_stack, \ + struct forth_vars *vars); \ + static const struct forth_builtin __attribute__((used)) \ + __attribute__((section(LWAN_SECTION_NAME(forth_builtin)))) \ + __attribute__((aligned(8))) struct_id_ = { \ + .name = name_, \ + .callback = id_, \ + .d_pushes = d_pushes_, \ + .d_pops = d_pops_, \ + .r_pushes = r_pushes_, \ + .r_pops = r_pops_, \ + }; \ + static void id_(union forth_inst *inst, double *d_stack, double *r_stack, \ + struct forth_vars *vars) + +#define BUILTIN_COMPILER_DETAIL(name_, id_, struct_id_) \ + static const char *id_(struct forth_ctx *, const char *); \ + static const struct forth_builtin __attribute__((used)) \ + __attribute__((section(LWAN_SECTION_NAME(forth_compiler_builtin)))) \ + __attribute__((aligned(8))) struct_id_ = { \ + .name = name_, \ + .callback_compiler = id_, \ + }; \ + static const char *id_(struct forth_ctx *ctx __attribute__((unused)), \ + const char *code) + +#define BUILTIN(name_, d_pushes_, d_pops_) \ + BUILTIN_DETAIL(name_, LWAN_TMP_ID, LWAN_TMP_ID, d_pushes_, d_pops_, 0, 0) +#define BUILTIN_R(name_, d_pushes_, d_pops_, r_pushes_, r_pops_) \ + BUILTIN_DETAIL(name_, LWAN_TMP_ID, LWAN_TMP_ID, d_pushes_, d_pops_, \ + r_pushes_, r_pops_) + +#define BUILTIN_COMPILER(name_) \ + BUILTIN_COMPILER_DETAIL(name_, LWAN_TMP_ID, LWAN_TMP_ID) + +BUILTIN_COMPILER("\\") +{ + code = strchr(code, '\n'); + return code ? code + 1 : NULL; +} + +BUILTIN_COMPILER("(") +{ + code = strchr(code, ')'); + return code ? code + 1 : NULL; +} + +BUILTIN_COMPILER(":") +{ + if (UNLIKELY(is_inside_word_def(ctx))) { + lwan_status_error("Already defining word"); + return NULL; + } + + ctx->defining_word = NULL; + return code; +} + +BUILTIN_COMPILER(";") +{ + if (ctx->j != ctx->j_stack) { + lwan_status_error("Unmatched if/then/else"); + return NULL; + } + + if (UNLIKELY(!is_inside_word_def(ctx))) { + lwan_status_error("Ending word without defining one"); + return NULL; + } + + ctx->defining_word = ctx->main; + return code; +} + +BUILTIN_COMPILER("if") +{ + if ((size_t)(ctx->j - ctx->j_stack) >= N_ELEMENTS(ctx->j_stack)) { + lwan_status_error("Too many nested 'if' words"); + return NULL; + } + + EMIT(.callback = op_jump_if); + *ctx->j++ = EMIT(.pc = 0); + return code; +} + +static const char * +builtin_else_then(struct forth_ctx *ctx, const char *code, bool is_then) +{ + if (ctx->j == ctx->j_stack) { + lwan_status_error("'%s' before 'if'", is_then ? "then" : "else"); + return NULL; + } + + union forth_inst *prev_pc_imm = + forth_code_get_elem(&ctx->defining_word->code, *--ctx->j); + + if (is_then) { + EMIT(.callback = op_nop); + } else { + EMIT(.callback = op_jump); + + if ((size_t)(ctx->j - ctx->j_stack) >= N_ELEMENTS(ctx->j_stack)) { + lwan_status_error("Else is too deep"); + return NULL; + } + + *ctx->j++ = EMIT(.pc = 0); + } + + prev_pc_imm->pc = forth_code_len(&ctx->defining_word->code); + + return code; +} + +BUILTIN_COMPILER("else") { return builtin_else_then(ctx, code, false); } +BUILTIN_COMPILER("then") { return builtin_else_then(ctx, code, true); } + +#define PUSH_D(value_) ({ *d_stack = (value_); d_stack++; }) +#define PUSH_R(value_) ({ *r_stack = (value_); r_stack++; }) +#define DROP_D() ({ d_stack--; }) +#define DROP_R() ({ r_stack--; }) +#define POP_D() ({ DROP_D(); *d_stack; }) +#define POP_R() ({ DROP_R(); *r_stack; }) + +#define NEXT() TAIL_CALL inst[1].callback(&inst[1], d_stack, r_stack, vars) + +BUILTIN("x", 1, 0) +{ + PUSH_D(vars->x); + NEXT(); +} +BUILTIN("y", 1, 0) +{ + PUSH_D(vars->y); + NEXT(); +} +BUILTIN("t", 1, 0) +{ + PUSH_D(vars->t); + NEXT(); +} +BUILTIN("dt", 1, 0) +{ + PUSH_D(vars->dt); + NEXT(); +} + +BUILTIN("mx", 1, 0) +{ + /* stub */ + PUSH_D(0.0); + NEXT(); +} + +BUILTIN("my", 1, 0) +{ + /* stub */ + PUSH_D(0.0); + NEXT(); +} + +BUILTIN("button", 1, 1) +{ + /* stub */ + DROP_D(); + PUSH_D(0.0); + NEXT(); +} + +BUILTIN("buttons", 1, 0) +{ + /* stub */ + PUSH_D(0.0); + NEXT(); +} + +BUILTIN("audio", 0, 1) +{ + /* stub */ + DROP_D(); + NEXT(); +} + +BUILTIN("sample", 3, 2) +{ + /* stub */ + DROP_D(); + DROP_D(); + PUSH_D(0); + PUSH_D(0); + PUSH_D(0); + NEXT(); +} + +BUILTIN("bwsample", 1, 2) +{ + /* stub */ + DROP_D(); + DROP_D(); + PUSH_D(0); + NEXT(); +} + +BUILTIN_R("push", 0, 1, 1, 0) +{ + PUSH_R(POP_D()); + NEXT(); +} + +BUILTIN_R("pop", 1, 0, 0, 1) +{ + PUSH_D(POP_R()); + NEXT(); +} + +BUILTIN_R(">r", 0, 1, 1, 0) +{ + PUSH_R(POP_D()); + NEXT(); +} + +BUILTIN_R("r>", 1, 0, 0, 1) +{ + PUSH_D(POP_R()); + NEXT(); +} + +BUILTIN_R("r@", 1, 0, 1, 1) +{ + double v = POP_R(); + PUSH_R(v); + PUSH_D(v); + NEXT(); +} + +BUILTIN("@", 1, 1) +{ + uint32_t slot = (uint32_t)POP_D(); + PUSH_D(vars->memory[slot % (uint32_t)N_ELEMENTS(vars->memory)]); + NEXT(); +} + +BUILTIN("!", 0, 2) +{ + double v = POP_D(); + uint32_t slot = (uint32_t)POP_D(); + vars->memory[slot % (uint32_t)N_ELEMENTS(vars->memory)] = v; + NEXT(); +} + +BUILTIN("dup", 2, 1) +{ + double v = POP_D(); + PUSH_D(v); + PUSH_D(v); + NEXT(); +} + +BUILTIN(" dupdup", 4, 1) +{ + double v = POP_D(); + PUSH_D(v); + PUSH_D(v); + PUSH_D(v); + PUSH_D(v); + NEXT(); +} + +BUILTIN("over", 3, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v2); + PUSH_D(v1); + PUSH_D(v2); + NEXT(); +} + +BUILTIN("2dup", 4, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v2); + PUSH_D(v1); + PUSH_D(v2); + PUSH_D(v1); + NEXT(); +} + +BUILTIN("z+", 2, 4) +{ + double v1 = POP_D(); + double v2 = POP_D(); + double v3 = POP_D(); + double v4 = POP_D(); + PUSH_D(v2 + v4); + PUSH_D(v1 + v3); + NEXT(); +} + +BUILTIN("z*", 2, 4) +{ + double v1 = POP_D(); + double v2 = POP_D(); + double v3 = POP_D(); + double v4 = POP_D(); + PUSH_D(v4 * v2 - v3 * v1); + PUSH_D(v4 * v1 + v3 * v2); + NEXT(); +} + +BUILTIN("drop", 0, 1) +{ + DROP_D(); + NEXT(); +} + +BUILTIN("swap", 2, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v1); + PUSH_D(v2); + NEXT(); +} + +BUILTIN("rot", 3, 3) +{ + double v1 = POP_D(); + double v2 = POP_D(); + double v3 = POP_D(); + PUSH_D(v2); + PUSH_D(v1); + PUSH_D(v3); + NEXT(); +} + +BUILTIN("-rot", 3, 3) +{ + double v1 = POP_D(); + double v2 = POP_D(); + double v3 = POP_D(); + PUSH_D(v1); + PUSH_D(v3); + PUSH_D(v2); + NEXT(); +} + +BUILTIN(" -rotswap", 3, 3) +{ + double v1 = POP_D(); + double v2 = POP_D(); + double v3 = POP_D(); + PUSH_D(v1); + PUSH_D(v2); + PUSH_D(v3); + NEXT(); +} + +BUILTIN("=", 1, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v1 == v2 ? 1.0 : 0.0); + NEXT(); +} + +BUILTIN("<>", 1, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v1 != v2 ? 1.0 : 0.0); + NEXT(); +} + +BUILTIN(">", 1, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v1 > v2 ? 1.0 : 0.0); + NEXT(); +} + +BUILTIN("<", 1, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v1 < v2 ? 1.0 : 0.0); + NEXT(); +} + +BUILTIN(">=", 1, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v1 >= v2 ? 1.0 : 0.0); + NEXT(); +} + +BUILTIN(" >=swap", 2, 3) +{ + double v1 = POP_D(); + double v2 = POP_D(); + double v3 = POP_D(); + + PUSH_D(v1 >= v2 ? 1.0 : 0.0); + PUSH_D(v3); + NEXT(); +} + +BUILTIN("<=", 1, 2) +{ + double v1 = POP_D(); + double v2 = POP_D(); + PUSH_D(v1 <= v2 ? 1.0 : 0.0); + NEXT(); +} + +BUILTIN("+", 1, 2) +{ + PUSH_D(POP_D() + POP_D()); + NEXT(); +} + +BUILTIN(" fma", 1, 3) +{ + double m1 = POP_D(); + double m2 = POP_D(); + double a = POP_D(); + PUSH_D(fma(m1, m2, a)); + NEXT(); +} + +BUILTIN("*", 1, 2) +{ + PUSH_D(POP_D() * POP_D()); + NEXT(); +} + +BUILTIN("-", 1, 2) +{ + double v = POP_D(); + PUSH_D(POP_D() - v); + NEXT(); +} + +BUILTIN("/", 1, 2) +{ + double v = POP_D(); + if (UNLIKELY(v == 0.0)) { + DROP_D(); + PUSH_D(__builtin_inf()); + } else { + PUSH_D(POP_D() / v); + } + NEXT(); +} + +BUILTIN("mod", 1, 2) +{ + double v = POP_D(); + PUSH_D(fmod(POP_D(), v)); + NEXT(); +} + +BUILTIN("pow", 1, 2) +{ + double v = POP_D(); + PUSH_D(pow(fabs(POP_D()), v)); + NEXT(); +} + +BUILTIN("**", 1, 2) +{ + double v = POP_D(); + PUSH_D(pow(fabs(POP_D()), v)); + NEXT(); +} + +BUILTIN("atan2", 1, 2) +{ + double v = POP_D(); + PUSH_D(atan2(POP_D(), v)); + NEXT(); +} + +BUILTIN("and", 1, 2) +{ + double v = POP_D(); + PUSH_D((POP_D() != 0.0 && v != 0.0) ? 1.0 : 0.0); + NEXT(); +} + +BUILTIN("or", 1, 2) +{ + double v = POP_D(); + PUSH_D((POP_D() != 0.0 || v != 0.0) ? 1.0 : 0.0); + NEXT(); +} + +BUILTIN("not", 1, 1) +{ + PUSH_D(POP_D() != 0.0 ? 0.0 : 1.0); + NEXT(); +} + +BUILTIN("min", 1, 2) +{ + PUSH_D(fmin(POP_D(), POP_D())); + NEXT(); +} + +BUILTIN("max", 1, 2) +{ + PUSH_D(fmax(POP_D(), POP_D())); + NEXT(); +} + +BUILTIN("negate", 1, 1) +{ + PUSH_D(-POP_D()); + NEXT(); +} + +BUILTIN("sin", 1, 1) +{ + PUSH_D(sin(POP_D())); + NEXT(); +} + +BUILTIN("cos", 1, 1) +{ + PUSH_D(cos(POP_D())); + NEXT(); +} + +BUILTIN("tan", 1, 1) +{ + PUSH_D(tan(POP_D())); + NEXT(); +} + +BUILTIN("log", 1, 1) +{ + PUSH_D(log(fabs(POP_D()))); + NEXT(); +} + +BUILTIN("exp", 1, 1) +{ + PUSH_D(exp(POP_D())); + NEXT(); +} + +BUILTIN("sqrt", 1, 1) +{ + PUSH_D(sqrt(fabs(POP_D()))); + NEXT(); +} + +BUILTIN("floor", 1, 1) +{ + PUSH_D(floor(POP_D())); + NEXT(); +} + +BUILTIN("ceil", 1, 1) +{ + PUSH_D(ceil(POP_D())); + NEXT(); +} + +BUILTIN("abs", 1, 1) +{ + PUSH_D(fabs(POP_D())); + NEXT(); +} + +BUILTIN("pi", 1, 0) +{ + PUSH_D(M_PI); + NEXT(); +} + +BUILTIN("random", 1, 0) +{ + PUSH_D(drand48()); + NEXT(); +} + +BUILTIN(" mult2", 1, 1) +{ + *(d_stack - 1) *= 2.0; + NEXT(); +} + +BUILTIN(" pow2", 1, 1) +{ + *(d_stack - 1) *= *(d_stack - 1); + NEXT(); +} + +BUILTIN(" div2", 1, 1) +{ + *(d_stack - 1) /= 2.0; + NEXT(); +} + +BUILTIN(" multpi", 1, 1) +{ + *(d_stack - 1) *= M_PI; + NEXT(); +} + +BUILTIN(" multhalfpi", 1, 1) +{ + *(d_stack - 1) *= M_PI / 2.0; + NEXT(); +} + +#undef NEXT + +__attribute__((no_sanitize_address)) static void +register_builtins(struct forth_ctx *ctx) +{ + const struct forth_builtin *iter; + + LWAN_SECTION_FOREACH(forth_builtin, iter) { + if (!new_word(ctx, NULL, iter)) { + lwan_status_critical("could not register forth word: %s", + iter->name); + } + } + LWAN_SECTION_FOREACH(forth_compiler_builtin, iter) { + if (!new_word(ctx, NULL, iter)) { + lwan_status_critical("could not register forth word: %s", + iter->name); + } + } +} + +static void word_free(void *ptr) +{ + struct forth_word *word = ptr; + + if (!is_word_builtin(word)) + forth_code_reset(&word->code); + free(word); +} + +struct forth_ctx *forth_new(void) +{ + struct forth_ctx *ctx = malloc(sizeof(*ctx)); + + if (!ctx) + return NULL; + + ctx->words = hash_str_new(NULL, word_free); + if (!ctx->words) { + free(ctx); + return NULL; + } + + struct forth_word *word = new_word(ctx, " ", NULL); + if (!word) { + hash_unref(ctx->words); + free(ctx); + return NULL; + } + + ctx->main = word; + ctx->defining_word = word; + + register_builtins(ctx); + + return ctx; +} + +void forth_free(struct forth_ctx *ctx) +{ + if (!ctx) + return; + + hash_unref(ctx->words); + free(ctx); +} + +size_t forth_d_stack_len(const struct forth_ctx *ctx, + const struct forth_vars *vars) +{ + return (size_t)(vars->final_d_stack_ptr - ctx->d_stack); +} + +double forth_d_stack_pop(struct forth_vars *vars) +{ + vars->final_d_stack_ptr--; + double v = *vars->final_d_stack_ptr; + return v; +} + +#if defined(FUZZ_TEST) +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct forth_ctx *ctx = forth_new(); + if (!ctx) + return 1; + + char *input = strndup((const char *)data, size); + if (!input) { + forth_free(ctx); + return 1; + } + + if (!forth_parse_string(ctx, input)) { + forth_free(ctx); + free(input); + return 1; + } + + free(input); + + struct forth_vars vars = {.x = 1, .y = 0}; + forth_run(ctx, &vars); + + forth_free(ctx); + + return 0; +} +#elif defined(MAIN) +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + + struct forth_ctx *ctx = forth_new(); + if (!ctx) + return 1; + + if (!forth_parse_string(ctx, + ": nice 60 5 4 + + ; : juanita 400 10 5 5 + + + ; " + "x if nice else juanita then 2 * 4 / 2 *")) { + lwan_status_critical("could not parse forth program"); + __builtin_unreachable(); + } + + printf("running with x=0\n"); + struct forth_vars vars = {.x = 0, .y = 0}; + if (forth_run(ctx, &vars)) { + printf("D stack: %zu elems", forth_d_stack_len(ctx, &vars)); + for (size_t len = forth_d_stack_len(ctx, &vars); len; len--) { + printf(" %lf", forth_d_stack_pop(&vars)); + } + } + + printf("\nrunning with x=1\n"); + vars = (struct forth_vars){.x = 1, .y = 0}; + if (forth_run(ctx, &vars)) { + printf("D stack: %zu elems", forth_d_stack_len(ctx, &vars)); + for (size_t len = forth_d_stack_len(ctx, &vars); len; len--) { + printf(" %lf", forth_d_stack_pop(&vars)); + } + } + + forth_free(ctx); + + return 0; +} +#endif diff --git a/src/samples/forthsalon/forth.h b/src/samples/forthsalon/forth.h new file mode 100644 index 000000000..f942b7bb7 --- /dev/null +++ b/src/samples/forthsalon/forth.h @@ -0,0 +1,41 @@ +/* + * lwan - web server + * Copyright (c) 2025 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#pragma once + +struct forth_ctx; + +struct forth_vars { + double x, y; + double t, dt; + + double memory[16]; + + double *final_r_stack_ptr; + double *final_d_stack_ptr; +}; + +bool forth_run(struct forth_ctx *ctx, struct forth_vars *vars); +bool forth_parse_string(struct forth_ctx *ctx, const char *code); +void forth_free(struct forth_ctx *ctx); +struct forth_ctx *forth_new(void); +size_t forth_d_stack_len(const struct forth_ctx *ctx, + const struct forth_vars *vars); +double forth_d_stack_pop(struct forth_vars *vars); diff --git a/src/samples/forthsalon/gif.h b/src/samples/forthsalon/gif.h new file mode 100644 index 000000000..2c52cfb1b --- /dev/null +++ b/src/samples/forthsalon/gif.h @@ -0,0 +1,863 @@ +// +// gif.h +// by Charlie Tangora +// Public domain. +// Email me : ctangora -at- gmail -dot- com +// +// This file offers a simple, very limited way to create animated GIFs directly in code. +// +// Those looking for particular cleverness are likely to be disappointed; it's pretty +// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg +// dithering. (It does at least use delta encoding - only the changed portions of each +// frame are saved.) +// +// So resulting files are often quite large. The hope is that it will be handy nonetheless +// as a quick and easily-integrated way for programs to spit out animations. +// +// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) +// +// If capturing a buffer with a bottom-left origin (such as OpenGL), define GIF_FLIP_VERT +// to automatically flip the buffer data when writing the image (the buffer itself is +// unchanged. +// +// USAGE: +// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. +// Pass subsequent frames to GifWriteFrame(). +// Finally, call GifEnd() to close the file handle and free memory. +// + +#ifndef gif_h +#define gif_h + +/* FIXME(lpereira): Someday I need to go through these warnings and fix them up. */ +#pragma GCC diagnostic ignored "-Wconversion" +#pragma GCC diagnostic ignored "-Wpointer-sign" +#pragma GCC diagnostic ignored "-Warith-conversion" + +#include // for memcpy and bzero +#include // for integer typedefs +#include // for bool macros + +// Define these macros to hook into a custom memory allocator. +// TEMP_MALLOC and TEMP_FREE will only be called in stack fashion - frees in the reverse order of mallocs +// and any temp memory allocated by a function will be freed before it exits. +// MALLOC and FREE are used only by GifBegin and GifEnd respectively (to allocate a buffer the size of the image, which +// is used to find changed pixels for delta-encoding.) + +#ifndef GIF_TEMP_MALLOC +#include +#define GIF_TEMP_MALLOC malloc +#endif + +#ifndef GIF_TEMP_FREE +#include +#define GIF_TEMP_FREE free +#endif + +#ifndef GIF_MALLOC +#include +#define GIF_MALLOC malloc +#endif + +#ifndef GIF_FREE +#include +#define GIF_FREE free +#endif + +const int kGifTransIndex = 0; + +typedef struct +{ + int bitDepth; + + uint8_t r[256]; + uint8_t g[256]; + uint8_t b[256]; + + // k-d tree over RGB space, organized in heap fashion + // i.e. left child of node i is node i*2, right child is node i*2+1 + // nodes 256-511 are implicitly the leaves, containing a color + uint8_t treeSplitElt[256]; + uint8_t treeSplit[256]; +} GifPalette; + +// max, min, and abs functions +int GifIMax(int l, int r) { return l>r?l:r; } +int GifIMin(int l, int r) { return l (1<bitDepth)-1) + { + int ind = treeRoot-(1<bitDepth); + if(ind == kGifTransIndex) return; + + // check whether this color is better than the current winner + int r_err = r - ((int32_t)pPal->r[ind]); + int g_err = g - ((int32_t)pPal->g[ind]); + int b_err = b - ((int32_t)pPal->b[ind]); + int diff = GifIAbs(r_err)+GifIAbs(g_err)+GifIAbs(b_err); + + if(diff < *bestDiff) + { + *bestInd = ind; + *bestDiff = diff; + } + + return; + } + + // take the appropriate color (r, g, or b) for this node of the k-d tree + int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; + int splitComp = comps[pPal->treeSplitElt[treeRoot]]; + + int splitPos = pPal->treeSplit[treeRoot]; + if(splitPos > splitComp) + { + // check the left subtree + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2); + if( *bestDiff > splitPos - splitComp ) + { + // cannot prove there's not a better value in the right subtree, check that too + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2+1); + } + } + else + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2+1); + if( *bestDiff > splitComp - splitPos ) + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2); + } + } +} + +void GifSwapPixels(uint8_t* image, int pixA, int pixB) +{ + uint8_t rA = image[pixA*4]; + uint8_t gA = image[pixA*4+1]; + uint8_t bA = image[pixA*4+2]; + uint8_t aA = image[pixA*4+3]; + + uint8_t rB = image[pixB*4]; + uint8_t gB = image[pixB*4+1]; + uint8_t bB = image[pixB*4+2]; + uint8_t aB = image[pixA*4+3]; + + image[pixA*4] = rB; + image[pixA*4+1] = gB; + image[pixA*4+2] = bB; + image[pixA*4+3] = aB; + + image[pixB*4] = rA; + image[pixB*4+1] = gA; + image[pixB*4+2] = bA; + image[pixB*4+3] = aA; +} + +// just the partition operation from quicksort +int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotValue) +{ + int storeIndex = left; + bool split = 0; + for(int ii=left; ii neededCenter) + GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); + + if(pivotIndex < neededCenter) + GifPartitionByMedian(image, pivotIndex+1, right, com, neededCenter); + } +} + +// Just partition around a given pivot, returning the split point +int GifPartitionByMean(uint8_t* image, int left, int right, int com, int neededMean) +{ + if(left < right-1) + { + return GifPartition(image, left, right-1, com, neededMean); + } + return left; +} + +// Builds a palette by creating a balanced k-d tree of all pixels in the image +void GifSplitPalette(uint8_t* image, int numPixels, int treeNode, int treeLevel, bool buildForDither, GifPalette* pal) +{ + if(numPixels == 0) + return; + + int numColors = (1 << pal->bitDepth); + + // base case, bottom of the tree + if(treeNode >= numColors) + { + int entry = treeNode - numColors; + + if(buildForDither) + { + // Dithering needs at least one color as dark as anything + // in the image and at least one brightest color - + // otherwise it builds up error and produces strange artifacts + if( entry == 1 ) + { + // special case: the darkest color in the image + uint32_t r=255, g=255, b=255; + for(int ii=0; iir[entry] = (uint8_t)r; + pal->g[entry] = (uint8_t)g; + pal->b[entry] = (uint8_t)b; + + return; + } + + if( entry == numColors-1 ) + { + // special case: the lightest color in the image + uint32_t r=0, g=0, b=0; + for(int ii=0; iir[entry] = (uint8_t)r; + pal->g[entry] = (uint8_t)g; + pal->b[entry] = (uint8_t)b; + + return; + } + } + + // otherwise, take the average of all colors in this subcube + uint64_t r=0, g=0, b=0; + for(int ii=0; iir[entry] = (uint8_t)r; + pal->g[entry] = (uint8_t)g; + pal->b[entry] = (uint8_t)b; + + return; + } + + // Find the axis with the largest range + int minR = 255, maxR = 0; + int minG = 255, maxG = 0; + int minB = 255, maxB = 0; + for(int ii=0; ii maxR) maxR = r; + if(r < minR) minR = r; + + if(g > maxG) maxG = g; + if(g < minG) minG = g; + + if(b > maxB) maxB = b; + if(b < minB) minB = b; + } + + int rRange = maxR - minR; + int gRange = maxG - minG; + int bRange = maxB - minB; + + // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) + int splitCom = 1; int rangeMin = minG; int rangeMax = maxG; + if(bRange > gRange) { splitCom = 2; rangeMin = minB; rangeMax = maxB; } + if(rRange > bRange && rRange > gRange) { splitCom = 0; rangeMin = minR; rangeMax = maxR; } + + int subPixelsA = numPixels / 2; + + GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); + int splitValue = image[subPixelsA*4+splitCom]; + + // if the split is very unbalanced, split at the mean instead of the median to preserve rare colors + int splitUnbalance = GifIAbs( (splitValue - rangeMin) - (rangeMax - splitValue) ); + if( splitUnbalance > (1536 >> treeLevel) ) + { + splitValue = rangeMin + (rangeMax-rangeMin) / 2; + subPixelsA = GifPartitionByMean(image, 0, numPixels, splitCom, splitValue); + } + + // add the bottom node for the transparency index + if( treeNode == numColors/2 ) + { + subPixelsA = 0; + splitValue = 0; + } + + int subPixelsB = numPixels-subPixelsA; + pal->treeSplitElt[treeNode] = (uint8_t)splitCom; + pal->treeSplit[treeNode] = (uint8_t)splitValue; + + GifSplitPalette(image, subPixelsA, treeNode*2, treeLevel+1, buildForDither, pal); + GifSplitPalette(image+subPixelsA*4, subPixelsB, treeNode*2+1, treeLevel+1, buildForDither, pal); +} + +// Finds all pixels that have changed from the previous image and +// moves them to the fromt of th buffer. +// This allows us to build a palette optimized for the colors of the +// changed pixels only. +int GifPickChangedPixels( const uint8_t* lastFrame, uint8_t* frame, int numPixels ) +{ + int numChanged = 0; + uint8_t* writeIter = frame; + + for (int ii=0; iibitDepth = bitDepth; + + // SplitPalette is destructive (it sorts the pixels by color) so + // we must create a copy of the image for it to destroy + size_t imageSize = (size_t)(width * height * 4 * sizeof(uint8_t)); + uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); + memcpy(destroyableImage, nextFrame, imageSize); + + int numPixels = (int)(width * height); + if(lastFrame) + numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); + + GifSplitPalette(destroyableImage, numPixels, 1, 0, buildForDither, pPal); + + GIF_TEMP_FREE(destroyableImage); + + // add the bottom node for the transparency index + pPal->treeSplit[1 << (bitDepth-1)] = 0; + pPal->treeSplitElt[1 << (bitDepth-1)] = 0; + + pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; +} + +// Implements Floyd-Steinberg dithering, writes palette value to alpha +void GifDitherImage( const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal ) +{ + int numPixels = (int)(width * height); + + // quantPixels initially holds color*256 for all pixels + // The extra 8 bits of precision allow for sub-single-color error values + // to be propagated + int32_t *quantPixels = (int32_t *)GIF_TEMP_MALLOC(sizeof(int32_t) * (size_t)numPixels * 4); + + for( int ii=0; iir[bestInd]) * 256; + int32_t g_err = nextPix[1] - (int32_t)(pPal->g[bestInd]) * 256; + int32_t b_err = nextPix[2] - (int32_t)(pPal->b[bestInd]) * 256; + + nextPix[0] = pPal->r[bestInd]; + nextPix[1] = pPal->g[bestInd]; + nextPix[2] = pPal->b[bestInd]; + nextPix[3] = bestInd; + + // Propagate the error to the four adjacent locations + // that we haven't touched yet + int quantloc_7 = (int)(yy * width + xx + 1); + int quantloc_3 = (int)(yy * width + width + xx - 1); + int quantloc_5 = (int)(yy * width + width + xx); + int quantloc_1 = (int)(yy * width + width + xx + 1); + + if(quantloc_7 < numPixels) + { + int32_t* pix7 = quantPixels+4*quantloc_7; + pix7[0] += GifIMax( -pix7[0], r_err * 7 / 16 ); + pix7[1] += GifIMax( -pix7[1], g_err * 7 / 16 ); + pix7[2] += GifIMax( -pix7[2], b_err * 7 / 16 ); + } + + if(quantloc_3 < numPixels) + { + int32_t* pix3 = quantPixels+4*quantloc_3; + pix3[0] += GifIMax( -pix3[0], r_err * 3 / 16 ); + pix3[1] += GifIMax( -pix3[1], g_err * 3 / 16 ); + pix3[2] += GifIMax( -pix3[2], b_err * 3 / 16 ); + } + + if(quantloc_5 < numPixels) + { + int32_t* pix5 = quantPixels+4*quantloc_5; + pix5[0] += GifIMax( -pix5[0], r_err * 5 / 16 ); + pix5[1] += GifIMax( -pix5[1], g_err * 5 / 16 ); + pix5[2] += GifIMax( -pix5[2], b_err * 5 / 16 ); + } + + if(quantloc_1 < numPixels) + { + int32_t* pix1 = quantPixels+4*quantloc_1; + pix1[0] += GifIMax( -pix1[0], r_err / 16 ); + pix1[1] += GifIMax( -pix1[1], g_err / 16 ); + pix1[2] += GifIMax( -pix1[2], b_err / 16 ); + } + } + } + + // Copy the palettized result to the output buffer + for( int ii=0; iir[bestInd]; + outFrame[1] = pPal->g[bestInd]; + outFrame[2] = pPal->b[bestInd]; + outFrame[3] = (uint8_t)bestInd; + } + + if(lastFrame) lastFrame += 4; + outFrame += 4; + nextFrame += 4; + } +} + +// Simple structure to write out the LZW-compressed portion of the image +// one bit at a time +typedef struct +{ + uint32_t chunkIndex; + uint8_t chunk[256]; // bytes are written in here until we have 256 of them, then written to the file + + uint8_t bitIndex; // how many bits in the partial byte written so far + uint8_t byte; // current partial byte + + uint8_t padding[2]; // make padding explicit +} GifBitStatus; + +// insert a single bit +void GifWriteBit( GifBitStatus* stat, uint32_t bit ) +{ + bit = bit & 1; + bit = bit << stat->bitIndex; + stat->byte |= bit; + + ++stat->bitIndex; + if( stat->bitIndex > 7 ) + { + // move the newly-finished byte to the chunk buffer + stat->chunk[stat->chunkIndex++] = stat->byte; + // and start a new byte + stat->bitIndex = 0; + stat->byte = 0; + } +} + +// write all bytes so far to the file +void GifWriteChunk( struct lwan_strbuf* f, GifBitStatus* stat ) +{ + lwan_strbuf_append_char(f, (int)stat->chunkIndex); + lwan_strbuf_append_str(f, stat->chunk, stat->chunkIndex); + + stat->bitIndex = 0; + stat->byte = 0; + stat->chunkIndex = 0; +} + +void GifWriteCode( struct lwan_strbuf* f, GifBitStatus* stat, uint32_t code, uint32_t length ) +{ + for( uint32_t ii=0; ii> 1; + + if( stat->chunkIndex == 255 ) + { + GifWriteChunk(f, stat); + } + } +} + +// The LZW dictionary is a 256-ary tree constructed as the file is encoded, +// this is one node +typedef struct +{ + uint16_t m_next[256]; +} GifLzwNode; + +// write a 256-color (8-bit) image palette to the file +void GifWritePalette( const GifPalette* pPal, struct lwan_strbuf* f ) +{ + lwan_strbuf_append_char(f, 0); // first color: transparency + lwan_strbuf_append_char(f, 0); + lwan_strbuf_append_char(f, 0); + + for(int ii=1; ii<(1 << pPal->bitDepth); ++ii) + { + uint32_t r = pPal->r[ii]; + uint32_t g = pPal->g[ii]; + uint32_t b = pPal->b[ii]; + + lwan_strbuf_append_char(f, r); + lwan_strbuf_append_char(f, g); + lwan_strbuf_append_char(f, b); + } +} + +// write the image header, LZW-compress and write out the image +void GifWriteLzwImage(struct lwan_strbuf* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) +{ + // graphics control extension + lwan_strbuf_append_char(f, 0x21); + lwan_strbuf_append_char(f, 0xf9); + lwan_strbuf_append_char(f, 0x04); + lwan_strbuf_append_char(f, 0x05); // leave prev frame in place, this frame has transparency + lwan_strbuf_append_char(f, (delay & 0xff)); + lwan_strbuf_append_char(f, ((delay>>8) & 0xff)); + lwan_strbuf_append_char(f, kGifTransIndex); // transparent color index + lwan_strbuf_append_char(f, 0); + + lwan_strbuf_append_char(f, 0x2c); // image descriptor block + + lwan_strbuf_append_char(f, left & 0xff); // corner of image in canvas space + lwan_strbuf_append_char(f, (left >> 8) & 0xff); + lwan_strbuf_append_char(f, top & 0xff); + lwan_strbuf_append_char(f, (top >> 8) & 0xff); + + lwan_strbuf_append_char(f, width & 0xff); // width and height of image + lwan_strbuf_append_char(f, (width >> 8) & 0xff); + lwan_strbuf_append_char(f, height & 0xff); + lwan_strbuf_append_char(f, (height >> 8) & 0xff); + + //lwan_strbuf_append_char(f, 0); // no local color table, no transparency + //lwan_strbuf_append_char(f, 0x80); // no local color table, but transparency + + lwan_strbuf_append_char(f, 0x80 + pPal->bitDepth-1); // local color table present, 2 ^ bitDepth entries + GifWritePalette(pPal, f); + + const int minCodeSize = pPal->bitDepth; + const uint32_t clearCode = 1 << pPal->bitDepth; + + lwan_strbuf_append_char(f, minCodeSize); // min code size 8 bits + + GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode)*4096); + + memset(codetree, 0, sizeof(GifLzwNode)*4096); + int32_t curCode = -1; + uint32_t codeSize = (uint32_t)minCodeSize + 1; + uint32_t maxCode = clearCode+1; + + GifBitStatus stat; + stat.byte = 0; + stat.bitIndex = 0; + stat.chunkIndex = 0; + + GifWriteCode(f, &stat, clearCode, codeSize); // start with a fresh LZW dictionary + + for(uint32_t yy=0; yy= (1ul << codeSize) ) + { + // dictionary entry count has broken a size barrier, + // we need more bits for codes + codeSize++; + } + if( maxCode == 4095 ) + { + // the dictionary is full, clear it out and begin anew + GifWriteCode(f, &stat, clearCode, codeSize); // clear tree + + memset(codetree, 0, sizeof(GifLzwNode)*4096); + codeSize = (uint32_t)(minCodeSize + 1); + maxCode = clearCode+1; + } + + curCode = nextValue; + } + } + } + + // compression footer + GifWriteCode(f, &stat, (uint32_t)curCode, codeSize); + GifWriteCode(f, &stat, clearCode, codeSize); + GifWriteCode(f, &stat, clearCode + 1, (uint32_t)minCodeSize + 1); + + // write out the last partial chunk + while( stat.bitIndex ) GifWriteBit(&stat, 0); + if( stat.chunkIndex ) GifWriteChunk(f, &stat); + + lwan_strbuf_append_char(f, 0); // image block terminator + + GIF_TEMP_FREE(codetree); +} + +typedef struct +{ + struct lwan_strbuf* f; + uint8_t* oldImage; + bool firstFrame; + + uint8_t padding[7]; // make padding explicit +} GifWriter; + +// Creates a gif file. +// The input GIFWriter is assumed to be uninitialized. +// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. +bool GifBegin( GifWriter* writer, struct lwan_strbuf *f, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither ) +{ + (void)bitDepth; (void)dither; // Mute "Unused argument" warnings + + if(!f) return false; + + writer->f = f; + writer->firstFrame = true; + + // allocate + writer->oldImage = (uint8_t*)GIF_MALLOC(width*height*4); + + lwan_strbuf_append_strz(writer->f, "GIF89a"); + + // screen descriptor + lwan_strbuf_append_char(writer->f, width & 0xff); + lwan_strbuf_append_char(writer->f, (width >> 8) & 0xff); + lwan_strbuf_append_char(writer->f, height & 0xff); + lwan_strbuf_append_char(writer->f, (height >> 8) & 0xff); + + lwan_strbuf_append_char(writer->f, 0xf0); // there is an unsorted global color table of 2 entries + lwan_strbuf_append_char(writer->f, 0); // background color + lwan_strbuf_append_char(writer->f, 0); // pixels are square (we need to specify this because it's 1989) + + // now the "global" palette (really just a dummy palette) + // color 0: black + lwan_strbuf_append_char(writer->f, 0); + lwan_strbuf_append_char(writer->f, 0); + lwan_strbuf_append_char(writer->f, 0); + // color 1: also black + lwan_strbuf_append_char(writer->f, 0); + lwan_strbuf_append_char(writer->f, 0); + lwan_strbuf_append_char(writer->f, 0); + + if( delay != 0 ) + { + // animation header + lwan_strbuf_append_char(writer->f, 0x21); // extension + lwan_strbuf_append_char(writer->f, 0xff); // application specific + lwan_strbuf_append_char(writer->f, 11); // length 11 + lwan_strbuf_append_strz(writer->f, "NETSCAPE2.0"); // yes, really + lwan_strbuf_append_char(writer->f, 3); // 3 bytes of NETSCAPE2.0 data + + lwan_strbuf_append_char(writer->f, 1); // this is the Netscape 2.0 sub-block ID and it must be 1, otherwise some viewers error + lwan_strbuf_append_char(writer->f, 0); // loop infinitely (byte 0) + lwan_strbuf_append_char(writer->f, 0); // loop infinitely (byte 1) + + lwan_strbuf_append_char(writer->f, 0); // block terminator + } + + return true; +} + +// Writes out a new frame to a GIF in progress. +// The GIFWriter should have been created by GIFBegin. +// AFAIK, it is legal to use different bit depths for different frames of an image - +// this may be handy to save bits in animations that don't change much. +bool GifWriteFrame( GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, int bitDepth, bool dither) +{ + if(!writer->f) return false; + + const uint8_t* oldImage = writer->firstFrame? NULL : writer->oldImage; + writer->firstFrame = false; + + GifPalette pal; + GifMakePalette((dither? NULL : oldImage), image, width, height, bitDepth, dither, &pal); + + if(dither) + GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); + else + GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); + + GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); + + return true; +} + +// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. +// Many if not most viewers will still display a GIF properly if the EOF code is missing, +// but it's still a good idea to write it out. +bool GifEnd( GifWriter* writer ) +{ + if(!writer->f) return false; + + lwan_strbuf_append_char(writer->f, 0x3b); // end of file + GIF_FREE(writer->oldImage); + + writer->f = NULL; + writer->oldImage = NULL; + + return true; +} + +#endif diff --git a/src/samples/forthsalon/main.c b/src/samples/forthsalon/main.c new file mode 100644 index 000000000..adeb78f6a --- /dev/null +++ b/src/samples/forthsalon/main.c @@ -0,0 +1,163 @@ +/* + * lwan - web server + * Copyright (c) 2025 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include + +#include "lwan.h" +#include "forth.h" +#include "gif.h" + +/* Twister by boomlinde + * https://forthsalon.appspot.com/haiku-view/ag5mb3J0aHNhbG9uLWhyZHISCxIFSGFpa3UYgICAvJXxgwsM + */ +static const char twister[] = ": t' t pi * 2 / ;\n" +": l * + sin ;\n" +": r t' 1 y t' + 4 l + 1.57 ;\n" +": x' x 4 * 2 - t' y 3 l + ;\n" +": v 2dup x' >= swap x' < * -rot swap - l ;\n" +": a r 4 l ; : b r 1 l ;\n" +": c r 2 l ; : d r 3 l ;\n" +"0 d a v a b v b c v c d v 0.1 0.2"; + +static void destroy_forth_ctx(void *p) { forth_free(p); } +static void destroy_gif_writer(void *p) +{ + GifEnd(p); + free(p); +} + +static struct timespec current_precise_monotonic_timespec(void) +{ + struct timespec now; + + if (UNLIKELY(clock_gettime(CLOCK_MONOTONIC, &now) < 0)) { + lwan_status_perror("clock_gettime"); + return (struct timespec){}; + } + + return now; +} + +static double elapsed_time_ms(const struct timespec then) +{ + const struct timespec now = current_precise_monotonic_timespec(); + struct timespec diff = { + .tv_sec = now.tv_sec - then.tv_sec, + .tv_nsec = now.tv_nsec - then.tv_nsec, + }; + + if (diff.tv_nsec < 0) { + diff.tv_sec--; + diff.tv_nsec += 1000000000l; + } + + return (double)diff.tv_sec / 1000.0 + (double)diff.tv_nsec / 1000000.0; +} + +LWAN_HANDLER_ROUTE(benchmark, "/benchmark") +{ + struct forth_ctx *f = forth_new(); + coro_defer(request->conn->coro, destroy_forth_ctx, f); + + if (!forth_parse_string(f, twister)) + return HTTP_INTERNAL_ERROR; + + struct timespec before = current_precise_monotonic_timespec(); + for (int i = 0; i < 100000; i++) { + struct forth_vars vars = { + .x = i / 64., + .y = i / 64., + .t = 0, + }; + if (!forth_run(f, &vars)) + return HTTP_INTERNAL_ERROR; + } + + response->mime_type = "text/plain"; + lwan_strbuf_printf(response->buffer, "elapsed time: %lfms", + elapsed_time_ms(before)); + + return HTTP_OK; +} + +LWAN_HANDLER_ROUTE(twister, "/") +{ + struct forth_ctx *f = forth_new(); + double current_time = (int32_t)time(NULL); + + coro_defer(request->conn->coro, destroy_forth_ctx, f); + + if (!forth_parse_string(f, twister)) + return HTTP_INTERNAL_ERROR; + + uint8_t *frame_buffer = coro_malloc(request->conn->coro, 64 * 64 * 4); + if (!frame_buffer) + return HTTP_INTERNAL_ERROR; + + response->mime_type = "image/gif"; + + if (!lwan_response_set_chunked(request, HTTP_OK)) + return HTTP_INTERNAL_ERROR; + + GifWriter *writer = coro_malloc_full(request->conn->coro, sizeof(*writer), + destroy_gif_writer); + + GifBegin(writer, response->buffer, 64, 64, 2, 8, true); + + for (int frame = 0; frame < 1000; frame++) { + for (int x = 0; x < 64; x++) { + for (int y = 0; y < 64; y++) { + uint8_t *pixel = &frame_buffer[4 * (y * 64 + x)]; + + struct forth_vars vars = { + .x = x / 64., + .y = y / 64., + .t = current_time, + }; + if (!forth_run(f, &vars)) + return HTTP_INTERNAL_ERROR; + switch (forth_d_stack_len(f, &vars)) { + case 3: + pixel[3] = 0; + pixel[2] = (uint8_t)(round(forth_d_stack_pop(&vars) * 255.)); + pixel[1] = (uint8_t)(round(forth_d_stack_pop(&vars) * 255.)); + pixel[0] = (uint8_t)(round(forth_d_stack_pop(&vars) * 255.)); + break; + case 4: + pixel[3] = (uint8_t)(round(forth_d_stack_pop(&vars) * 255.)); + pixel[2] = (uint8_t)(round(forth_d_stack_pop(&vars) * 255.)); + pixel[1] = (uint8_t)(round(forth_d_stack_pop(&vars) * 255.)); + pixel[0] = (uint8_t)(round(forth_d_stack_pop(&vars) * 255.)); + break; + default: + return HTTP_INTERNAL_ERROR; + } + } + } + + GifWriteFrame(writer, frame_buffer, 64, 64, 2, 8, true); + lwan_response_send_chunk(request); + lwan_request_sleep(request, 16); + current_time += .016; + } + + return HTTP_OK; +} + +int main(void) { return lwan_main(); } diff --git a/freegeoip/CMakeLists.txt b/src/samples/freegeoip/CMakeLists.txt similarity index 69% rename from freegeoip/CMakeLists.txt rename to src/samples/freegeoip/CMakeLists.txt index 21d571fe9..d98e52ab6 100644 --- a/freegeoip/CMakeLists.txt +++ b/src/samples/freegeoip/CMakeLists.txt @@ -2,16 +2,19 @@ include(FindPkgConfig) pkg_check_modules(SQLITE sqlite3>=3.6.20) if (SQLITE_FOUND) + include_directories(BEFORE ${CMAKE_BINARY_DIR}) + add_executable(freegeoip freegeoip.c ) target_link_libraries(freegeoip - -Wl,-whole-archive lwan-common -Wl,-no-whole-archive - dl + ${LWAN_COMMON_LIBS} ${ADDITIONAL_LIBRARIES} ${SQLITE_LIBRARIES} + ${SQLITE_LDFLAGS} ) + include_directories(${SQLITE_INCLUDE_DIRS}) else () message(STATUS "Freegeoip sample application not being built: SQLite not found") endif () diff --git a/src/samples/freegeoip/freegeoip.c b/src/samples/freegeoip/freegeoip.c new file mode 100644 index 000000000..b398c88af --- /dev/null +++ b/src/samples/freegeoip/freegeoip.c @@ -0,0 +1,458 @@ +/* + * lwan - web server + * Copyright (c) 2012 L. A. F. Pereira + * + * SQL query is copied from freegeoip.go + * Copyright (c) 2013 Alexandre Fiori + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _DEFAULT_SOURCE +#include +#include +#include +#include + +#include "lwan.h" +#include "lwan-cache.h" +#include "lwan-mod-serve-files.h" +#include "lwan-template.h" + +/* Set to 0 to disable */ +#define QUERIES_PER_HOUR 10000 + +struct ip_info { + struct cache_entry base; + struct { + char *code; + char *name; + } country, region; + struct { + char *name; + char *zip_code; + } city; + double latitude, longitude; + struct { + char *code, *area; + } metro; + char *ip; + const char *callback; +}; + +struct template_mime { + struct lwan_tpl *tpl; + const char *mime_type; +}; + +#undef TPL_STRUCT +#define TPL_STRUCT struct ip_info +static const struct lwan_var_descriptor template_descriptor[] = { + TPL_VAR_STR(country.code), TPL_VAR_STR(country.name), + TPL_VAR_STR(region.code), TPL_VAR_STR(region.name), + TPL_VAR_STR(city.name), TPL_VAR_STR(city.zip_code), + TPL_VAR_DOUBLE(latitude), TPL_VAR_DOUBLE(longitude), + TPL_VAR_STR(metro.code), TPL_VAR_STR(metro.area), + TPL_VAR_STR(ip), TPL_VAR_STR(callback), + TPL_VAR_SENTINEL, +}; + +static const char json_template_str[] = + "{{callback?}}alert('Using JSONP, especially with a third-party service, is insecure. Stop using this ASAP.');{{callback}}({{/callback?}}" + "{" + "\"country_code\":\"{{country.code}}\"," + "\"country_name\":\"{{country.name}}\"," + "\"region_code\":\"{{region.code}}\"," + "\"region_name\":\"{{region.name}}\"," + "\"city\":\"{{city.name}}\"," + "\"zipcode\":\"{{city.zip_code}}\"," + "\"latitude\":{{latitude}}," + "\"longitude\":{{longitude}}," + "\"metro_code\":\"{{metro.code}}\"," + "\"areacode\":\"{{metro.area}}\"," + "\"ip\":\"{{ip}}\"" + "}" + "{{callback?}});{{/callback?}}"; + +static const char xml_template_str[] = + "" + "" + "{{ip}}" + "{{country.code}}" + "{{country.name}}" + "{{region.code}}" + "{{region.name}}" + "{{city.name}}" + "{{city.zip_code}}" + "{{latitude}}" + "{{longitude}}" + "{{metro.code}}" + "{{metro.area}}" + ""; + +static const char csv_template_str[] = "\"{{ip}}\"," + "\"{{country.code}}\"," + "\"{{country.name}}\"," + "\"{{region.code}}\"," + "\"{{region.name}}\"," + "\"{{city.name}}\"," + "\"{{city.zip_code}}\"," + "\"{{latitude}}\"," + "\"{{longitude}}\"," + "\"{{metro.code}}\"," + "\"{{metro.area}}\""; + +static const char ip_to_city_query[] = + "SELECT " + " city_location.country_code, country_blocks.country_name," + " city_location.region_code, region_names.region_name," + " city_location.city_name, city_location.postal_code," + " city_location.latitude, city_location.longitude," + " city_location.metro_code, city_location.area_code " + "FROM city_blocks " + " NATURAL JOIN city_location " + " INNER JOIN country_blocks ON " + " city_location.country_code = country_blocks.country_code " + " INNER JOIN region_names ON " + " city_location.country_code = region_names.country_code " + " AND " + " city_location.region_code = region_names.region_code " + "WHERE city_blocks.ip_start <= ? " + "ORDER BY city_blocks.ip_start DESC LIMIT 1"; + +union ip_to_octet { + unsigned char octet[sizeof(in_addr_t)]; + in_addr_t ip; +}; + +struct ip_net { + union ip_to_octet ip; + union ip_to_octet mask; +}; + +/* http://en.wikipedia.org/wiki/Reserved_IP_addresses */ +#define ADDRESS(o1, o2, o3, o4) \ + { \ + .octet[0] = o1, .octet[1] = o2, .octet[2] = o3, .octet[3] = o4 \ + } + +static const struct ip_net reserved_ips[] = { + {ADDRESS(0, 0, 0, 0), ADDRESS(255, 0, 0, 0)}, + {ADDRESS(10, 0, 0, 0), ADDRESS(255, 0, 0, 0)}, + {ADDRESS(100, 64, 0, 0), ADDRESS(255, 192, 0, 0)}, + {ADDRESS(127, 0, 0, 0), ADDRESS(255, 0, 0, 0)}, + {ADDRESS(169, 254, 0, 0), ADDRESS(255, 255, 0, 0)}, + {ADDRESS(172, 16, 0, 0), ADDRESS(255, 240, 0, 0)}, + {ADDRESS(192, 0, 0, 0), ADDRESS(255, 255, 255, 248)}, + {ADDRESS(192, 0, 2, 0), ADDRESS(255, 255, 255, 0)}, + {ADDRESS(192, 88, 99, 0), ADDRESS(255, 255, 255, 0)}, + {ADDRESS(192, 168, 0, 0), ADDRESS(255, 255, 0, 0)}, + {ADDRESS(198, 18, 0, 0), ADDRESS(255, 254, 0, 0)}, + {ADDRESS(198, 51, 100, 0), ADDRESS(255, 255, 255, 0)}, + {ADDRESS(203, 0, 113, 0), ADDRESS(255, 255, 255, 0)}, + {ADDRESS(224, 0, 0, 0), ADDRESS(240, 0, 0, 0)}, + {ADDRESS(240, 0, 0, 0), ADDRESS(240, 0, 0, 0)}, + {ADDRESS(255, 255, 255, 255), ADDRESS(255, 255, 255, 255)}, +}; + +#undef ADDRESS + +#if QUERIES_PER_HOUR != 0 +struct query_limit { + struct cache_entry base; + unsigned queries; +}; + +static struct cache *query_limit; +#endif + +static struct cache *cache = NULL; +static sqlite3 *db = NULL; + +static bool net_contains_ip(const struct ip_net *net, in_addr_t ip) +{ + union ip_to_octet _ip = {.ip = ip}; + return (net->ip.octet[0] & net->mask.octet[0]) == + (_ip.octet[0] & net->mask.octet[0]) && + (net->ip.octet[1] & net->mask.octet[1]) == + (_ip.octet[1] & net->mask.octet[1]) && + (net->ip.octet[2] & net->mask.octet[2]) == + (_ip.octet[2] & net->mask.octet[2]) && + (net->ip.octet[3] & net->mask.octet[3]) == + (_ip.octet[3] & net->mask.octet[3]); +} + +static bool is_reserved_ip(in_addr_t ip) +{ + size_t i; + for (i = 0; i < N_ELEMENTS(reserved_ips); i++) { + if (net_contains_ip(&reserved_ips[i], ip)) + return true; + } + return false; +} + +static void destroy_ipinfo(struct cache_entry *entry, + void *context __attribute__((unused))) +{ + struct ip_info *ip_info = (struct ip_info *)entry; + + if (!ip_info) + return; + + free(ip_info->country.code); + free(ip_info->country.name); + free(ip_info->region.code); + free(ip_info->region.name); + free(ip_info->city.name); + free(ip_info->city.zip_code); + free(ip_info->metro.code); + free(ip_info->metro.area); + free(ip_info->ip); + free(ip_info); +} + +static ALWAYS_INLINE char *text_column_helper(sqlite3_stmt *stmt, int ind) +{ + const unsigned char *value; + + value = sqlite3_column_text(stmt, ind); + return value ? strdup((char *)value) : NULL; +} + +static struct cache_entry *create_ipinfo(const void *key, + void *cache_ctx __attribute__((unused)), + void *create_ctx __attribute__((unused))) +{ + sqlite3_stmt *stmt; + struct ip_info *ip_info = NULL; + struct in_addr addr; + + if (UNLIKELY(!inet_aton(key, &addr))) + goto end_no_finalize; + + if (is_reserved_ip(addr.s_addr)) { + ip_info = calloc(1, sizeof(*ip_info)); + if (LIKELY(ip_info)) { + ip_info->country.code = strdup("RD"); + ip_info->country.name = strdup("Reserved"); + ip_info->ip = strdup(key); + } + goto end_no_finalize; + } + + if (sqlite3_prepare(db, ip_to_city_query, sizeof(ip_to_city_query) - 1, + &stmt, NULL) != SQLITE_OK) + goto end_no_finalize; + + if (sqlite3_bind_int64(stmt, 1, ntohl(addr.s_addr)) != SQLITE_OK) + goto end; + + if (sqlite3_step(stmt) != SQLITE_ROW) + goto end; + + ip_info = malloc(sizeof(*ip_info)); + if (!ip_info) + goto end; + +#define TEXT_COLUMN(index) text_column_helper(stmt, index) + + ip_info->country.code = TEXT_COLUMN(0); + ip_info->country.name = TEXT_COLUMN(1); + ip_info->region.code = TEXT_COLUMN(2); + ip_info->region.name = TEXT_COLUMN(3); + ip_info->city.name = TEXT_COLUMN(4); + ip_info->city.zip_code = TEXT_COLUMN(5); + ip_info->latitude = sqlite3_column_double(stmt, 6); + ip_info->longitude = sqlite3_column_double(stmt, 7); + ip_info->metro.code = TEXT_COLUMN(8); + ip_info->metro.area = TEXT_COLUMN(9); + +#undef TEXT_COLUMN + + ip_info->ip = strdup(key); + +end: + sqlite3_finalize(stmt); +end_no_finalize: + return (struct cache_entry *)ip_info; +} + +#if QUERIES_PER_HOUR != 0 +static struct cache_entry * +create_query_limit(const void *key __attribute__((unused)), + void *cache_ctx __attribute__((unused)), + void *create_ctx __attribute__((unused))) +{ + struct query_limit *entry = malloc(sizeof(*entry)); + if (LIKELY(entry)) + entry->queries = 0; + return (struct cache_entry *)entry; +} + +static void destroy_query_limit(struct cache_entry *entry, + void *context __attribute__((unused))) +{ + free(entry); +} +#endif + +static struct ip_info *internal_query(struct lwan_request *request, + const char *ip_address) +{ + const char *query; + + if (request->url.len == 0) + query = ip_address; + else if (request->url.len < 7) + query = NULL; + else + query = request->url.value; + if (UNLIKELY(!query)) + return NULL; + + return (struct ip_info *)cache_coro_get_and_ref_entry( + cache, request->conn->coro, query); +} + +#if QUERIES_PER_HOUR != 0 +static bool is_rate_limited(const char *ip_address) +{ + bool limited; + int error; + struct query_limit *limit; + + limit = (struct query_limit *)cache_get_and_ref_entry(query_limit, + ip_address, &error); + if (!limit) + return true; + + limited = ATOMIC_AAF(&limit->queries, 1) > QUERIES_PER_HOUR; + cache_entry_unref(query_limit, &limit->base); + + return limited; +} +#endif + +LWAN_HANDLER(templated_output) +{ + const struct template_mime *tm = data; + const char *ip_address; + struct ip_info *info; + char ip_address_buf[INET6_ADDRSTRLEN]; + + ip_address = lwan_request_get_remote_address(request, ip_address_buf); + if (UNLIKELY(!ip_address)) + return HTTP_INTERNAL_ERROR; + +#if QUERIES_PER_HOUR != 0 + if (UNLIKELY(is_rate_limited(ip_address))) + return HTTP_FORBIDDEN; +#endif + + info = internal_query(request, ip_address); + if (UNLIKELY(!info)) + return HTTP_NOT_FOUND; + + const char *callback = lwan_request_get_query_param(request, "callback"); + struct ip_info info_with_callback = *info; + info_with_callback.callback = callback; + + if (!lwan_tpl_apply_with_buffer(tm->tpl, response->buffer, + &info_with_callback)) { + return HTTP_INTERNAL_ERROR; + } + + response->mime_type = tm->mime_type; + + return HTTP_OK; +} + +static struct template_mime compile_template(const char *template, + const char *mime_type) +{ + struct lwan_tpl *tpl = lwan_tpl_compile_string_full( + template, template_descriptor, LWAN_TPL_FLAG_CONST_TEMPLATE); + + if (!tpl) { + lwan_status_critical("Could not compile template for mime-type %s", + mime_type); + } + + return (struct template_mime){.tpl = tpl, .mime_type = mime_type}; +} + +int main(void) +{ + struct lwan l; + + lwan_init(&l); + + struct template_mime json_tpl = + compile_template(json_template_str, "application/json; charset=UTF-8"); + struct template_mime csv_tpl = + compile_template(csv_template_str, "application/csv; charset=UTF-8"); + struct template_mime xml_tpl = + compile_template(xml_template_str, "text/plain; charset=UTF-8"); + + int result = + sqlite3_open_v2("./db/ipdb.sqlite", &db, SQLITE_OPEN_READONLY, NULL); + if (result != SQLITE_OK) + lwan_status_critical("Could not open database: %s", sqlite3_errmsg(db)); + cache = cache_create(create_ipinfo, destroy_ipinfo, NULL, 10); + + sqlite3_exec(db, "PRAGMA mmap_size=123217920", NULL, NULL, NULL); + sqlite3_exec(db, "PRAGMA journal_mode=OFF", NULL, NULL, NULL); + sqlite3_exec(db, "PRAGMA locking_mode=EXCLUSIVE", NULL, NULL, NULL); + +#if QUERIES_PER_HOUR != 0 + lwan_status_info("Limiting to %d queries per hour per client", + QUERIES_PER_HOUR); + query_limit = + cache_create(create_query_limit, destroy_query_limit, NULL, 3600); +#else + lwan_status_info("Rate-limiting disabled"); +#endif + + const struct lwan_url_map default_map[] = { + {.prefix = "/json/", + .handler = LWAN_HANDLER_REF(templated_output), + .data = &json_tpl}, + {.prefix = "/xml/", + .handler = LWAN_HANDLER_REF(templated_output), + .data = &xml_tpl}, + {.prefix = "/csv/", + .handler = LWAN_HANDLER_REF(templated_output), + .data = &csv_tpl}, + {.prefix = "/", SERVE_FILES("./static")}, + {.prefix = NULL}, + }; + + lwan_set_url_map(&l, default_map); + lwan_main_loop(&l); + lwan_shutdown(&l); + + lwan_tpl_free(json_tpl.tpl); + lwan_tpl_free(xml_tpl.tpl); + lwan_tpl_free(csv_tpl.tpl); +#if QUERIES_PER_HOUR != 0 + cache_destroy(query_limit); +#endif + cache_destroy(cache); + sqlite3_close(db); + + return 0; +} diff --git a/src/samples/hello-no-meta/CMakeLists.txt b/src/samples/hello-no-meta/CMakeLists.txt new file mode 100644 index 000000000..8d70abdd4 --- /dev/null +++ b/src/samples/hello-no-meta/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(hello-no-meta + main.c +) + +target_link_libraries(hello-no-meta + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) diff --git a/src/samples/hello-no-meta/main.c b/src/samples/hello-no-meta/main.c new file mode 100644 index 000000000..6d7f394ad --- /dev/null +++ b/src/samples/hello-no-meta/main.c @@ -0,0 +1,55 @@ +/* + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include "lwan.h" + +/* Defining a handler like this will make it invisible for configuration + * files, but it can be referenced by a url_map and will work just the + * same. */ +static enum lwan_http_status hello_world(struct lwan_request *request + __attribute__((unused)), + struct lwan_response *response, + void *data __attribute__((unused))) +{ + static const char message[] = "Hello, World!"; + + response->mime_type = "text/plain"; + lwan_strbuf_set_static(response->buffer, message, sizeof(message) - 1); + + return HTTP_OK; +} + +int main(void) +{ + const struct lwan_url_map default_map[] = { + {.prefix = "/", .handler = hello_world}, + {.prefix = NULL} + }; + struct lwan l; + + lwan_init(&l); + + lwan_set_url_map(&l, default_map); + lwan_main_loop(&l); + + lwan_shutdown(&l); + + return 0; +} diff --git a/src/samples/hello/CMakeLists.txt b/src/samples/hello/CMakeLists.txt new file mode 100644 index 000000000..d9afd70d3 --- /dev/null +++ b/src/samples/hello/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(hello + main.c +) + +target_link_libraries(hello + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) diff --git a/common/lwan-http-authorize.h b/src/samples/hello/main.c similarity index 66% rename from common/lwan-http-authorize.h rename to src/samples/hello/main.c index c2655970b..2ffd4d146 100644 --- a/common/lwan-http-authorize.h +++ b/src/samples/hello/main.c @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -17,15 +17,16 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#pragma once - #include "lwan.h" -bool lwan_http_authorize_init(void); -void lwan_http_authorize_shutdown(void); +LWAN_HANDLER_ROUTE(hello_world, "/") +{ + static const char message[] = "Hello, World!"; + + response->mime_type = "text/plain"; + lwan_strbuf_set_static(response->buffer, message, sizeof(message) - 1); + + return HTTP_OK; +} -bool -lwan_http_authorize(lwan_request_t *request, - lwan_value_t *authorization, - const char *realm, - const char *password_file); +int main(void) { return lwan_main(); } diff --git a/src/samples/pastebin/CMakeLists.txt b/src/samples/pastebin/CMakeLists.txt new file mode 100644 index 000000000..8b5c121f9 --- /dev/null +++ b/src/samples/pastebin/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(pastebin + main.c +) + +target_link_libraries(pastebin + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) diff --git a/src/samples/pastebin/main.c b/src/samples/pastebin/main.c new file mode 100644 index 000000000..fc13832a5 --- /dev/null +++ b/src/samples/pastebin/main.c @@ -0,0 +1,218 @@ +/* + * lwan - web server + * Copyright (c) 2022 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include + +#include "hash.h" +#include "int-to-str.h" +#include "lwan.h" +#include "lwan-cache.h" +#include "lwan-private.h" + +#define CACHE_FOR_HOURS 2 + +static struct cache *pastes; + +struct paste { + struct cache_entry entry; + size_t len; + char value[]; +}; + +static struct cache_entry *create_paste(const void *key __attribute__((unused)), + void *cache_ctx __attribute__((unused)), + void *create_ctx) +{ + const struct lwan_value *body = create_ctx; + size_t alloc_size; + + if (!body) + return NULL; + + if (__builtin_add_overflow(sizeof(struct paste), body->len, &alloc_size)) + return NULL; + + struct paste *paste = malloc(alloc_size); + if (paste) { + paste->len = body->len; + memcpy(paste->value, body->value, body->len); + } + + return (struct cache_entry *)paste; +} + +static void destroy_paste(struct cache_entry *entry, + void *context __attribute__((unused))) +{ + free(entry); +} + +static enum lwan_http_status post_paste(struct lwan_request *request, + struct lwan_response *response) +{ + const struct lwan_value *body = lwan_request_get_request_body(request); + + if (!body) + return HTTP_BAD_REQUEST; + + for (int try = 0; try < 10; try++) { + void *key; + + do { + key = (void *)(uintptr_t)lwan_random_uint64(); + } while (!key); + + struct cache_entry *paste = cache_coro_get_and_ref_entry_with_ctx( + pastes, request->conn->coro, key, (void *)body); + + if (paste) { + if (!cache_entry_is_new(paste)) + continue; + + const char *host_hdr = lwan_request_get_host(request); + + if (!host_hdr) + return HTTP_BAD_REQUEST; + + response->mime_type = "text/plain"; + lwan_strbuf_printf(response->buffer, "https://%s/p/%zu\n\n", + host_hdr, (uint64_t)(uintptr_t)key); + + return HTTP_OK; + } + } + + return HTTP_UNAVAILABLE; +} + +static enum lwan_http_status doc(struct lwan_request *request, + struct lwan_response *response) +{ + const char *host_hdr = lwan_request_get_host(request); + + if (!host_hdr) + return HTTP_BAD_REQUEST; + + response->mime_type = "text/plain"; + + lwan_strbuf_printf( + response->buffer, + "Simple Paste Bin\n" + "================\n" + "\n" + "To post a file: curl -X POST --data-binary @/path/to/filename " + "https://%s/\n" + "To post clipboard: xsel -o | curl -X POST --data-binary @- " + "https://%s/\n" + "To view: Access the URL given as a response.\n" + " Extension suffixes may be used to provide " + "response with different MIME-type.\n" + "\n" + "Items are cached for %d hours and are not stored on disk", + host_hdr, host_hdr, CACHE_FOR_HOURS); + + return HTTP_OK; +} + +LWAN_HANDLER_ROUTE(view_root, "/") +{ + switch (lwan_request_get_method(request)) { + case REQUEST_METHOD_POST: + return post_paste(request, response); + case REQUEST_METHOD_GET: + return doc(request, response); + default: + return HTTP_NOT_ALLOWED; + } +} + +static bool parse_uint64(const char *s, uint64_t *out) +{ + char *endptr; + + if (!*s) + return false; + + errno = 0; + *out = strtoull(s, &endptr, 10); + + if (errno != 0) + return false; + + if (*endptr != '\0' || s == endptr) + return false; + + return true; +} + +LWAN_HANDLER_ROUTE(view_paste, "/p/") +{ + char *dot = memrchr(request->url.value, '.', request->url.len); + const char *mime_type; + + if (dot) { + mime_type = lwan_determine_mime_type_for_file_name(dot); + *dot = '\0'; + } else { + mime_type = "text/plain"; + } + + uint64_t key; + + if (!parse_uint64(request->url.value, &key)) + return HTTP_BAD_REQUEST; + + struct paste *paste = (struct paste *)cache_coro_get_and_ref_entry( + pastes, request->conn->coro, (void *)(uintptr_t)key); + + if (!paste) + return HTTP_NOT_FOUND; + + response->mime_type = mime_type; + lwan_strbuf_set_static(response->buffer, paste->value, paste->len); + + return HTTP_OK; +} + +int main(void) +{ + struct lwan l; + + lwan_init(&l); + + lwan_detect_url_map(&l); + + pastes = cache_create_full(create_paste, + destroy_paste, + hash_int64_new, + NULL, + CACHE_FOR_HOURS * 60 * 60); + if (!pastes) + lwan_status_critical("Could not create paste cache"); + + lwan_main_loop(&l); + + lwan_shutdown(&l); + + return 0; +} diff --git a/src/samples/send-money-json-api/CMakeLists.txt b/src/samples/send-money-json-api/CMakeLists.txt new file mode 100644 index 000000000..a7c4eb9f1 --- /dev/null +++ b/src/samples/send-money-json-api/CMakeLists.txt @@ -0,0 +1,9 @@ +add_executable(send-money-json-api + main.c + ../techempower/json.c +) + +target_link_libraries(send-money-json-api + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) diff --git a/src/samples/send-money-json-api/main.c b/src/samples/send-money-json-api/main.c new file mode 100644 index 000000000..2c9ecf348 --- /dev/null +++ b/src/samples/send-money-json-api/main.c @@ -0,0 +1,151 @@ +/* + * lwan - web server + * Copyright (c) 2024 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +/* + * Hastily written to compare results with other languages and + * frameworks after this Twitter thread: + * https://twitter.com/iSeiryu/status/1793830738153889902 + */ + +#include +#include + +#define ARRAY_SIZE N_ELEMENTS + +#include "../techempower/json.h" +#include "lwan.h" + +struct address { + const char *street; + const char *city; + const char *state; + const char *zip; +}; + +struct account_holder { + const char *id; + const char *firstName; + const char *lastName; + struct address address; + const char *email; +}; + +struct send_money_request { + struct account_holder from, to; + int amount; + const char *sendOn; +}; + +struct receipt { + char *from_account; + char *to_account; + char *created_on; + char *to_address; + int amount; +}; + +static const struct json_obj_descr receipt_descr[] = { + JSON_OBJ_DESCR_PRIM(struct receipt, from_account, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct receipt, to_account, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct receipt, amount, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM(struct receipt, created_on, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct receipt, to_address, JSON_TOK_STRING), +}; + +static const struct json_obj_descr address_descr[] = { + JSON_OBJ_DESCR_PRIM(struct address, street, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct address, city, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct address, state, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct address, zip, JSON_TOK_STRING), +}; + +static const struct json_obj_descr account_holder_descr[] = { + JSON_OBJ_DESCR_PRIM(struct account_holder, id, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct account_holder, firstName, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct account_holder, lastName, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM(struct account_holder, email, JSON_TOK_STRING), + JSON_OBJ_DESCR_OBJECT(struct account_holder, address, address_descr), +}; + +static const struct json_obj_descr send_money_request_descr[] = { + JSON_OBJ_DESCR_PRIM(struct send_money_request, amount, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM(struct send_money_request, sendOn, JSON_TOK_STRING), + JSON_OBJ_DESCR_OBJECT( + struct send_money_request, from, account_holder_descr), + JSON_OBJ_DESCR_OBJECT(struct send_money_request, to, account_holder_descr), +}; + +static int append_to_strbuf(const char *bytes, size_t len, void *data) +{ + struct lwan_strbuf *strbuf = data; + + return !lwan_strbuf_append_str(strbuf, bytes, len); +} + +static inline struct tm *localtime_now(void) +{ + static __thread struct tm result; + time_t now = time(NULL); + return localtime_r(&now, &result); +} + +LWAN_HANDLER_ROUTE(send_money, "/send-money") +{ + struct send_money_request smr; + const struct lwan_value *body; + + if (lwan_request_get_method(request) != REQUEST_METHOD_POST) + return HTTP_BAD_REQUEST; + + body = lwan_request_get_request_body(request); + if (!body) { + return HTTP_BAD_REQUEST; + } + + if (json_obj_parse(body->value, body->len, send_money_request_descr, + N_ELEMENTS(send_money_request_descr), + &smr) != (1 << 0 | 1 << 1 | 1 << 2 | 1 << 3)) { + return HTTP_BAD_REQUEST; + } + + char formatted_time[25]; + strftime(formatted_time, 25, "%FT%T%z", localtime_now()); + + struct receipt r = { + .from_account = coro_printf(request->conn->coro, "%s %s", + smr.from.firstName, smr.from.lastName), + .to_account = coro_printf(request->conn->coro, "%s %s", + smr.to.firstName, smr.to.lastName), + .to_address = coro_printf(request->conn->coro, "%s, %s, %s, %s", + smr.to.address.street, smr.to.address.city, + smr.to.address.state, smr.to.address.zip), + .created_on = formatted_time, + .amount = smr.amount, + }; + if (json_obj_encode_full(receipt_descr, N_ELEMENTS(receipt_descr), &r, + append_to_strbuf, response->buffer, false) != 0) { + return HTTP_INTERNAL_ERROR; + } + + response->mime_type = "application/json"; + return HTTP_OK; +} + +int main(void) { return lwan_main(); } diff --git a/src/samples/smolsite/CMakeLists.txt b/src/samples/smolsite/CMakeLists.txt new file mode 100644 index 000000000..aef6689fb --- /dev/null +++ b/src/samples/smolsite/CMakeLists.txt @@ -0,0 +1,43 @@ +check_function_exists(fmemopen HAVE_FMEMOPEN) +if (HAVE_FMEMOPEN) + find_program(ZIP NAMES zip) + if (ZIP) + add_executable(smolsite + main.c + junzip.c + qrcodegen.c + ../clock/gifenc.c + ) + + target_link_libraries(smolsite + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} + ) + + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/smolsite.zip + COMMAND ${ZIP} -DXjq9 ${CMAKE_BINARY_DIR}/smolsite.zip ${CMAKE_SOURCE_DIR}/src/samples/smolsite/index.html + DEPENDS ${CMAKE_SOURCE_DIR}/src/samples/smolsite/index.html + COMMENT "Zipping smolsite ZIP" + ) + add_custom_target(generate_smolsite_zip + DEPENDS ${CMAKE_BINARY_DIR}/smolsite.zip + ) + + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/smolsite.h + COMMAND bin2hex + ${CMAKE_SOURCE_DIR}/src/samples/smolsite/smolsite.html smolsite_html + ${CMAKE_BINARY_DIR}/smolsite.zip smolsite_zip > ${CMAKE_BINARY_DIR}/smolsite.h + DEPENDS ${CMAKE_SOURCE_DIR}/src/samples/smolsite/smolsite.html + ${CMAKE_BINARY_DIR}/smolsite.zip + bin2hex + COMMENT "Bundling smolsite template" + ) + add_custom_target(generate_smolsite + DEPENDS ${CMAKE_BINARY_DIR}/smolsite.h + ) + + add_dependencies(smolsite generate_smolsite generate_smolsite_zip) + endif () +endif () diff --git a/src/samples/smolsite/index.html b/src/samples/smolsite/index.html new file mode 100644 index 000000000..b55978405 --- /dev/null +++ b/src/samples/smolsite/index.html @@ -0,0 +1,61 @@ + + + +

It's a smolsite!

+ +

This whole site fits inside the URL!

+ +

To host yours: use the command-line: put everything in a ZIP file, Base 64 encode it, and tack it after "https://smolsite.zip/". For instance, if your +smol site is comprised of two files, index.html and mylogo.png, you can generate a URL like so:

+ +
+$ echo "https://smolsite.zip/%60zip%20-DXjq9%20-%20index.html%20mylogo.png%20|%20base64%20--wrap%200%60"
+
+ +

FAQ

+

I don't have a command line, can I still share a smolsite?

+

Sure, drag the ZIP file to the bottom purple bar.

+ +

Can I host a website on a piece of paper?

+

Yes! Click on the QR code below to see a printable/scannable version +of this website.

+ +

Is this open source?

+

Yes, it's open source, part of the Lwan web server project.

+ +

Is the ZIP file unpacked in the server?

The contents are +processed in the server and kept in RAM for 15 minutes. The content is gone +after that, unless someone opens the same link again.

+ +

Is this protected against ZIP bombs?

Deflated data is sent back +unmodified to browsers, and no recursive unzipping happens. So, yes, it's +somewhat protected against ZIP bombs.

+ +

Can content be taken down?

We'll do our best to ensure exact URLs +are blocked from being shared by smolsite. (We can't fully take the content +down, as it is in the URL and that's something we do not control: if someone +has a link to improper content, they can extract it without the need of +smolsite.) +Send the full link to abuse.smolsite@tia.mat.br +with the reason the content should be taken down and we'll take it from there.

+ + + + + diff --git a/src/samples/smolsite/junzip.c b/src/samples/smolsite/junzip.c new file mode 100644 index 000000000..813cc80c9 --- /dev/null +++ b/src/samples/smolsite/junzip.c @@ -0,0 +1,323 @@ +// JUnzip library by Joonas Pihlajamaa. See junzip.h for license and details. + +#include +#include +#include + +#include "junzip.h" + +// Read ZIP file end record. Will move within file. +int jzReadEndRecord(JZFile *zip, JZEndRecord *endRecord) +{ + size_t fileSize, readBytes, i; + JZEndRecord *er; + + if (zip->seek(zip, 0, SEEK_END)) { + fprintf(stderr, "Couldn't go to end of zip file!"); + return Z_ERRNO; + } + + if ((fileSize = zip->tell(zip)) <= sizeof(JZEndRecord)) { + fprintf(stderr, "Too small file to be a zip!"); + return Z_ERRNO; + } + + readBytes = (fileSize < sizeof(zip->buffer)) ? fileSize : sizeof(zip->buffer); + + if (zip->seek(zip, fileSize - readBytes, SEEK_SET)) { + fprintf(stderr, "Cannot seek in zip file!"); + return Z_ERRNO; + } + + if (zip->read(zip, zip->buffer, readBytes) < readBytes) { + fprintf(stderr, "Couldn't read end of zip file!"); + return Z_ERRNO; + } + + // Naively assume signature can only be found in one place... + for (i = readBytes - sizeof(JZEndRecord); i; i--) { + er = (JZEndRecord *)(zip->buffer + i); + if (er->signature == 0x06054B50) + goto signature_found; + } + + fprintf(stderr, "End record signature not found in zip!"); + return Z_ERRNO; + +signature_found: + memcpy(endRecord, er, sizeof(JZEndRecord)); + + if (endRecord->diskNumber || endRecord->centralDirectoryDiskNumber || + endRecord->numEntries != endRecord->numEntriesThisDisk) { + fprintf(stderr, "Multifile zips not supported!"); + return Z_ERRNO; + } + + return Z_OK; +} + +// Read ZIP file global directory. Will move within file. +int jzReadCentralDirectory(JZFile *zip, + JZEndRecord *endRecord, + JZRecordCallback callback, + void *user_data) +{ + JZGlobalFileHeader fileHeader; + JZFileHeader header; + int i; + + if (zip->seek(zip, endRecord->centralDirectoryOffset, SEEK_SET)) { + fprintf(stderr, "Cannot seek in zip file!"); + return Z_ERRNO; + } + + for (i = 0; i < endRecord->numEntries; i++) { + if (zip->read(zip, &fileHeader, sizeof(JZGlobalFileHeader)) < + sizeof(JZGlobalFileHeader)) { + fprintf(stderr, "Couldn't read file header %d!", i); + return Z_ERRNO; + } + + if (fileHeader.signature != 0x02014B50) { + fprintf(stderr, "Invalid file header signature %d!", i); + return Z_ERRNO; + } + + if (fileHeader.fileNameLength + 1 >= JZ_BUFFER_SIZE) { + fprintf(stderr, "Too long file name %d!", i); + return Z_ERRNO; + } + + if (zip->read(zip, zip->buffer, fileHeader.fileNameLength) < + fileHeader.fileNameLength) { + fprintf(stderr, "Couldn't read filename %d!", i); + return Z_ERRNO; + } + + zip->buffer[fileHeader.fileNameLength] = '\0'; // NULL terminate + + if (zip->seek(zip, fileHeader.extraFieldLength, SEEK_CUR) || + zip->seek(zip, fileHeader.fileCommentLength, SEEK_CUR)) { + fprintf(stderr, "Couldn't skip extra field or file comment %d", i); + return Z_ERRNO; + } + + // Construct JZFileHeader from global file header + memcpy(&header, &fileHeader.compressionMethod, sizeof(header)); + header.offset = fileHeader.relativeOffsetOflocalHeader; + + if (!callback(zip, i, &header, (char *)zip->buffer, user_data)) + break; // end if callback returns zero + } + + return Z_OK; +} + +// Read local ZIP file header. Silent on errors so optimistic reading possible. +int jzReadLocalFileHeaderRaw(JZFile *zip, + JZLocalFileHeader *header, + char *filename, + int len) +{ + + if (zip->read(zip, header, sizeof(JZLocalFileHeader)) < + sizeof(JZLocalFileHeader)) + return Z_ERRNO; + + if (header->signature != 0x04034B50) + return Z_ERRNO; + + if (len) { // read filename + if (header->fileNameLength >= len) + return Z_ERRNO; // filename cannot fit + + if (zip->read(zip, filename, header->fileNameLength) < + header->fileNameLength) + return Z_ERRNO; // read fail + + filename[header->fileNameLength] = '\0'; // NULL terminate + } else { // skip filename + if (zip->seek(zip, header->fileNameLength, SEEK_CUR)) + return Z_ERRNO; + } + + if (header->extraFieldLength) { + if (zip->seek(zip, header->extraFieldLength, SEEK_CUR)) + return Z_ERRNO; + } + + // For now, silently ignore bit flags and hope ZLIB can uncompress + // if(header->generalPurposeBitFlag) + // return Z_ERRNO; // Flags not supported + + if (header->compressionMethod == 0 && + (header->compressedSize != header->uncompressedSize)) + return Z_ERRNO; // Method is "store" but sizes indicate otherwise, abort + + return Z_OK; +} + +int jzReadLocalFileHeader(JZFile *zip, + JZFileHeader *header, + char *filename, + int len) +{ + JZLocalFileHeader localHeader; + + if (jzReadLocalFileHeaderRaw(zip, &localHeader, filename, len) != Z_OK) + return Z_ERRNO; + + memcpy(header, &localHeader.compressionMethod, sizeof(JZFileHeader)); + header->offset = 0; // not used in local context + + return Z_OK; +} + +// Read data from file stream, described by header, to preallocated buffer +int jzReadData(JZFile *zip, JZFileHeader *header, void *buffer) +{ +#ifdef HAVE_ZLIB + unsigned char *bytes = (unsigned char *)buffer; // cast + long compressedLeft, uncompressedLeft; + int ret; + z_stream strm; +#endif + + if (header->compressionMethod == 0) { // Store - just read it + if (zip->read(zip, buffer, header->uncompressedSize) < + header->uncompressedSize || + zip->error(zip)) + return Z_ERRNO; +#ifdef HAVE_ZLIB + } else if (header->compressionMethod == 8) { // Deflate - using zlib + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + strm.avail_in = 0; + strm.next_in = Z_NULL; + + // Use inflateInit2 with negative window bits to indicate raw data + if ((ret = inflateInit2(&strm, -MAX_WBITS)) != Z_OK) + return ret; // Zlib errors are negative + + // Inflate compressed data + for (compressedLeft = header->compressedSize, + uncompressedLeft = header->uncompressedSize; + compressedLeft && uncompressedLeft && ret != Z_STREAM_END; + compressedLeft -= strm.avail_in) { + // Read next chunk + strm.avail_in = + zip->read(zip, zip->buffer, + (sizeof(zip->buffer) < compressedLeft) ? sizeof(zip->buffer) + : compressedLeft); + + if (strm.avail_in == 0 || zip->error(zip)) { + inflateEnd(&strm); + return Z_ERRNO; + } + + strm.next_in = zip->buffer; + strm.avail_out = uncompressedLeft; + strm.next_out = bytes; + + compressedLeft -= strm.avail_in; // inflate will change avail_in + + ret = inflate(&strm, Z_NO_FLUSH); + + if (ret == Z_STREAM_ERROR) + return ret; // shouldn't happen + + switch (ret) { + case Z_NEED_DICT: + ret = Z_DATA_ERROR; /* and fall through */ + case Z_DATA_ERROR: + case Z_MEM_ERROR: + (void)inflateEnd(&strm); + return ret; + } + + bytes += uncompressedLeft - strm.avail_out; // bytes uncompressed + uncompressedLeft = strm.avail_out; + } + + inflateEnd(&strm); +#else +#ifdef HAVE_PUFF + } else if (header->compressionMethod == 8) { // Deflate - using puff() + unsigned long destlen = header->uncompressedSize, + sourcelen = header->compressedSize; + unsigned char *comp = (unsigned char *)malloc(sourcelen); + if (comp == NULL) + return Z_ERRNO; // couldn't allocate + unsigned long read = zip->read(zip, comp, sourcelen); + if (read != sourcelen) + return Z_ERRNO; // TODO: more robust read loop + int ret = puff((unsigned char *)buffer, &destlen, comp, &sourcelen); + free(comp); + if (ret) + return Z_ERRNO; // something went wrong +#endif // HAVE_PUFF +#endif + } else { + return Z_ERRNO; + } + + return Z_OK; +} + +typedef struct { + JZFile handle; + FILE *fp; +} StdioJZFile; + +static size_t stdio_read_file_handle_read(JZFile *file, void *buf, size_t size) +{ + StdioJZFile *handle = (StdioJZFile *)file; + return fread(buf, 1, size, handle->fp); +} + +static size_t stdio_read_file_handle_tell(JZFile *file) +{ + StdioJZFile *handle = (StdioJZFile *)file; + return (size_t)ftell(handle->fp); +} + +static int stdio_read_file_handle_seek(JZFile *file, size_t offset, int whence) +{ + StdioJZFile *handle = (StdioJZFile *)file; + return fseek(handle->fp, (long)offset, whence); +} + +static int stdio_read_file_handle_error(JZFile *file) +{ + StdioJZFile *handle = (StdioJZFile *)file; + return ferror(handle->fp); +} + +static void stdio_read_file_handle_close(JZFile *file) +{ + StdioJZFile *handle = (StdioJZFile *)file; + fclose(handle->fp); + free(file); +} + +JZFile *jzfile_from_stdio_file(FILE *fp) +{ + StdioJZFile *handle = (StdioJZFile *)malloc(sizeof(StdioJZFile)); + + handle->handle.read = stdio_read_file_handle_read; + handle->handle.tell = stdio_read_file_handle_tell; + handle->handle.seek = stdio_read_file_handle_seek; + handle->handle.error = stdio_read_file_handle_error; + handle->handle.close = stdio_read_file_handle_close; + handle->fp = fp; + + return &(handle->handle); +} + +void jzfile_free(JZFile *f) +{ + if (f->close) + f->close(f); +} diff --git a/src/samples/smolsite/junzip.h b/src/samples/smolsite/junzip.h new file mode 100644 index 000000000..4e6b8e313 --- /dev/null +++ b/src/samples/smolsite/junzip.h @@ -0,0 +1,145 @@ +/** + * JUnzip library by Joonas Pihlajamaa (firstname.lastname@iki.fi). + * Released into public domain. https://github.com/jokkebk/JUnzip + */ + +#ifndef __JUNZIP_H +#define __JUNZIP_H + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include + +// Enable compiling without Zlib as well +// (no compression support, only "store") +#ifdef HAVE_ZLIB +#include +#else +#define Z_OK 0 +#define Z_ERRNO -1 +#endif + +#ifdef HAVE_PUFF +#include "puff.h" +#endif + +// If you don't have stdint.h, the following two lines should work for most 32/64 bit systems +// typedef unsigned int uint32_t; +// typedef unsigned short uint16_t; + +#define JZHOUR(t) ((t)>>11) +#define JZMINUTE(t) (((t)>>5) & 63) +#define JZSECOND(t) (((t) & 31) * 2) +#define JZTIME(h,m,s) (((h)<<11) + ((m)<<5) + (s)/2) + +#define JZYEAR(t) (((t)>>9) + 1980) +#define JZMONTH(t) (((t)>>5) & 15) +#define JZDAY(t) ((t) & 31) +#define JZDATE(y,m,d) ((((y)-1980)<<9) + ((m)<<5) + (d)) + +typedef struct JZFile JZFile; + +#define JZ_BUFFER_SIZE 65536 + +struct JZFile { + size_t (*read)(JZFile *file, void *buf, size_t size); + size_t (*tell)(JZFile *file); + int (*seek)(JZFile *file, size_t offset, int whence); + int (*error)(JZFile *file); + void (*close)(JZFile *file); + unsigned char buffer[JZ_BUFFER_SIZE]; +}; + +JZFile * +jzfile_from_stdio_file(FILE *fp); + +void jzfile_free(JZFile *f); + +typedef struct __attribute__ ((__packed__)) { + uint32_t signature; // 0x04034B50 + uint16_t versionNeededToExtract; // unsupported + uint16_t generalPurposeBitFlag; // unsupported + uint16_t compressionMethod; + uint16_t lastModFileTime; + uint16_t lastModFileDate; + uint32_t crc32; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t fileNameLength; + uint16_t extraFieldLength; // unsupported +} JZLocalFileHeader; + +typedef struct __attribute__ ((__packed__)) { + uint32_t signature; // 0x02014B50 + uint16_t versionMadeBy; // unsupported + uint16_t versionNeededToExtract; // unsupported + uint16_t generalPurposeBitFlag; // unsupported + uint16_t compressionMethod; + uint16_t lastModFileTime; + uint16_t lastModFileDate; + uint32_t crc32; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t fileNameLength; + uint16_t extraFieldLength; // unsupported + uint16_t fileCommentLength; // unsupported + uint16_t diskNumberStart; // unsupported + uint16_t internalFileAttributes; // unsupported + uint32_t externalFileAttributes; // unsupported + uint32_t relativeOffsetOflocalHeader; +} JZGlobalFileHeader; + +typedef struct __attribute__ ((__packed__)) { + uint16_t compressionMethod; + uint16_t lastModFileTime; + uint16_t lastModFileDate; + uint32_t crc32; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint32_t offset; +} JZFileHeader; + +typedef struct __attribute__ ((__packed__)) { + uint32_t signature; // 0x06054b50 + uint16_t diskNumber; // unsupported + uint16_t centralDirectoryDiskNumber; // unsupported + uint16_t numEntriesThisDisk; // unsupported + uint16_t numEntries; + uint32_t centralDirectorySize; + uint32_t centralDirectoryOffset; + uint16_t zipCommentLength; + // Followed by .ZIP file comment (variable size) +} JZEndRecord; + +// Callback prototype for central and local file record reading functions +typedef int (*JZRecordCallback)(JZFile *zip, int index, JZFileHeader *header, + char *filename, void *user_data); + + +// Read ZIP file end record. Will move within file. +int jzReadEndRecord(JZFile *zip, JZEndRecord *endRecord); + +// Read ZIP file global directory. Will move within file. +// Callback is called for each record, until callback returns zero +int jzReadCentralDirectory(JZFile *zip, JZEndRecord *endRecord, + JZRecordCallback callback, void *user_data); + +// Read local ZIP file header. Silent on errors so optimistic reading possible. +int jzReadLocalFileHeader(JZFile *zip, JZFileHeader *header, + char *filename, int len); + +// Same as above but returns the full raw header +int jzReadLocalFileHeaderRaw(JZFile *zip, JZLocalFileHeader *header, + char *filename, int len); + +// Read data from file stream, described by header, to preallocated buffer +// Return value is zlib coded, e.g. Z_OK, or error code +int jzReadData(JZFile *zip, JZFileHeader *header, void *buffer); + +#ifdef __cplusplus +}; +#endif /* __cplusplus */ + +#endif diff --git a/src/samples/smolsite/main.c b/src/samples/smolsite/main.c new file mode 100644 index 000000000..168640292 --- /dev/null +++ b/src/samples/smolsite/main.c @@ -0,0 +1,412 @@ +/* + * smolsite.zip + * Copyright (c) 2023 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#include "base64.h" +#include "hash.h" +#include "int-to-str.h" +#include "lwan.h" +#include "sha1.h" +#include "lwan-cache.h" +#include "lwan-private.h" +#include "lwan-template.h" + +#include "junzip.h" +#include "qrcodegen.h" +#include "../clock/gifenc.h" + +#include "smolsite.h" + +#define CACHE_FOR_MINUTES 15 + +static struct cache *sites; + +struct file { + ptrdiff_t data_offset; + size_t size_compressed; + const char *mime_type; + bool deflated; +}; + +struct site { + struct cache_entry entry; + struct lwan_value zipped; + struct hash *files; + struct lwan_strbuf qr_code; + int has_qr_code; +}; + +struct iframe_tpl_vars { + const char *digest; + int has_qr_code; +}; + +#undef TPL_STRUCT +#define TPL_STRUCT struct iframe_tpl_vars +static const struct lwan_var_descriptor iframe_tpl_desc[] = { + TPL_VAR_STR(digest), + TPL_VAR_INT(has_qr_code), + TPL_VAR_SENTINEL, +}; + +static struct lwan_tpl *iframe_tpl; +static struct lwan_value smolsite_zip_base64; + +static void calc_hash(struct lwan_value value, + char digest_str[static 41]) +{ + /* FIXME: Is SHA-1 overkill? */ + sha1_context ctx; + unsigned char digest[20]; + + sha1_init(&ctx); + sha1_update(&ctx, (const unsigned char *)value.value, value.len); + sha1_finalize(&ctx, digest); + + snprintf(digest_str, 41, + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], + digest[6], digest[7], digest[8], digest[9], digest[10], digest[11], + digest[12], digest[13], digest[14], digest[15], digest[16], + digest[17], digest[18], digest[19]); +} + +static int file_cb( + JZFile *zip, int idx, JZFileHeader *header, char *filename, void *user_data) +{ + struct site *site = user_data; + char filename_buf[1024]; + ptrdiff_t data_offset; + size_t cur_offset = zip->tell(zip); + + if (zip->seek(zip, header->offset, SEEK_SET)) + return 0; + + JZFileHeader local; + if (jzReadLocalFileHeader(zip, &local, filename_buf, sizeof(filename_buf))) + return 0; + if (__builtin_add_overflow(zip->tell(zip), local.offset, &data_offset)) + return 0; + if (data_offset < 0) + return 0; + if ((size_t)data_offset > site->zipped.len) + return 0; + + uint32_t last_data_offset; + if (__builtin_add_overflow(local.compressedSize, data_offset, &last_data_offset)) + return 0; + if (last_data_offset > site->zipped.len) + return 0; + + struct file *file = malloc(sizeof(*file)); + if (!file) + return 0; + + file->data_offset = data_offset; + file->deflated = local.compressionMethod == 8; + file->size_compressed = local.compressedSize; + file->mime_type = lwan_determine_mime_type_for_file_name(filename); + + char *key = strdup(filename); + if (key) { + if (!hash_add_unique(site->files, key, file)) { + zip->seek(zip, cur_offset, SEEK_SET); + return 1; + } + } + + free(key); + free(file); + return 0; +} + +static bool generate_qr_code_gif(const char *b64, struct lwan_strbuf *output) +{ + uint8_t qrcode[qrcodegen_BUFFER_LEN_MAX]; + uint8_t tempBuffer[qrcodegen_BUFFER_LEN_MAX]; + char *url; + bool ok; + + if (!lwan_strbuf_init(output)) + return false; + + if (asprintf(&url, "https://smolsite.zip/%s", b64) < 0) + return false; + + ok = qrcodegen_encodeText(url, tempBuffer, qrcode, qrcodegen_Ecc_LOW, + qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, + qrcodegen_Mask_AUTO, true); + free(url); + if (!ok) + return false; + + int size = qrcodegen_getSize(qrcode); + if ((int)(uint16_t)size != size) + return false; + + ge_GIF *gif = + ge_new_gif(output, (uint16_t)size, (uint16_t)size, NULL, 4, -1); + if (!gif) { + lwan_strbuf_free(output); + return false; + } + + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + gif->frame[y * size + x] = qrcodegen_getModule(qrcode, x, y) ? 0 : 15; + } + } + ge_add_frame(gif, 0); + ge_close_gif(gif); + + return true; +} + +static struct cache_entry * +create_site(const void *key, void *context, void *create_ctx) +{ + struct lwan_strbuf qr_code = LWAN_STRBUF_STATIC_INIT; + const struct lwan_value *base64_encoded = create_ctx; + unsigned char *decoded = NULL; + size_t decoded_len; + + if (!base64_encoded) + return NULL; + + if (UNLIKELY(!base64_validate((unsigned char *)base64_encoded->value, + base64_encoded->len))) { + return NULL; + } + + decoded = base64_decode((unsigned char *)base64_encoded->value, + base64_encoded->len, &decoded_len); + if (UNLIKELY(!decoded)) + return NULL; + + struct site *site = malloc(sizeof(*site)); + if (!site) + goto no_site; + + site->has_qr_code = + generate_qr_code_gif((const char *)base64_encoded->value, &qr_code); + + site->qr_code = qr_code; + site->zipped = + (struct lwan_value){.value = (char *)decoded, .len = decoded_len}; + + FILE *zip_mem = fmemopen(decoded, decoded_len, "rb"); + if (!zip_mem) + goto no_file; + + JZFile *zip = jzfile_from_stdio_file(zip_mem); + if (!zip) { + fclose(zip_mem); + goto no_file; + } + + JZEndRecord end_record; + if (jzReadEndRecord(zip, &end_record)) + goto no_end_record; + + site->files = hash_str_new(free, free); + if (!site->files) + goto no_hash; + + if (jzReadCentralDirectory(zip, &end_record, file_cb, site)) + goto no_central_dir; + + jzfile_free(zip); + + return (struct cache_entry *)site; + +no_central_dir: + hash_unref(site->files); +no_hash: +no_end_record: + jzfile_free(zip); +no_file: + free(site); +no_site: + free(decoded); + lwan_strbuf_free(&qr_code); + return NULL; +} + +static void destroy_site(struct cache_entry *entry, void *context) +{ + struct site *site = (struct site *)entry; + lwan_strbuf_free(&site->qr_code); + hash_unref(site->files); + free(site->zipped.value); + free(site); +} + +LWAN_HANDLER_ROUTE(view_root, "/") +{ + char digest_str[41]; + struct site *site; + + if (!request->url.len) { + const struct lwan_key_value redir_headers[] = { + {"Location", smolsite_zip_base64.value}, + {"Cache-Control", "no-cache, max-age=0, private, no-transform"}, + {}, + }; + response->headers = coro_memdup(request->conn->coro, redir_headers, + sizeof(redir_headers)); + return response->headers ? HTTP_TEMPORARY_REDIRECT + : HTTP_INTERNAL_ERROR; + } + + /* Lwan gives us a percent-decoded URL, but '+' is part of the Base64 + * alphabet */ + for (char *p = strchr(request->url.value, ' '); p; p = strchr(p + 1, ' ')) + *p = '+'; + + calc_hash(request->url, digest_str); + + site = (struct site *)cache_coro_get_and_ref_entry_with_ctx( + sites, request->conn->coro, digest_str, &request->url); + if (!site) + return HTTP_INTERNAL_ERROR; + + response->mime_type = "text/html; charset=utf-8"; + + struct iframe_tpl_vars vars = { + .digest = digest_str, + .has_qr_code = site->has_qr_code, + }; + if (!lwan_tpl_apply_with_buffer(iframe_tpl, response->buffer, &vars)) + return HTTP_INTERNAL_ERROR; + + return HTTP_OK; +} + +LWAN_HANDLER_ROUTE(qr_code, "/q/") +{ + struct site *site = (struct site *)cache_coro_get_and_ref_entry( + sites, request->conn->coro, request->url.value); + if (!site) + return HTTP_NOT_FOUND; + if (!site->has_qr_code) + return HTTP_NOT_FOUND; + + lwan_strbuf_set_static(response->buffer, + lwan_strbuf_get_buffer(&site->qr_code), + lwan_strbuf_get_length(&site->qr_code)); + response->mime_type = "image/gif"; + return HTTP_OK; +} + +LWAN_HANDLER_ROUTE(view_site, "/s/") +{ + if (request->url.len < 40) + return HTTP_NOT_FOUND; + + char *slash = memchr(request->url.value, '/', request->url.len); + if (!slash) + return HTTP_NOT_FOUND; + if (slash - request->url.value < 40) + return HTTP_NOT_FOUND; + *slash = '\0'; + if (strcspn(request->url.value, "0123456789abcdefABCDEF")) + return HTTP_NOT_FOUND; + + const char *file_name = slash + 1; + + struct site *site = (struct site *)cache_coro_get_and_ref_entry( + sites, request->conn->coro, request->url.value); + if (!site) + return HTTP_NOT_FOUND; + + if (*file_name == '\0') + file_name = "index.html"; + + struct file *file = hash_find(site->files, file_name); + if (!file) + return HTTP_NOT_FOUND; + + if (file->deflated) { + enum lwan_request_flags accept = + lwan_request_get_accept_encoding(request); + + if (!(accept & REQUEST_ACCEPT_DEFLATE)) + return HTTP_NOT_ACCEPTABLE; + + static const struct lwan_key_value deflate_headers[] = { + {"Content-Encoding", "deflate"}, + {}, + }; + response->headers = deflate_headers; + } + + lwan_strbuf_set_static(response->buffer, + site->zipped.value + file->data_offset, + file->size_compressed); + response->mime_type = file->mime_type; + + return HTTP_OK; +} + +static struct lwan_value base64_encode_to_value(struct lwan_value input) +{ + size_t len; + unsigned char *encoded = + base64_encode((unsigned char *)input.value, input.len, &len); + if (!encoded) + lwan_status_critical("Could not base64-encode smolsite.zip!"); + return (struct lwan_value){.value = (char *)encoded, .len = len}; +} + +int main(void) +{ + struct lwan l; + + lwan_init(&l); + lwan_detect_url_map(&l); + + smolsite_zip_base64 = base64_encode_to_value(smolsite_zip_value); + + iframe_tpl = lwan_tpl_compile_value_full(smolsite_html_value, + iframe_tpl_desc, + LWAN_TPL_FLAG_CONST_TEMPLATE); + if (!iframe_tpl) + lwan_status_critical("Could not compile template"); + + sites = cache_create_full(create_site, destroy_site, hash_str_new, NULL, + CACHE_FOR_MINUTES * 60); + if (!sites) + lwan_status_critical("Could not create site cache"); + + lwan_main_loop(&l); + lwan_shutdown(&l); + cache_destroy(sites); + lwan_tpl_free(iframe_tpl); + free(smolsite_zip_base64.value); + + return 0; +} diff --git a/src/samples/smolsite/qrcodegen.c b/src/samples/smolsite/qrcodegen.c new file mode 100644 index 000000000..e28d6b06a --- /dev/null +++ b/src/samples/smolsite/qrcodegen.c @@ -0,0 +1,1028 @@ +/* + * QR Code generator library (C) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#include +#include +#include +#include +#include "qrcodegen.h" + +#ifndef QRCODEGEN_TEST + #define testable static // Keep functions private +#else + #define testable // Expose private functions +#endif + + +/*---- Forward declarations for private functions ----*/ + +// Regarding all public and private functions defined in this source file: +// - They require all pointer/array arguments to be not null unless the array length is zero. +// - They only read input scalar/array arguments, write to output pointer/array +// arguments, and return scalar values; they are "pure" functions. +// - They don't read mutable global variables or write to any global variables. +// - They don't perform I/O, read the clock, print to console, etc. +// - They allocate a small and constant amount of stack memory. +// - They don't allocate or free any memory on the heap. +// - They don't recurse or mutually recurse. All the code +// could be inlined into the top-level public functions. +// - They run in at most quadratic time with respect to input arguments. +// Most functions run in linear time, and some in constant time. +// There are no unbounded loops or non-obvious termination conditions. +// - They are completely thread-safe if the caller does not give the +// same writable buffer to concurrent calls to these functions. + +testable void appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen); + +testable void addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]); +testable int getNumDataCodewords(int version, enum qrcodegen_Ecc ecl); +testable int getNumRawDataModules(int ver); + +testable void reedSolomonComputeDivisor(int degree, uint8_t result[]); +testable void reedSolomonComputeRemainder(const uint8_t data[], int dataLen, + const uint8_t generator[], int degree, uint8_t result[]); +testable uint8_t reedSolomonMultiply(uint8_t x, uint8_t y); + +testable void initializeFunctionModules(int version, uint8_t qrcode[]); +static void drawLightFunctionModules(uint8_t qrcode[], int version); +static void drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]); +testable int getAlignmentPatternPositions(int version, uint8_t result[7]); +static void fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]); + +static void drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]); +static void applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask); +static long getPenaltyScore(const uint8_t qrcode[]); +static int finderPenaltyCountPatterns(const int runHistory[7], int qrsize); +static int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, int runHistory[7], int qrsize); +static void finderPenaltyAddHistory(int currentRunLength, int runHistory[7], int qrsize); + +testable bool getModuleBounded(const uint8_t qrcode[], int x, int y); +testable void setModuleBounded(uint8_t qrcode[], int x, int y, bool isDark); +testable void setModuleUnbounded(uint8_t qrcode[], int x, int y, bool isDark); +static bool getBit(int x, int i); + +testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars); +testable int getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version); +static int numCharCountBits(enum qrcodegen_Mode mode, int version); + + + +/*---- Private tables of constants ----*/ + +// The set of all legal characters in alphanumeric mode, where each character +// value maps to the index in the string. For checking text and encoding segments. +static const char *ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + +// Sentinel value for use in only some functions. +#define LENGTH_OVERFLOW -1 + +// For generating error correction codes. +testable const int8_t ECC_CODEWORDS_PER_BLOCK[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High +}; + +#define qrcodegen_REED_SOLOMON_DEGREE_MAX 30 // Based on the table above + +// For generating error correction codes. +testable const int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High +}; + +// For automatic mask pattern selection. +static const int PENALTY_N1 = 3; +static const int PENALTY_N2 = 3; +static const int PENALTY_N3 = 40; +static const int PENALTY_N4 = 10; + + + +/*---- High-level QR Code encoding functions ----*/ + +// Public function - see documentation comment in header file. +bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[], + enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl) { + + size_t textLen = strlen(text); + if (textLen == 0) + return qrcodegen_encodeSegmentsAdvanced(NULL, 0, ecl, minVersion, maxVersion, mask, boostEcl, tempBuffer, qrcode); + size_t bufLen = (size_t)qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion); + + struct qrcodegen_Segment seg; + if (qrcodegen_isNumeric(text)) { + if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_NUMERIC, textLen) > bufLen) + goto fail; + seg = qrcodegen_makeNumeric(text, tempBuffer); + } else if (qrcodegen_isAlphanumeric(text)) { + if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_ALPHANUMERIC, textLen) > bufLen) + goto fail; + seg = qrcodegen_makeAlphanumeric(text, tempBuffer); + } else { + if (textLen > bufLen) + goto fail; + for (size_t i = 0; i < textLen; i++) + tempBuffer[i] = (uint8_t)text[i]; + seg.mode = qrcodegen_Mode_BYTE; + seg.bitLength = calcSegmentBitLength(seg.mode, textLen); + if (seg.bitLength == LENGTH_OVERFLOW) + goto fail; + seg.numChars = (int)textLen; + seg.data = tempBuffer; + } + return qrcodegen_encodeSegmentsAdvanced(&seg, 1, ecl, minVersion, maxVersion, mask, boostEcl, tempBuffer, qrcode); + +fail: + qrcode[0] = 0; // Set size to invalid value for safety + return false; +} + + +// Public function - see documentation comment in header file. +bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], size_t dataLen, uint8_t qrcode[], + enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl) { + + struct qrcodegen_Segment seg; + seg.mode = qrcodegen_Mode_BYTE; + seg.bitLength = calcSegmentBitLength(seg.mode, dataLen); + if (seg.bitLength == LENGTH_OVERFLOW) { + qrcode[0] = 0; // Set size to invalid value for safety + return false; + } + seg.numChars = (int)dataLen; + seg.data = dataAndTemp; + return qrcodegen_encodeSegmentsAdvanced(&seg, 1, ecl, minVersion, maxVersion, mask, boostEcl, dataAndTemp, qrcode); +} + + +// Appends the given number of low-order bits of the given value to the given byte-based +// bit buffer, increasing the bit length. Requires 0 <= numBits <= 16 and val < 2^numBits. +testable void appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen) { + assert(0 <= numBits && numBits <= 16 && (unsigned long)val >> numBits == 0); + for (int i = numBits - 1; i >= 0; i--, (*bitLen)++) + buffer[*bitLen >> 3] |= (uint8_t)(((val >> i) & 1) << (7 - (*bitLen & 7))); +} + + + +/*---- Low-level QR Code encoding functions ----*/ + +// Public function - see documentation comment in header file. +bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], size_t len, + enum qrcodegen_Ecc ecl, uint8_t tempBuffer[], uint8_t qrcode[]) { + return qrcodegen_encodeSegmentsAdvanced(segs, len, ecl, + qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true, tempBuffer, qrcode); +} + + +// Public function - see documentation comment in header file. +bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], size_t len, enum qrcodegen_Ecc ecl, + int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl, uint8_t tempBuffer[], uint8_t qrcode[]) { + assert(segs != NULL || len == 0); + assert(qrcodegen_VERSION_MIN <= minVersion && minVersion <= maxVersion && maxVersion <= qrcodegen_VERSION_MAX); + assert(0 <= (int)ecl && (int)ecl <= 3 && -1 <= (int)mask && (int)mask <= 7); + + // Find the minimal version number to use + int version, dataUsedBits; + for (version = minVersion; ; version++) { + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = getTotalBits(segs, len, version); + if (dataUsedBits != LENGTH_OVERFLOW && dataUsedBits <= dataCapacityBits) + break; // This version number is found to be suitable + if (version >= maxVersion) { // All versions in the range could not fit the given data + qrcode[0] = 0; // Set size to invalid value for safety + return false; + } + } + assert(dataUsedBits != LENGTH_OVERFLOW); + + // Increase the error correction level while the data still fits in the current version number + for (int i = (int)qrcodegen_Ecc_MEDIUM; i <= (int)qrcodegen_Ecc_HIGH; i++) { // From low to high + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, (enum qrcodegen_Ecc)i) * 8) + ecl = (enum qrcodegen_Ecc)i; + } + + // Concatenate all segments to create the data bit string + memset(qrcode, 0, (size_t)qrcodegen_BUFFER_LEN_FOR_VERSION(version) * sizeof(qrcode[0])); + int bitLen = 0; + for (size_t i = 0; i < len; i++) { + const struct qrcodegen_Segment *seg = &segs[i]; + appendBitsToBuffer((unsigned int)seg->mode, 4, qrcode, &bitLen); + appendBitsToBuffer((unsigned int)seg->numChars, numCharCountBits(seg->mode, version), qrcode, &bitLen); + for (int j = 0; j < seg->bitLength; j++) { + int bit = (seg->data[j >> 3] >> (7 - (j & 7))) & 1; + appendBitsToBuffer((unsigned int)bit, 1, qrcode, &bitLen); + } + } + assert(bitLen == dataUsedBits); + + // Add terminator and pad up to a byte if applicable + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; + assert(bitLen <= dataCapacityBits); + int terminatorBits = dataCapacityBits - bitLen; + if (terminatorBits > 4) + terminatorBits = 4; + appendBitsToBuffer(0, terminatorBits, qrcode, &bitLen); + appendBitsToBuffer(0, (8 - bitLen % 8) % 8, qrcode, &bitLen); + assert(bitLen % 8 == 0); + + // Pad with alternating bytes until data capacity is reached + for (uint8_t padByte = 0xEC; bitLen < dataCapacityBits; padByte ^= 0xEC ^ 0x11) + appendBitsToBuffer(padByte, 8, qrcode, &bitLen); + + // Compute ECC, draw modules + addEccAndInterleave(qrcode, version, ecl, tempBuffer); + initializeFunctionModules(version, qrcode); + drawCodewords(tempBuffer, getNumRawDataModules(version) / 8, qrcode); + drawLightFunctionModules(qrcode, version); + initializeFunctionModules(version, tempBuffer); + + // Do masking + if (mask == qrcodegen_Mask_AUTO) { // Automatically choose best mask + long minPenalty = LONG_MAX; + for (int i = 0; i < 8; i++) { + enum qrcodegen_Mask msk = (enum qrcodegen_Mask)i; + applyMask(tempBuffer, qrcode, msk); + drawFormatBits(ecl, msk, qrcode); + long penalty = getPenaltyScore(qrcode); + if (penalty < minPenalty) { + mask = msk; + minPenalty = penalty; + } + applyMask(tempBuffer, qrcode, msk); // Undoes the mask due to XOR + } + } + assert(0 <= (int)mask && (int)mask <= 7); + applyMask(tempBuffer, qrcode, mask); // Apply the final choice of mask + drawFormatBits(ecl, mask, qrcode); // Overwrite old format bits + return true; +} + + + +/*---- Error correction code generation functions ----*/ + +// Appends error correction bytes to each block of the given data array, then interleaves +// bytes from the blocks and stores them in the result array. data[0 : dataLen] contains +// the input data. data[dataLen : rawCodewords] is used as a temporary work area and will +// be clobbered by this function. The final answer is stored in result[0 : rawCodewords]. +testable void addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]) { + // Calculate parameter numbers + assert(0 <= (int)ecl && (int)ecl < 4 && qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX); + int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[(int)ecl][version]; + int blockEccLen = ECC_CODEWORDS_PER_BLOCK [(int)ecl][version]; + int rawCodewords = getNumRawDataModules(version) / 8; + int dataLen = getNumDataCodewords(version, ecl); + int numShortBlocks = numBlocks - rawCodewords % numBlocks; + int shortBlockDataLen = rawCodewords / numBlocks - blockEccLen; + + // Split data into blocks, calculate ECC, and interleave + // (not concatenate) the bytes into a single sequence + uint8_t rsdiv[qrcodegen_REED_SOLOMON_DEGREE_MAX]; + reedSolomonComputeDivisor(blockEccLen, rsdiv); + const uint8_t *dat = data; + for (int i = 0; i < numBlocks; i++) { + int datLen = shortBlockDataLen + (i < numShortBlocks ? 0 : 1); + uint8_t *ecc = &data[dataLen]; // Temporary storage + reedSolomonComputeRemainder(dat, datLen, rsdiv, blockEccLen, ecc); + for (int j = 0, k = i; j < datLen; j++, k += numBlocks) { // Copy data + if (j == shortBlockDataLen) + k -= numShortBlocks; + result[k] = dat[j]; + } + for (int j = 0, k = dataLen + i; j < blockEccLen; j++, k += numBlocks) // Copy ECC + result[k] = ecc[j]; + dat += datLen; + } +} + + +// Returns the number of 8-bit codewords that can be used for storing data (not ECC), +// for the given version number and error correction level. The result is in the range [9, 2956]. +testable int getNumDataCodewords(int version, enum qrcodegen_Ecc ecl) { + int v = version, e = (int)ecl; + assert(0 <= e && e < 4); + return getNumRawDataModules(v) / 8 + - ECC_CODEWORDS_PER_BLOCK [e][v] + * NUM_ERROR_CORRECTION_BLOCKS[e][v]; +} + + +// Returns the number of data bits that can be stored in a QR Code of the given version number, after +// all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. +// The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. +testable int getNumRawDataModules(int ver) { + assert(qrcodegen_VERSION_MIN <= ver && ver <= qrcodegen_VERSION_MAX); + int result = (16 * ver + 128) * ver + 64; + if (ver >= 2) { + int numAlign = ver / 7 + 2; + result -= (25 * numAlign - 10) * numAlign - 55; + if (ver >= 7) + result -= 36; + } + assert(208 <= result && result <= 29648); + return result; +} + + + +/*---- Reed-Solomon ECC generator functions ----*/ + +// Computes a Reed-Solomon ECC generator polynomial for the given degree, storing in result[0 : degree]. +// This could be implemented as a lookup table over all possible parameter values, instead of as an algorithm. +testable void reedSolomonComputeDivisor(int degree, uint8_t result[]) { + assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX); + // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. + // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}. + memset(result, 0, (size_t)degree * sizeof(result[0])); + result[degree - 1] = 1; // Start off with the monomial x^0 + + // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), + // drop the highest monomial term which is always 1x^degree. + // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). + uint8_t root = 1; + for (int i = 0; i < degree; i++) { + // Multiply the current product by (x - r^i) + for (int j = 0; j < degree; j++) { + result[j] = reedSolomonMultiply(result[j], root); + if (j + 1 < degree) + result[j] ^= result[j + 1]; + } + root = reedSolomonMultiply(root, 0x02); + } +} + + +// Computes the Reed-Solomon error correction codeword for the given data and divisor polynomials. +// The remainder when data[0 : dataLen] is divided by divisor[0 : degree] is stored in result[0 : degree]. +// All polynomials are in big endian, and the generator has an implicit leading 1 term. +testable void reedSolomonComputeRemainder(const uint8_t data[], int dataLen, + const uint8_t generator[], int degree, uint8_t result[]) { + assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX); + memset(result, 0, (size_t)degree * sizeof(result[0])); + for (int i = 0; i < dataLen; i++) { // Polynomial division + uint8_t factor = data[i] ^ result[0]; + memmove(&result[0], &result[1], (size_t)(degree - 1) * sizeof(result[0])); + result[degree - 1] = 0; + for (int j = 0; j < degree; j++) + result[j] ^= reedSolomonMultiply(generator[j], factor); + } +} + +#undef qrcodegen_REED_SOLOMON_DEGREE_MAX + + +// Returns the product of the two given field elements modulo GF(2^8/0x11D). +// All inputs are valid. This could be implemented as a 256*256 lookup table. +testable uint8_t reedSolomonMultiply(uint8_t x, uint8_t y) { + // Russian peasant multiplication + uint8_t z = 0; + for (int i = 7; i >= 0; i--) { + z = (uint8_t)((z << 1) ^ ((z >> 7) * 0x11D)); + z ^= (uint8_t)(((y >> i) & 1) * x); + } + return z; +} + + + +/*---- Drawing function modules ----*/ + +// Clears the given QR Code grid with light modules for the given +// version's size, then marks every function module as dark. +testable void initializeFunctionModules(int version, uint8_t qrcode[]) { + // Initialize QR Code + int qrsize = version * 4 + 17; + memset(qrcode, 0, (size_t)((qrsize * qrsize + 7) / 8 + 1) * sizeof(qrcode[0])); + qrcode[0] = (uint8_t)qrsize; + + // Fill horizontal and vertical timing patterns + fillRectangle(6, 0, 1, qrsize, qrcode); + fillRectangle(0, 6, qrsize, 1, qrcode); + + // Fill 3 finder patterns (all corners except bottom right) and format bits + fillRectangle(0, 0, 9, 9, qrcode); + fillRectangle(qrsize - 8, 0, 8, 9, qrcode); + fillRectangle(0, qrsize - 8, 9, 8, qrcode); + + // Fill numerous alignment patterns + uint8_t alignPatPos[7]; + int numAlign = getAlignmentPatternPositions(version, alignPatPos); + for (int i = 0; i < numAlign; i++) { + for (int j = 0; j < numAlign; j++) { + // Don't draw on the three finder corners + if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))) + fillRectangle(alignPatPos[i] - 2, alignPatPos[j] - 2, 5, 5, qrcode); + } + } + + // Fill version blocks + if (version >= 7) { + fillRectangle(qrsize - 11, 0, 3, 6, qrcode); + fillRectangle(0, qrsize - 11, 6, 3, qrcode); + } +} + + +// Draws light function modules and possibly some dark modules onto the given QR Code, without changing +// non-function modules. This does not draw the format bits. This requires all function modules to be previously +// marked dark (namely by initializeFunctionModules()), because this may skip redrawing dark function modules. +static void drawLightFunctionModules(uint8_t qrcode[], int version) { + // Draw horizontal and vertical timing patterns + int qrsize = qrcodegen_getSize(qrcode); + for (int i = 7; i < qrsize - 7; i += 2) { + setModuleBounded(qrcode, 6, i, false); + setModuleBounded(qrcode, i, 6, false); + } + + // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) + for (int dy = -4; dy <= 4; dy++) { + for (int dx = -4; dx <= 4; dx++) { + int dist = abs(dx); + if (abs(dy) > dist) + dist = abs(dy); + if (dist == 2 || dist == 4) { + setModuleUnbounded(qrcode, 3 + dx, 3 + dy, false); + setModuleUnbounded(qrcode, qrsize - 4 + dx, 3 + dy, false); + setModuleUnbounded(qrcode, 3 + dx, qrsize - 4 + dy, false); + } + } + } + + // Draw numerous alignment patterns + uint8_t alignPatPos[7]; + int numAlign = getAlignmentPatternPositions(version, alignPatPos); + for (int i = 0; i < numAlign; i++) { + for (int j = 0; j < numAlign; j++) { + if ((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0)) + continue; // Don't draw on the three finder corners + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) + setModuleBounded(qrcode, alignPatPos[i] + dx, alignPatPos[j] + dy, dx == 0 && dy == 0); + } + } + } + + // Draw version blocks + if (version >= 7) { + // Calculate error correction code and pack bits + int rem = version; // version is uint6, in the range [7, 40] + for (int i = 0; i < 12; i++) + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + long bits = (long)version << 12 | rem; // uint18 + assert(bits >> 18 == 0); + + // Draw two copies + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 3; j++) { + int k = qrsize - 11 + j; + setModuleBounded(qrcode, k, i, (bits & 1) != 0); + setModuleBounded(qrcode, i, k, (bits & 1) != 0); + bits >>= 1; + } + } + } +} + + +// Draws two copies of the format bits (with its own error correction code) based +// on the given mask and error correction level. This always draws all modules of +// the format bits, unlike drawLightFunctionModules() which might skip dark modules. +static void drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]) { + // Calculate error correction code and pack bits + assert(0 <= (int)mask && (int)mask <= 7); + static const int table[] = {1, 0, 3, 2}; + int data = table[(int)ecl] << 3 | (int)mask; // errCorrLvl is uint2, mask is uint3 + int rem = data; + for (int i = 0; i < 10; i++) + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + int bits = (data << 10 | rem) ^ 0x5412; // uint15 + assert(bits >> 15 == 0); + + // Draw first copy + for (int i = 0; i <= 5; i++) + setModuleBounded(qrcode, 8, i, getBit(bits, i)); + setModuleBounded(qrcode, 8, 7, getBit(bits, 6)); + setModuleBounded(qrcode, 8, 8, getBit(bits, 7)); + setModuleBounded(qrcode, 7, 8, getBit(bits, 8)); + for (int i = 9; i < 15; i++) + setModuleBounded(qrcode, 14 - i, 8, getBit(bits, i)); + + // Draw second copy + int qrsize = qrcodegen_getSize(qrcode); + for (int i = 0; i < 8; i++) + setModuleBounded(qrcode, qrsize - 1 - i, 8, getBit(bits, i)); + for (int i = 8; i < 15; i++) + setModuleBounded(qrcode, 8, qrsize - 15 + i, getBit(bits, i)); + setModuleBounded(qrcode, 8, qrsize - 8, true); // Always dark +} + + +// Calculates and stores an ascending list of positions of alignment patterns +// for this version number, returning the length of the list (in the range [0,7]). +// Each position is in the range [0,177), and are used on both the x and y axes. +// This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. +testable int getAlignmentPatternPositions(int version, uint8_t result[7]) { + if (version == 1) + return 0; + int numAlign = version / 7 + 2; + int step = (version == 32) ? 26 : + (version * 4 + numAlign * 2 + 1) / (numAlign * 2 - 2) * 2; + for (int i = numAlign - 1, pos = version * 4 + 10; i >= 1; i--, pos -= step) + result[i] = (uint8_t)pos; + result[0] = 6; + return numAlign; +} + + +// Sets every module in the range [left : left + width] * [top : top + height] to dark. +static void fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]) { + for (int dy = 0; dy < height; dy++) { + for (int dx = 0; dx < width; dx++) + setModuleBounded(qrcode, left + dx, top + dy, true); + } +} + + + +/*---- Drawing data modules and masking ----*/ + +// Draws the raw codewords (including data and ECC) onto the given QR Code. This requires the initial state of +// the QR Code to be dark at function modules and light at codeword modules (including unused remainder bits). +static void drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]) { + int qrsize = qrcodegen_getSize(qrcode); + int i = 0; // Bit index into the data + // Do the funny zigzag scan + for (int right = qrsize - 1; right >= 1; right -= 2) { // Index of right column in each column pair + if (right == 6) + right = 5; + for (int vert = 0; vert < qrsize; vert++) { // Vertical counter + for (int j = 0; j < 2; j++) { + int x = right - j; // Actual x coordinate + bool upward = ((right + 1) & 2) == 0; + int y = upward ? qrsize - 1 - vert : vert; // Actual y coordinate + if (!getModuleBounded(qrcode, x, y) && i < dataLen * 8) { + bool dark = getBit(data[i >> 3], 7 - (i & 7)); + setModuleBounded(qrcode, x, y, dark); + i++; + } + // If this QR Code has any remainder bits (0 to 7), they were assigned as + // 0/false/light by the constructor and are left unchanged by this method + } + } + } + assert(i == dataLen * 8); +} + + +// XORs the codeword modules in this QR Code with the given mask pattern +// and given pattern of function modules. The codeword bits must be drawn +// before masking. Due to the arithmetic of XOR, calling applyMask() with +// the same mask value a second time will undo the mask. A final well-formed +// QR Code needs exactly one (not zero, two, etc.) mask applied. +static void applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask) { + assert(0 <= (int)mask && (int)mask <= 7); // Disallows qrcodegen_Mask_AUTO + int qrsize = qrcodegen_getSize(qrcode); + for (int y = 0; y < qrsize; y++) { + for (int x = 0; x < qrsize; x++) { + if (getModuleBounded(functionModules, x, y)) + continue; + bool invert; + switch ((int)mask) { + case 0: invert = (x + y) % 2 == 0; break; + case 1: invert = y % 2 == 0; break; + case 2: invert = x % 3 == 0; break; + case 3: invert = (x + y) % 3 == 0; break; + case 4: invert = (x / 3 + y / 2) % 2 == 0; break; + case 5: invert = x * y % 2 + x * y % 3 == 0; break; + case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; + case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; + default: assert(false); return; + } + bool val = getModuleBounded(qrcode, x, y); + setModuleBounded(qrcode, x, y, val ^ invert); + } + } +} + + +// Calculates and returns the penalty score based on state of the given QR Code's current modules. +// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. +static long getPenaltyScore(const uint8_t qrcode[]) { + int qrsize = qrcodegen_getSize(qrcode); + long result = 0; + + // Adjacent modules in row having same color, and finder-like patterns + for (int y = 0; y < qrsize; y++) { + bool runColor = false; + int runX = 0; + int runHistory[7] = {0}; + for (int x = 0; x < qrsize; x++) { + if (getModuleBounded(qrcode, x, y) == runColor) { + runX++; + if (runX == 5) + result += PENALTY_N1; + else if (runX > 5) + result++; + } else { + finderPenaltyAddHistory(runX, runHistory, qrsize); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory, qrsize) * PENALTY_N3; + runColor = getModuleBounded(qrcode, x, y); + runX = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runX, runHistory, qrsize) * PENALTY_N3; + } + // Adjacent modules in column having same color, and finder-like patterns + for (int x = 0; x < qrsize; x++) { + bool runColor = false; + int runY = 0; + int runHistory[7] = {0}; + for (int y = 0; y < qrsize; y++) { + if (getModuleBounded(qrcode, x, y) == runColor) { + runY++; + if (runY == 5) + result += PENALTY_N1; + else if (runY > 5) + result++; + } else { + finderPenaltyAddHistory(runY, runHistory, qrsize); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory, qrsize) * PENALTY_N3; + runColor = getModuleBounded(qrcode, x, y); + runY = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runY, runHistory, qrsize) * PENALTY_N3; + } + + // 2*2 blocks of modules having same color + for (int y = 0; y < qrsize - 1; y++) { + for (int x = 0; x < qrsize - 1; x++) { + bool color = getModuleBounded(qrcode, x, y); + if ( color == getModuleBounded(qrcode, x + 1, y) && + color == getModuleBounded(qrcode, x, y + 1) && + color == getModuleBounded(qrcode, x + 1, y + 1)) + result += PENALTY_N2; + } + } + + // Balance of dark and light modules + int dark = 0; + for (int y = 0; y < qrsize; y++) { + for (int x = 0; x < qrsize; x++) { + if (getModuleBounded(qrcode, x, y)) + dark++; + } + } + int total = qrsize * qrsize; // Note that size is odd, so dark/total != 1/2 + // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)% + int k = (int)((labs(dark * 20L - total * 10L) + total - 1) / total) - 1; + assert(0 <= k && k <= 9); + result += k * PENALTY_N4; + assert(0 <= result && result <= 2568888L); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4 + return result; +} + + +// Can only be called immediately after a light run is added, and +// returns either 0, 1, or 2. A helper function for getPenaltyScore(). +static int finderPenaltyCountPatterns(const int runHistory[7], int qrsize) { + int n = runHistory[1]; + assert(n <= qrsize * 3); (void)qrsize; + bool core = n > 0 && runHistory[2] == n && runHistory[3] == n * 3 && runHistory[4] == n && runHistory[5] == n; + // The maximum QR Code size is 177, hence the dark run length n <= 177. + // Arithmetic is promoted to int, so n*4 will not overflow. + return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0); +} + + +// Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore(). +static int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, int runHistory[7], int qrsize) { + if (currentRunColor) { // Terminate dark run + finderPenaltyAddHistory(currentRunLength, runHistory, qrsize); + currentRunLength = 0; + } + currentRunLength += qrsize; // Add light border to final run + finderPenaltyAddHistory(currentRunLength, runHistory, qrsize); + return finderPenaltyCountPatterns(runHistory, qrsize); +} + + +// Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore(). +static void finderPenaltyAddHistory(int currentRunLength, int runHistory[7], int qrsize) { + if (runHistory[0] == 0) + currentRunLength += qrsize; // Add light border to initial run + memmove(&runHistory[1], &runHistory[0], 6 * sizeof(runHistory[0])); + runHistory[0] = currentRunLength; +} + + + +/*---- Basic QR Code information ----*/ + +// Public function - see documentation comment in header file. +int qrcodegen_getSize(const uint8_t qrcode[]) { + assert(qrcode != NULL); + int result = qrcode[0]; + assert((qrcodegen_VERSION_MIN * 4 + 17) <= result + && result <= (qrcodegen_VERSION_MAX * 4 + 17)); + return result; +} + + +// Public function - see documentation comment in header file. +bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y) { + assert(qrcode != NULL); + int qrsize = qrcode[0]; + return (0 <= x && x < qrsize && 0 <= y && y < qrsize) && getModuleBounded(qrcode, x, y); +} + + +// Returns the color of the module at the given coordinates, which must be in bounds. +testable bool getModuleBounded(const uint8_t qrcode[], int x, int y) { + int qrsize = qrcode[0]; + assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize); + int index = y * qrsize + x; + return getBit(qrcode[(index >> 3) + 1], index & 7); +} + + +// Sets the color of the module at the given coordinates, which must be in bounds. +testable void setModuleBounded(uint8_t qrcode[], int x, int y, bool isDark) { + int qrsize = qrcode[0]; + assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize); + int index = y * qrsize + x; + int bitIndex = index & 7; + int byteIndex = (index >> 3) + 1; + if (isDark) + qrcode[byteIndex] |= (uint8_t)(1 << bitIndex); + else + qrcode[byteIndex] &= (uint8_t)((1 << bitIndex) ^ 0xFF); +} + + +// Sets the color of the module at the given coordinates, doing nothing if out of bounds. +testable void setModuleUnbounded(uint8_t qrcode[], int x, int y, bool isDark) { + int qrsize = qrcode[0]; + if (0 <= x && x < qrsize && 0 <= y && y < qrsize) + setModuleBounded(qrcode, x, y, isDark); +} + + +// Returns true iff the i'th bit of x is set to 1. Requires x >= 0 and 0 <= i <= 14. +static bool getBit(int x, int i) { + return ((x >> i) & 1) != 0; +} + + + +/*---- Segment handling ----*/ + +// Public function - see documentation comment in header file. +bool qrcodegen_isNumeric(const char *text) { + assert(text != NULL); + for (; *text != '\0'; text++) { + if (*text < '0' || *text > '9') + return false; + } + return true; +} + + +// Public function - see documentation comment in header file. +bool qrcodegen_isAlphanumeric(const char *text) { + assert(text != NULL); + for (; *text != '\0'; text++) { + if (strchr(ALPHANUMERIC_CHARSET, *text) == NULL) + return false; + } + return true; +} + + +// Public function - see documentation comment in header file. +size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars) { + int temp = calcSegmentBitLength(mode, numChars); + if (temp == LENGTH_OVERFLOW) + return SIZE_MAX; + assert(0 <= temp && temp <= INT16_MAX); + return ((size_t)temp + 7) / 8; +} + + +// Returns the number of data bits needed to represent a segment +// containing the given number of characters using the given mode. Notes: +// - Returns LENGTH_OVERFLOW on failure, i.e. numChars > INT16_MAX +// or the number of needed bits exceeds INT16_MAX (i.e. 32767). +// - Otherwise, all valid results are in the range [0, INT16_MAX]. +// - For byte mode, numChars measures the number of bytes, not Unicode code points. +// - For ECI mode, numChars must be 0, and the worst-case number of bits is returned. +// An actual ECI segment can have shorter data. For non-ECI modes, the result is exact. +testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars) { + // All calculations are designed to avoid overflow on all platforms + if (numChars > (unsigned int)INT16_MAX) + return LENGTH_OVERFLOW; + long result = (long)numChars; + if (mode == qrcodegen_Mode_NUMERIC) + result = (result * 10 + 2) / 3; // ceil(10/3 * n) + else if (mode == qrcodegen_Mode_ALPHANUMERIC) + result = (result * 11 + 1) / 2; // ceil(11/2 * n) + else if (mode == qrcodegen_Mode_BYTE) + result *= 8; + else if (mode == qrcodegen_Mode_KANJI) + result *= 13; + else if (mode == qrcodegen_Mode_ECI && numChars == 0) + result = 3 * 8; + else { // Invalid argument + assert(false); + return LENGTH_OVERFLOW; + } + assert(result >= 0); + if (result > INT16_MAX) + return LENGTH_OVERFLOW; + return (int)result; +} + + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]) { + assert(data != NULL || len == 0); + struct qrcodegen_Segment result; + result.mode = qrcodegen_Mode_BYTE; + result.bitLength = calcSegmentBitLength(result.mode, len); + assert(result.bitLength != LENGTH_OVERFLOW); + result.numChars = (int)len; + if (len > 0) + memcpy(buf, data, len * sizeof(buf[0])); + result.data = buf; + return result; +} + + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]) { + assert(digits != NULL); + struct qrcodegen_Segment result; + size_t len = strlen(digits); + result.mode = qrcodegen_Mode_NUMERIC; + int bitLen = calcSegmentBitLength(result.mode, len); + assert(bitLen != LENGTH_OVERFLOW); + result.numChars = (int)len; + if (bitLen > 0) + memset(buf, 0, ((size_t)bitLen + 7) / 8 * sizeof(buf[0])); + result.bitLength = 0; + + unsigned int accumData = 0; + int accumCount = 0; + for (; *digits != '\0'; digits++) { + char c = *digits; + assert('0' <= c && c <= '9'); + accumData = accumData * 10 + (unsigned int)(c - '0'); + accumCount++; + if (accumCount == 3) { + appendBitsToBuffer(accumData, 10, buf, &result.bitLength); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 or 2 digits remaining + appendBitsToBuffer(accumData, accumCount * 3 + 1, buf, &result.bitLength); + assert(result.bitLength == bitLen); + result.data = buf; + return result; +} + + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]) { + assert(text != NULL); + struct qrcodegen_Segment result; + size_t len = strlen(text); + result.mode = qrcodegen_Mode_ALPHANUMERIC; + int bitLen = calcSegmentBitLength(result.mode, len); + assert(bitLen != LENGTH_OVERFLOW); + result.numChars = (int)len; + if (bitLen > 0) + memset(buf, 0, ((size_t)bitLen + 7) / 8 * sizeof(buf[0])); + result.bitLength = 0; + + unsigned int accumData = 0; + int accumCount = 0; + for (; *text != '\0'; text++) { + const char *temp = strchr(ALPHANUMERIC_CHARSET, *text); + assert(temp != NULL); + accumData = accumData * 45 + (unsigned int)(temp - ALPHANUMERIC_CHARSET); + accumCount++; + if (accumCount == 2) { + appendBitsToBuffer(accumData, 11, buf, &result.bitLength); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 character remaining + appendBitsToBuffer(accumData, 6, buf, &result.bitLength); + assert(result.bitLength == bitLen); + result.data = buf; + return result; +} + + +// Public function - see documentation comment in header file. +struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]) { + struct qrcodegen_Segment result; + result.mode = qrcodegen_Mode_ECI; + result.numChars = 0; + result.bitLength = 0; + if (assignVal < 0) + assert(false); + else if (assignVal < (1 << 7)) { + memset(buf, 0, 1 * sizeof(buf[0])); + appendBitsToBuffer((unsigned int)assignVal, 8, buf, &result.bitLength); + } else if (assignVal < (1 << 14)) { + memset(buf, 0, 2 * sizeof(buf[0])); + appendBitsToBuffer(2, 2, buf, &result.bitLength); + appendBitsToBuffer((unsigned int)assignVal, 14, buf, &result.bitLength); + } else if (assignVal < 1000000L) { + memset(buf, 0, 3 * sizeof(buf[0])); + appendBitsToBuffer(6, 3, buf, &result.bitLength); + appendBitsToBuffer((unsigned int)(assignVal >> 10), 11, buf, &result.bitLength); + appendBitsToBuffer((unsigned int)(assignVal & 0x3FF), 10, buf, &result.bitLength); + } else + assert(false); + result.data = buf; + return result; +} + + +// Calculates the number of bits needed to encode the given segments at the given version. +// Returns a non-negative number if successful. Otherwise returns LENGTH_OVERFLOW if a segment +// has too many characters to fit its length field, or the total bits exceeds INT16_MAX. +testable int getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version) { + assert(segs != NULL || len == 0); + long result = 0; + for (size_t i = 0; i < len; i++) { + int numChars = segs[i].numChars; + int bitLength = segs[i].bitLength; + assert(0 <= numChars && numChars <= INT16_MAX); + assert(0 <= bitLength && bitLength <= INT16_MAX); + int ccbits = numCharCountBits(segs[i].mode, version); + assert(0 <= ccbits && ccbits <= 16); + if (numChars >= (1L << ccbits)) + return LENGTH_OVERFLOW; // The segment's length doesn't fit the field's bit width + result += 4L + ccbits + bitLength; + if (result > INT16_MAX) + return LENGTH_OVERFLOW; // The sum might overflow an int type + } + assert(0 <= result && result <= INT16_MAX); + return (int)result; +} + + +// Returns the bit width of the character count field for a segment in the given mode +// in a QR Code at the given version number. The result is in the range [0, 16]. +static int numCharCountBits(enum qrcodegen_Mode mode, int version) { + assert(qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX); + int i = (version + 7) / 17; + switch (mode) { + case qrcodegen_Mode_NUMERIC : { static const int temp[] = {10, 12, 14}; return temp[i]; } + case qrcodegen_Mode_ALPHANUMERIC: { static const int temp[] = { 9, 11, 13}; return temp[i]; } + case qrcodegen_Mode_BYTE : { static const int temp[] = { 8, 16, 16}; return temp[i]; } + case qrcodegen_Mode_KANJI : { static const int temp[] = { 8, 10, 12}; return temp[i]; } + case qrcodegen_Mode_ECI : return 0; + default: assert(false); return -1; // Dummy value + } +} + + +#undef LENGTH_OVERFLOW diff --git a/src/samples/smolsite/qrcodegen.h b/src/samples/smolsite/qrcodegen.h new file mode 100644 index 000000000..6bbc15762 --- /dev/null +++ b/src/samples/smolsite/qrcodegen.h @@ -0,0 +1,385 @@ +/* + * QR Code generator library (C) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#pragma once + +#include +#include +#include + + +#ifdef __cplusplus +extern "C" { +#endif + + +/* + * This library creates QR Code symbols, which is a type of two-dimension barcode. + * Invented by Denso Wave and described in the ISO/IEC 18004 standard. + * A QR Code structure is an immutable square grid of dark and light cells. + * The library provides functions to create a QR Code from text or binary data. + * The library covers the QR Code Model 2 specification, supporting all versions (sizes) + * from 1 to 40, all 4 error correction levels, and 4 character encoding modes. + * + * Ways to create a QR Code object: + * - High level: Take the payload data and call qrcodegen_encodeText() or qrcodegen_encodeBinary(). + * - Low level: Custom-make the list of segments and call + * qrcodegen_encodeSegments() or qrcodegen_encodeSegmentsAdvanced(). + * (Note that all ways require supplying the desired error correction level and various byte buffers.) + */ + + +/*---- Enum and struct types----*/ + +/* + * The error correction level in a QR Code symbol. + */ +enum qrcodegen_Ecc { + // Must be declared in ascending order of error protection + // so that an internal qrcodegen function works properly + qrcodegen_Ecc_LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords + qrcodegen_Ecc_MEDIUM , // The QR Code can tolerate about 15% erroneous codewords + qrcodegen_Ecc_QUARTILE, // The QR Code can tolerate about 25% erroneous codewords + qrcodegen_Ecc_HIGH , // The QR Code can tolerate about 30% erroneous codewords +}; + + +/* + * The mask pattern used in a QR Code symbol. + */ +enum qrcodegen_Mask { + // A special value to tell the QR Code encoder to + // automatically select an appropriate mask pattern + qrcodegen_Mask_AUTO = -1, + // The eight actual mask patterns + qrcodegen_Mask_0 = 0, + qrcodegen_Mask_1, + qrcodegen_Mask_2, + qrcodegen_Mask_3, + qrcodegen_Mask_4, + qrcodegen_Mask_5, + qrcodegen_Mask_6, + qrcodegen_Mask_7, +}; + + +/* + * Describes how a segment's data bits are interpreted. + */ +enum qrcodegen_Mode { + qrcodegen_Mode_NUMERIC = 0x1, + qrcodegen_Mode_ALPHANUMERIC = 0x2, + qrcodegen_Mode_BYTE = 0x4, + qrcodegen_Mode_KANJI = 0x8, + qrcodegen_Mode_ECI = 0x7, +}; + + +/* + * A segment of character/binary/control data in a QR Code symbol. + * The mid-level way to create a segment is to take the payload data + * and call a factory function such as qrcodegen_makeNumeric(). + * The low-level way to create a segment is to custom-make the bit buffer + * and initialize a qrcodegen_Segment struct with appropriate values. + * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. + * Any segment longer than this is meaningless for the purpose of generating QR Codes. + * Moreover, the maximum allowed bit length is 32767 because + * the largest QR Code (version 40) has 31329 modules. + */ +struct qrcodegen_Segment { + // The mode indicator of this segment. + enum qrcodegen_Mode mode; + + // The length of this segment's unencoded data. Measured in characters for + // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. + // Always zero or positive. Not the same as the data's bit length. + int numChars; + + // The data bits of this segment, packed in bitwise big endian. + // Can be null if the bit length is zero. + uint8_t *data; + + // The number of valid data bits used in the buffer. Requires + // 0 <= bitLength <= 32767, and bitLength <= (capacity of data array) * 8. + // The character count (numChars) must agree with the mode and the bit buffer length. + int bitLength; +}; + + + +/*---- Macro constants and functions ----*/ + +#define qrcodegen_VERSION_MIN 1 // The minimum version number supported in the QR Code Model 2 standard +#define qrcodegen_VERSION_MAX 40 // The maximum version number supported in the QR Code Model 2 standard + +// Calculates the number of bytes needed to store any QR Code up to and including the given version number, +// as a compile-time constant. For example, 'uint8_t buffer[qrcodegen_BUFFER_LEN_FOR_VERSION(25)];' +// can store any single QR Code from version 1 to 25 (inclusive). The result fits in an int (or int16). +// Requires qrcodegen_VERSION_MIN <= n <= qrcodegen_VERSION_MAX. +#define qrcodegen_BUFFER_LEN_FOR_VERSION(n) ((((n) * 4 + 17) * ((n) * 4 + 17) + 7) / 8 + 1) + +// The worst-case number of bytes needed to store one QR Code, up to and including +// version 40. This value equals 3918, which is just under 4 kilobytes. +// Use this more convenient value to avoid calculating tighter memory bounds for buffers. +#define qrcodegen_BUFFER_LEN_MAX qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX) + + + +/*---- Functions (high level) to generate QR Codes ----*/ + +/* + * Encodes the given text string to a QR Code, returning true if successful. + * If the data is too long to fit in any version in the given range + * at the given ECC level, then false is returned. + * + * The input text must be encoded in UTF-8 and contain no NULs. + * Requires 1 <= minVersion <= maxVersion <= 40. + * + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or + * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow). + * + * About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion): + * - Before calling the function: + * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow + * reading and writing; hence each array must have a length of at least len. + * - The two ranges must not overlap (aliasing). + * - The initial state of both ranges can be uninitialized + * because the function always writes before reading. + * - After the function returns: + * - Both ranges have no guarantee on which elements are initialized and what values are stored. + * - tempBuffer contains no useful data and should be treated as entirely uninitialized. + * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule(). + * + * If successful, the resulting QR Code may use numeric, + * alphanumeric, or byte mode to encode the text. + * + * In the most optimistic case, a QR Code at version 40 with low ECC + * can hold any UTF-8 string up to 2953 bytes, or any alphanumeric string + * up to 4296 characters, or any digit string up to 7089 characters. + * These numbers represent the hard upper limit of the QR Code standard. + * + * Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + */ +bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[], + enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl); + + +/* + * Encodes the given binary data to a QR Code, returning true if successful. + * If the data is too long to fit in any version in the given range + * at the given ECC level, then false is returned. + * + * Requires 1 <= minVersion <= maxVersion <= 40. + * + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or + * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow). + * + * About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion): + * - Before calling the function: + * - The array ranges dataAndTemp[0 : len] and qrcode[0 : len] must allow + * reading and writing; hence each array must have a length of at least len. + * - The two ranges must not overlap (aliasing). + * - The input array range dataAndTemp[0 : dataLen] should normally be + * valid UTF-8 text, but is not required by the QR Code standard. + * - The initial state of dataAndTemp[dataLen : len] and qrcode[0 : len] + * can be uninitialized because the function always writes before reading. + * - After the function returns: + * - Both ranges have no guarantee on which elements are initialized and what values are stored. + * - dataAndTemp contains no useful data and should be treated as entirely uninitialized. + * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule(). + * + * If successful, the resulting QR Code will use byte mode to encode the data. + * + * In the most optimistic case, a QR Code at version 40 with low ECC can hold any byte + * sequence up to length 2953. This is the hard upper limit of the QR Code standard. + * + * Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + */ +bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], size_t dataLen, uint8_t qrcode[], + enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl); + + +/*---- Functions (low level) to generate QR Codes ----*/ + +/* + * Encodes the given segments to a QR Code, returning true if successful. + * If the data is too long to fit in any version at the given ECC level, + * then false is returned. + * + * The smallest possible QR Code version is automatically chosen for + * the output. The ECC level of the result may be higher than the + * ecl argument if it can be done without increasing the version. + * + * About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX): + * - Before calling the function: + * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow + * reading and writing; hence each array must have a length of at least len. + * - The two ranges must not overlap (aliasing). + * - The initial state of both ranges can be uninitialized + * because the function always writes before reading. + * - The input array segs can contain segments whose data buffers overlap with tempBuffer. + * - After the function returns: + * - Both ranges have no guarantee on which elements are initialized and what values are stored. + * - tempBuffer contains no useful data and should be treated as entirely uninitialized. + * - Any segment whose data buffer overlaps with tempBuffer[0 : len] + * must be treated as having invalid values in that array. + * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule(). + * + * Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + * + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary(). + */ +bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], size_t len, + enum qrcodegen_Ecc ecl, uint8_t tempBuffer[], uint8_t qrcode[]); + + +/* + * Encodes the given segments to a QR Code, returning true if successful. + * If the data is too long to fit in any version in the given range + * at the given ECC level, then false is returned. + * + * Requires 1 <= minVersion <= maxVersion <= 40. + * + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or + * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow). + * + * About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX): + * - Before calling the function: + * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow + * reading and writing; hence each array must have a length of at least len. + * - The two ranges must not overlap (aliasing). + * - The initial state of both ranges can be uninitialized + * because the function always writes before reading. + * - The input array segs can contain segments whose data buffers overlap with tempBuffer. + * - After the function returns: + * - Both ranges have no guarantee on which elements are initialized and what values are stored. + * - tempBuffer contains no useful data and should be treated as entirely uninitialized. + * - Any segment whose data buffer overlaps with tempBuffer[0 : len] + * must be treated as having invalid values in that array. + * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule(). + * + * Please consult the QR Code specification for information on + * data capacities per version, ECC level, and text encoding mode. + * + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary(). + */ +bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], size_t len, enum qrcodegen_Ecc ecl, + int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl, uint8_t tempBuffer[], uint8_t qrcode[]); + + +/* + * Tests whether the given string can be encoded as a segment in numeric mode. + * A string is encodable iff each character is in the range 0 to 9. + */ +bool qrcodegen_isNumeric(const char *text); + + +/* + * Tests whether the given string can be encoded as a segment in alphanumeric mode. + * A string is encodable iff each character is in the following set: 0 to 9, A to Z + * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ +bool qrcodegen_isAlphanumeric(const char *text); + + +/* + * Returns the number of bytes (uint8_t) needed for the data buffer of a segment + * containing the given number of characters using the given mode. Notes: + * - Returns SIZE_MAX on failure, i.e. numChars > INT16_MAX or the internal + * calculation of the number of needed bits exceeds INT16_MAX (i.e. 32767). + * - Otherwise, all valid results are in the range [0, ceil(INT16_MAX / 8)], i.e. at most 4096. + * - It is okay for the user to allocate more bytes for the buffer than needed. + * - For byte mode, numChars measures the number of bytes, not Unicode code points. + * - For ECI mode, numChars must be 0, and the worst-case number of bytes is returned. + * An actual ECI segment can have shorter data. For non-ECI modes, the result is exact. + */ +size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars); + + +/* + * Returns a segment representing the given binary data encoded in + * byte mode. All input byte arrays are acceptable. Any text string + * can be converted to UTF-8 bytes and encoded as a byte mode segment. + */ +struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]); + + +/* + * Returns a segment representing the given string of decimal digits encoded in numeric mode. + */ +struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]); + + +/* + * Returns a segment representing the given text string encoded in alphanumeric mode. + * The characters allowed are: 0 to 9, A to Z (uppercase only), space, + * dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ +struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]); + + +/* + * Returns a segment representing an Extended Channel Interpretation + * (ECI) designator with the given assignment value. + */ +struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]); + + +/*---- Functions to extract raw data from QR Codes ----*/ + +/* + * Returns the side length of the given QR Code, assuming that encoding succeeded. + * The result is in the range [21, 177]. Note that the length of the array buffer + * is related to the side length - every 'uint8_t qrcode[]' must have length at least + * qrcodegen_BUFFER_LEN_FOR_VERSION(version), which equals ceil(size^2 / 8 + 1). + */ +int qrcodegen_getSize(const uint8_t qrcode[]); + + +/* + * Returns the color of the module (pixel) at the given coordinates, which is false + * for light or true for dark. The top left corner has the coordinates (x=0, y=0). + * If the given coordinates are out of bounds, then false (light) is returned. + */ +bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y); + + +#ifdef __cplusplus +} +#endif diff --git a/src/samples/smolsite/smolsite.html b/src/samples/smolsite/smolsite.html new file mode 100644 index 000000000..011131f84 --- /dev/null +++ b/src/samples/smolsite/smolsite.html @@ -0,0 +1,109 @@ + + + 🗜 omg it's a smolsite + + + + + +{{has_qr_code?}} +
+ QR Code +{{/has_qr_code?}} + +

The address of this site contains the whole website! Learn how it works on 🗜️smolsite.zip.

+ + + \ No newline at end of file diff --git a/src/samples/techempower/CMakeLists.txt b/src/samples/techempower/CMakeLists.txt new file mode 100644 index 000000000..d461489ec --- /dev/null +++ b/src/samples/techempower/CMakeLists.txt @@ -0,0 +1,30 @@ +include(FindPkgConfig) +pkg_check_modules(SQLITE sqlite3>=3.6.20) +pkg_check_modules(MARIADB mariadb>=3.3) + +if (MARIADB_FOUND AND SQLITE_FOUND) + add_executable(techempower + techempower.c + json.c + database.c + ) + + target_link_libraries(techempower + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} + ${SQLITE_LIBRARIES} + ${SQLITE_LDFLAGS} + ${MARIADB_LDFLAGS} + ${MARIADB_LIBRARIES} + ) + include_directories(${SQLITE_INCLUDE_DIRS} ${MARIADB_INCLUDE_DIRS}) + include_directories(BEFORE ${CMAKE_BINARY_DIR}) + + if (${CMAKE_BUILD_TYPE} MATCHES "Coverage") + if (Python3_Interpreter_FOUND) + add_dependencies(generate-coverage techempower) + endif() + endif () +else () + message(STATUS "Not building benchmark suite: database libraries not found.") +endif () diff --git a/techempower/database.c b/src/samples/techempower/database.c similarity index 50% rename from techempower/database.c rename to src/samples/techempower/database.c index 5a820b54a..c48b0a895 100644 --- a/techempower/database.c +++ b/src/samples/techempower/database.c @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -14,25 +14,36 @@ * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. */ +#include +#include #include #include #include #include +#include #include "database.h" +#include "lwan-status.h" struct db_stmt { - bool (*bind)(const struct db_stmt *stmt, struct db_row *rows, size_t n_rows); - bool (*step)(const struct db_stmt *stmt, struct db_row *row); + bool (*bind)(const struct db_stmt *stmt, + struct db_row *rows); + bool (*step)(const struct db_stmt *stmt, va_list ap); void (*finalize)(struct db_stmt *stmt); + const char *param_signature; + const char *result_signature; }; struct db { void (*disconnect)(struct db *db); - struct db_stmt *(*prepare)(const struct db *db, const char *sql, const size_t sql_len); + struct db_stmt *(*prepare)(const struct db *db, + const char *sql, + const char *param_signature, + const char *result_signature); }; /* MySQL */ @@ -48,34 +59,35 @@ struct db_stmt_mysql { MYSQL_BIND *param_bind; MYSQL_BIND *result_bind; bool must_execute_again; + bool results_are_bound; + MYSQL_BIND param_result_bind[]; }; static bool db_stmt_bind_mysql(const struct db_stmt *stmt, - struct db_row *rows, size_t n_rows) + struct db_row *rows) { struct db_stmt_mysql *stmt_mysql = (struct db_stmt_mysql *)stmt; + const char *signature = stmt->param_signature; stmt_mysql->must_execute_again = true; + mysql_stmt_reset(stmt_mysql->stmt); - if (!stmt_mysql->param_bind) { - stmt_mysql->param_bind = calloc(n_rows, sizeof(*stmt_mysql->param_bind)); - if (!stmt_mysql->param_bind) - return false; - } else { - mysql_stmt_reset(stmt_mysql->stmt); - } - - for (size_t row = 0; row < n_rows; row++) { - if (rows[row].kind == '\0') break; - + for (size_t row = 0; signature[row]; row++) { MYSQL_BIND *param = &stmt_mysql->param_bind[row]; - if (rows[row].kind == 's') { + + switch (signature[row]) { + case 's': param->buffer_type = MYSQL_TYPE_STRING; param->buffer = rows[row].u.s; - } else if (rows[row].kind == 'i') { + break; + case 'i': param->buffer_type = MYSQL_TYPE_LONG; param->buffer = &rows[row].u.i; + break; + default: + return false; } + param->is_null = false; param->length = 0; } @@ -83,55 +95,55 @@ static bool db_stmt_bind_mysql(const struct db_stmt *stmt, return !mysql_stmt_bind_param(stmt_mysql->stmt, stmt_mysql->param_bind); } -static bool db_stmt_step_mysql(const struct db_stmt *stmt, struct db_row *row) +static bool db_stmt_step_mysql(const struct db_stmt *stmt, + va_list ap) { struct db_stmt_mysql *stmt_mysql = (struct db_stmt_mysql *)stmt; if (stmt_mysql->must_execute_again) { stmt_mysql->must_execute_again = false; + stmt_mysql->results_are_bound = false; if (mysql_stmt_execute(stmt_mysql->stmt)) return false; } - if (!stmt_mysql->result_bind) { - size_t n_rows = 0; - for (struct db_row *r = row; r->kind != '\0'; r++) - n_rows++; + if (!stmt_mysql->results_are_bound) { + const char *signature = stmt->result_signature; - if (!n_rows) + if (*signature == '\0') return false; - stmt_mysql->result_bind = calloc(n_rows, sizeof(*stmt_mysql->result_bind)); - if (!stmt_mysql->result_bind) - return false; - - stmt_mysql->param_bind = calloc(n_rows, sizeof(*stmt_mysql->param_bind)); - if (!stmt_mysql->param_bind) { - free(stmt_mysql->param_bind); - return false; - } + stmt_mysql->results_are_bound = true; MYSQL_BIND *result = stmt_mysql->result_bind; - for (size_t r = 0; r < n_rows; r++) { - if (row[r].kind == 's') { + for (size_t r = 0; signature[r]; r++) { + switch (signature[r]) { + case 's': result[r].buffer_type = MYSQL_TYPE_STRING; - result[r].buffer = row[r].u.s; - } else if (row[r].kind == 'i') { + result[r].buffer = va_arg(ap, char *); + result[r].buffer_length = va_arg(ap, size_t); + break; + case 'i': result[r].buffer_type = MYSQL_TYPE_LONG; - result[r].buffer = &row[r].u.i; - } else { - return false; + result[r].buffer = va_arg(ap, long *); + result[r].buffer_length = 0; + break; + default: + goto out; } result[r].is_null = false; - result[r].buffer_length = row[r].buffer_length; } if (mysql_stmt_bind_result(stmt_mysql->stmt, result)) - return false; + goto out; } return mysql_stmt_fetch(stmt_mysql->stmt) == 0; + +out: + stmt_mysql->results_are_bound = false; + return false; } static void db_stmt_finalize_mysql(struct db_stmt *stmt) @@ -139,40 +151,53 @@ static void db_stmt_finalize_mysql(struct db_stmt *stmt) struct db_stmt_mysql *stmt_mysql = (struct db_stmt_mysql *)stmt; mysql_stmt_close(stmt_mysql->stmt); - free(stmt_mysql->result_bind); - free(stmt_mysql->param_bind); free(stmt_mysql); } -static struct db_stmt *db_prepare_mysql(const struct db *db, const char *sql, - const size_t sql_len) +static struct db_stmt * +db_prepare_mysql(const struct db *db, + const char *sql, + const char *param_signature, + const char *result_signature) { const struct db_mysql *db_mysql = (const struct db_mysql *)db; - struct db_stmt_mysql *stmt_mysql = malloc(sizeof(*stmt_mysql)); + const size_t n_bounds = strlen(param_signature) + strlen(result_signature); + struct db_stmt_mysql *stmt_mysql = malloc(sizeof(*stmt_mysql) + n_bounds * sizeof(MYSQL_BIND)); if (!stmt_mysql) return NULL; stmt_mysql->stmt = mysql_stmt_init(db_mysql->con); - if (!stmt_mysql->stmt) { - free(stmt_mysql); - return NULL; - } + if (!stmt_mysql->stmt) + goto out_free_stmt; - if (mysql_stmt_prepare(stmt_mysql->stmt, sql, sql_len)) { - mysql_stmt_close(stmt_mysql->stmt); - free(stmt_mysql); - return NULL; - } + if (mysql_stmt_prepare(stmt_mysql->stmt, sql, strlen(sql))) + goto out_close_stmt; + + assert(strlen(param_signature) == mysql_stmt_param_count(stmt_mysql->stmt)); + assert(strlen(result_signature) == mysql_stmt_field_count(stmt_mysql->stmt)); stmt_mysql->base.bind = db_stmt_bind_mysql; stmt_mysql->base.step = db_stmt_step_mysql; stmt_mysql->base.finalize = db_stmt_finalize_mysql; - stmt_mysql->result_bind = NULL; - stmt_mysql->param_bind = NULL; + stmt_mysql->param_bind = &stmt_mysql->param_result_bind[0]; + stmt_mysql->result_bind = &stmt_mysql->param_result_bind[strlen(param_signature)]; stmt_mysql->must_execute_again = true; + stmt_mysql->results_are_bound = false; + + stmt_mysql->base.param_signature = param_signature; + stmt_mysql->base.result_signature = result_signature; - return (struct db_stmt*)stmt_mysql; + memset(stmt_mysql->param_result_bind, 0, n_bounds * sizeof(MYSQL_BIND)); + + return (struct db_stmt *)stmt_mysql; + +out_close_stmt: + mysql_stmt_close(stmt_mysql->stmt); +out_free_stmt: + free(stmt_mysql); + + return NULL; } static void db_disconnect_mysql(struct db *db) @@ -183,8 +208,10 @@ static void db_disconnect_mysql(struct db *db) free(db); } -struct db *db_connect_mysql(const char *host, const char *user, const char *pass, - const char *database) +struct db *db_connect_mysql(const char *host, + const char *user, + const char *pass, + const char *database) { struct db_mysql *db_mysql = malloc(sizeof(*db_mysql)); @@ -197,7 +224,8 @@ struct db *db_connect_mysql(const char *host, const char *user, const char *pass return NULL; } - if (!mysql_real_connect(db_mysql->con, host, user, pass, database, 0, NULL, 0)) + if (!mysql_real_connect(db_mysql->con, host, user, pass, database, 0, NULL, + 0)) goto error; if (mysql_set_character_set(db_mysql->con, "utf8")) @@ -226,49 +254,65 @@ struct db_stmt_sqlite { sqlite3_stmt *sqlite; }; -static bool db_stmt_bind_sqlite(const struct db_stmt *stmt, struct db_row *rows, size_t n_rows) +static bool db_stmt_bind_sqlite(const struct db_stmt *stmt, + struct db_row *rows) { - const struct db_stmt_sqlite *stmt_sqlite = (const struct db_stmt_sqlite *)stmt; - const struct db_row *rows_1_based = rows - 1; - int ret; + const struct db_stmt_sqlite *stmt_sqlite = + (const struct db_stmt_sqlite *)stmt; + const char *signature = stmt->param_signature; sqlite3_reset(stmt_sqlite->sqlite); sqlite3_clear_bindings(stmt_sqlite->sqlite); - for (size_t row = 1; row <= n_rows; row++) { - const struct db_row *r = &rows_1_based[row]; - if (r->kind == '\0') break; - - if (r->kind == 's') { - ret = sqlite3_bind_text(stmt_sqlite->sqlite, (int)row, r->u.s, -1, NULL); - if (ret != SQLITE_OK) - return false; - } else if (r->kind == 'i') { - ret = sqlite3_bind_int(stmt_sqlite->sqlite, (int)row, r->u.i); - if (ret != SQLITE_OK) - return false; - } else { + for (size_t row = 0; signature[row]; row++) { + const struct db_row *r = &rows[row]; + int ret; + + switch (signature[row]) { + case 's': + ret = sqlite3_bind_text(stmt_sqlite->sqlite, (int)row + 1, r->u.s, -1, + NULL); + break; + case 'i': + ret = sqlite3_bind_int(stmt_sqlite->sqlite, (int)row + 1, r->u.i); + break; + default: return false; } + + if (ret != SQLITE_OK) + return false; } return true; } -static bool db_stmt_step_sqlite(const struct db_stmt *stmt, struct db_row *row) +static bool db_stmt_step_sqlite(const struct db_stmt *stmt, + va_list ap) { - const struct db_stmt_sqlite *stmt_sqlite = (const struct db_stmt_sqlite *)stmt; + const struct db_stmt_sqlite *stmt_sqlite = + (const struct db_stmt_sqlite *)stmt; + const char *signature = stmt->result_signature; if (sqlite3_step(stmt_sqlite->sqlite) != SQLITE_ROW) return false; - int column_id = 0; - for (struct db_row *r = row; r->kind != '\0'; r++, column_id++) { - if (r->kind == 'i') { - r->u.i = sqlite3_column_int(stmt_sqlite->sqlite, column_id); - } else if (r->kind == 's') { - r->u.s = (char *)sqlite3_column_text(stmt_sqlite->sqlite, column_id); - } else { + for (int r = 0; signature[r]; r++) { + switch (signature[r]) { + case 'i': + *va_arg(ap, long *) = sqlite3_column_int(stmt_sqlite->sqlite, r); + break; + case 's': { + char *out = va_arg(ap, char *); + size_t bufsize = va_arg(ap, size_t); + + strncpy(out, + (const char *)sqlite3_column_text(stmt_sqlite->sqlite, r), + bufsize); + + break; + } + default: return false; } } @@ -284,8 +328,11 @@ static void db_stmt_finalize_sqlite(struct db_stmt *stmt) free(stmt_sqlite); } -static struct db_stmt *db_prepare_sqlite(const struct db *db, const char *sql, - const size_t sql_len) +static struct db_stmt * +db_prepare_sqlite(const struct db *db, + const char *sql, + const char *param_signature, + const char *result_signature) { const struct db_sqlite *db_sqlite = (const struct db_sqlite *)db; struct db_stmt_sqlite *stmt_sqlite = malloc(sizeof(*stmt_sqlite)); @@ -293,7 +340,8 @@ static struct db_stmt *db_prepare_sqlite(const struct db *db, const char *sql, if (!stmt_sqlite) return NULL; - int ret = sqlite3_prepare(db_sqlite->sqlite, sql, (int)sql_len, &stmt_sqlite->sqlite, NULL); + int ret = sqlite3_prepare_v2(db_sqlite->sqlite, sql, (int)strlen(sql), + &stmt_sqlite->sqlite, NULL); if (ret != SQLITE_OK) { free(stmt_sqlite); return NULL; @@ -303,6 +351,9 @@ static struct db_stmt *db_prepare_sqlite(const struct db *db, const char *sql, stmt_sqlite->base.step = db_stmt_step_sqlite; stmt_sqlite->base.finalize = db_stmt_finalize_sqlite; + stmt_sqlite->base.param_signature = param_signature; + stmt_sqlite->base.result_signature = result_signature; + return (struct db_stmt *)stmt_sqlite; } @@ -314,14 +365,15 @@ static void db_disconnect_sqlite(struct db *db) free(db); } -struct db *db_connect_sqlite(const char *path, bool read_only, const char *pragmas[]) +struct db * +db_connect_sqlite(const char *path, bool read_only, const char *pragmas[]) { struct db_sqlite *db_sqlite = malloc(sizeof(*db_sqlite)); if (!db_sqlite) return NULL; - int flags = read_only ? SQLITE_OPEN_READONLY : 0; + int flags = read_only ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE; int ret = sqlite3_open_v2(path, &db_sqlite->sqlite, flags, NULL); if (ret != SQLITE_OK) { free(db_sqlite); @@ -341,28 +393,31 @@ struct db *db_connect_sqlite(const char *path, bool read_only, const char *pragm /* Generic */ -inline bool db_stmt_bind(const struct db_stmt *stmt, struct db_row *rows, size_t n_rows) +inline bool db_stmt_bind(const struct db_stmt *stmt, struct db_row *rows) { - return stmt->bind(stmt, rows, n_rows); + return stmt->bind(stmt, rows); } -inline bool db_stmt_step(const struct db_stmt *stmt, struct db_row *row) +inline bool db_stmt_step(const struct db_stmt *stmt, ...) { - return stmt->step(stmt, row); -} + va_list ap; + bool ret; -inline void db_stmt_finalize(struct db_stmt *stmt) -{ - stmt->finalize(stmt); -} + va_start(ap, stmt); + ret = stmt->step(stmt, ap); + va_end(ap); -inline void db_disconnect(struct db *db) -{ - db->disconnect(db); + return ret; } -inline struct db_stmt *db_prepare_stmt(const struct db *db, const char *sql, - const size_t sql_len) +inline void db_stmt_finalize(struct db_stmt *stmt) { stmt->finalize(stmt); } + +inline void db_disconnect(struct db *db) { db->disconnect(db); } + +inline struct db_stmt *db_prepare_stmt(const struct db *db, + const char *sql, + const char *param_signature, + const char *result_signature) { - return db->prepare(db, sql, sql_len); + return db->prepare(db, sql, param_signature, result_signature); } diff --git a/techempower/database.h b/src/samples/techempower/database.h similarity index 58% rename from techempower/database.h rename to src/samples/techempower/database.h index e439d6ce0..323cdcbf4 100644 --- a/techempower/database.h +++ b/src/samples/techempower/database.h @@ -1,6 +1,6 @@ /* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -14,7 +14,8 @@ * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. */ #pragma once @@ -29,17 +30,24 @@ struct db_row { char *s; int i; } u; - char kind; /* 's' = string, 'i' = 'int', '\0' = last */ size_t buffer_length; }; -bool db_stmt_bind(const struct db_stmt *stmt, struct db_row *rows, size_t n_rows); -bool db_stmt_step(const struct db_stmt *stmt, struct db_row *row); + +struct db_stmt *db_prepare_stmt(const struct db *db, + const char *sql, + const char *param_signature, + const char *result_signature); void db_stmt_finalize(struct db_stmt *stmt); -void db_disconnect(struct db *db); -struct db_stmt *db_prepare_stmt(const struct db *db, const char *sql, - const size_t sql_len); -struct db *db_connect_sqlite(const char *path, bool read_only, const char *pragmas[]); -struct db *db_connect_mysql(const char *host, const char *user, const char *pass, const char *database); +bool db_stmt_bind(const struct db_stmt *stmt, struct db_row *rows); +bool db_stmt_step(const struct db_stmt *stmt, ...); +struct db *db_connect_sqlite(const char *path, + bool read_only, + const char *pragmas[]); +struct db *db_connect_mysql(const char *host, + const char *user, + const char *pass, + const char *database); +void db_disconnect(struct db *db); diff --git a/src/samples/techempower/json.c b/src/samples/techempower/json.c new file mode 100644 index 000000000..bf87beebe --- /dev/null +++ b/src/samples/techempower/json.c @@ -0,0 +1,954 @@ +/* + * Copyright (c) 2017 Intel Corporation + * Copyright (c) 2020 L. A. F. Pereira + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "json.h" +#include "lwan.h" +#include "int-to-str.h" + +struct token { + enum json_tokens type; + char *start; + char *end; +}; + +struct lexer { + void *(*state)(struct lexer *lexer); + char *start; + char *pos; + char *end; + struct token token; +}; + +struct json_obj { + struct lexer lexer; +}; + +struct json_obj_key_value { + const char *key; + size_t key_len; + struct token value; +}; + +static bool lexer_consume(struct lexer *lexer, + struct token *token, + enum json_tokens empty_token) +{ + if (lexer->token.type == empty_token) { + return false; + } + + *token = lexer->token; + lexer->token.type = empty_token; + + return true; +} + +static bool lexer_next(struct lexer *lexer, struct token *token) +{ + while (lexer->state) { + if (lexer_consume(lexer, token, JSON_TOK_NONE)) { + return true; + } + + lexer->state = lexer->state(lexer); + } + + return lexer_consume(lexer, token, JSON_TOK_EOF); +} + +static void *lexer_json(struct lexer *lexer); + +static void emit(struct lexer *lexer, enum json_tokens token) +{ + lexer->token.type = token; + lexer->token.start = lexer->start; + lexer->token.end = lexer->pos; + lexer->start = lexer->pos; +} + +static void* emit_cont(struct lexer *lexer, enum json_tokens token) +{ + emit(lexer, token); + return lexer_json; +} + +static void* emit_end(struct lexer *lexer, enum json_tokens token) +{ + emit(lexer, token); + return NULL; +} + +static int next(struct lexer *lexer) +{ + if (lexer->pos >= lexer->end) { + lexer->pos = lexer->end + 1; + + return '\0'; + } + + return *lexer->pos++; +} + +static void ignore(struct lexer *lexer) { lexer->start = lexer->pos; } + +static void backup(struct lexer *lexer) { lexer->pos--; } + +static int peek(struct lexer *lexer) +{ + int chr = next(lexer); + + backup(lexer); + + return chr; +} + +static void *lexer_string(struct lexer *lexer) +{ + ignore(lexer); + + while (true) { + int chr = next(lexer); + + if (UNLIKELY(chr == '\0')) { + return emit_end(lexer, JSON_TOK_ERROR); + } + + if (chr == '\\') { + switch (next(lexer)) { + case '"': + case '\\': + case '/': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + continue; + case 'u': + if (UNLIKELY(!isxdigit(next(lexer)))) { + goto error; + } + + if (UNLIKELY(!isxdigit(next(lexer)))) { + goto error; + } + + if (UNLIKELY(!isxdigit(next(lexer)))) { + goto error; + } + + if (UNLIKELY(!isxdigit(next(lexer)))) { + goto error; + } + + break; + default: + goto error; + } + } + + if (chr == '"') { + backup(lexer); + emit(lexer, JSON_TOK_STRING); + + next(lexer); + ignore(lexer); + + return lexer_json; + } + } + +error: + return emit_end(lexer, JSON_TOK_ERROR); +} + +static int accept_run(struct lexer *lexer, const char *run) +{ + for (; *run; run++) { + if (UNLIKELY(next(lexer) != *run)) { + return -EINVAL; + } + } + + return 0; +} + +static void *lexer_boolean(struct lexer *lexer) +{ + /* Already matched either `t' or `f' at this point */ + + switch (next(lexer)) { + case 'r': + if (LIKELY(!accept_run(lexer, "ue"))) { + return emit_cont(lexer, JSON_TOK_TRUE); + } + break; + case 'a': + if (LIKELY(!accept_run(lexer, "lse"))) { + return emit_cont(lexer, JSON_TOK_FALSE); + } + break; + } + + return emit_end(lexer, JSON_TOK_ERROR); +} + +static void *lexer_null(struct lexer *lexer) +{ + if (UNLIKELY(accept_run(lexer, "ull") < 0)) { + return emit_end(lexer, JSON_TOK_ERROR); + } + + return emit_cont(lexer, JSON_TOK_NULL); +} + +static void *lexer_number(struct lexer *lexer) +{ + while (true) { + int chr = next(lexer); + + if (isdigit(chr) || chr == '.') { + continue; + } + + backup(lexer); + return emit_cont(lexer, JSON_TOK_NUMBER); + } +} + +static void *lexer_json(struct lexer *lexer) +{ + while (true) { + int chr = next(lexer); + + switch (chr) { + case '\0': + return emit_end(lexer, JSON_TOK_EOF); + + case '}': + case '{': + case '[': + case ']': + case ',': + case ':': + return emit_cont(lexer, (enum json_tokens)chr); + + case '"': + return lexer_string; + + case 'n': + return lexer_null; + + case 't': + case 'f': + return lexer_boolean; + + case '-': + if (LIKELY(isdigit(peek(lexer)))) { + return lexer_number; + } + + /* fallthrough */ + default: + if (isspace(chr)) { + ignore(lexer); + continue; + } + + if (LIKELY(isdigit(chr))) { + return lexer_number; + } + + return emit_end(lexer, JSON_TOK_ERROR); + } + } +} + +static void lexer_init(struct lexer *lexer, char *data, size_t len) +{ + lexer->state = lexer_json; + lexer->start = data; + lexer->pos = data; + lexer->end = data + len; + lexer->token.type = JSON_TOK_NONE; +} + +static int obj_init(struct json_obj *json, char *data, size_t len) +{ + struct token token; + + lexer_init(&json->lexer, data, len); + + if (UNLIKELY(!lexer_next(&json->lexer, &token))) { + return -EINVAL; + } + + if (UNLIKELY(token.type != JSON_TOK_OBJECT_START)) { + return -EINVAL; + } + + return 0; +} + +static int element_token(enum json_tokens token) +{ + switch (token) { + case JSON_TOK_OBJECT_START: + case JSON_TOK_LIST_START: + case JSON_TOK_STRING: + case JSON_TOK_NUMBER: + case JSON_TOK_TRUE: + case JSON_TOK_FALSE: + return 0; + default: + return -EINVAL; + } +} + +static int obj_next(struct json_obj *json, struct json_obj_key_value *kv) +{ + struct token token; + + if (UNLIKELY(!lexer_next(&json->lexer, &token))) { + return -EINVAL; + } + + /* Match end of object or next key */ + switch (token.type) { + case JSON_TOK_OBJECT_END: + kv->key = NULL; + kv->key_len = 0; + kv->value = token; + + return 0; + case JSON_TOK_COMMA: + if (UNLIKELY(!lexer_next(&json->lexer, &token))) { + return -EINVAL; + } + + if (UNLIKELY(token.type != JSON_TOK_STRING)) { + return -EINVAL; + } + + /* fallthrough */ + case JSON_TOK_STRING: + kv->key = token.start; + kv->key_len = (size_t)(token.end - token.start); + break; + default: + return -EINVAL; + } + + /* Match : after key */ + if (UNLIKELY(!lexer_next(&json->lexer, &token))) { + return -EINVAL; + } + + if (UNLIKELY(token.type != JSON_TOK_COLON)) { + return -EINVAL; + } + + /* Match value */ + if (UNLIKELY(!lexer_next(&json->lexer, &kv->value))) { + return -EINVAL; + } + + return element_token(kv->value.type); +} + +static int arr_next(struct json_obj *json, struct token *value) +{ + if (UNLIKELY(!lexer_next(&json->lexer, value))) { + return -EINVAL; + } + + if (value->type == JSON_TOK_LIST_END) { + return 0; + } + + if (value->type == JSON_TOK_COMMA) { + if (UNLIKELY(!lexer_next(&json->lexer, value))) { + return -EINVAL; + } + } + + return element_token(value->type); +} + +static int decode_num(const struct token *token, int32_t *num) +{ + /* FIXME: strtod() is not available in newlib/minimal libc, + * so using strtol() here. + */ + char *endptr; + char prev_end; + + prev_end = *token->end; + *token->end = '\0'; + + errno = 0; + long v = strtol(token->start, &endptr, 10); + if ((long)(int)v != v) { + return -ERANGE; + } + + *num = (int)v; + + *token->end = prev_end; + + if (errno != 0) { + return -errno; + } + + if (endptr != token->end) { + return -EINVAL; + } + + return 0; +} + +static bool equivalent_types(enum json_tokens type1, enum json_tokens type2) +{ + if (type1 == JSON_TOK_TRUE || type1 == JSON_TOK_FALSE) { + return type2 == JSON_TOK_TRUE || type2 == JSON_TOK_FALSE; + } + + return type1 == type2; +} + +static int obj_parse(struct json_obj *obj, + const struct json_obj_descr *descr, + size_t descr_len, + void *val); +static int arr_parse(struct json_obj *obj, + const struct json_obj_descr *elem_descr, + size_t max_elements, + void *field, + void *val); + +static int decode_value(struct json_obj *obj, + const struct json_obj_descr *descr, + struct token *value, + void *field, + void *val) +{ + + if (!equivalent_types(value->type, descr->type)) { + return -EINVAL; + } + + switch (descr->type) { + case JSON_TOK_OBJECT_START: + return obj_parse(obj, descr->object.sub_descr, + descr->object.sub_descr_len, field); + case JSON_TOK_LIST_START: + return arr_parse(obj, descr->array.element_descr, + descr->array.n_elements, field, val); + case JSON_TOK_FALSE: + case JSON_TOK_TRUE: { + bool *v = field; + + *v = value->type == JSON_TOK_TRUE; + + return 0; + } + case JSON_TOK_NUMBER: { + int32_t *num = field; + + return decode_num(value, num); + } + case JSON_TOK_STRING: { + char **str = field; + + *value->end = '\0'; + *str = value->start; + + return 0; + } + default: + return -EINVAL; + } +} + +static ptrdiff_t get_elem_size(const struct json_obj_descr *descr) +{ + switch (descr->type) { + case JSON_TOK_NUMBER: + return sizeof(int32_t); + case JSON_TOK_STRING: + return sizeof(char *); + case JSON_TOK_TRUE: + case JSON_TOK_FALSE: + return sizeof(bool); + case JSON_TOK_LIST_START: + return (ptrdiff_t)descr->array.n_elements * + get_elem_size(descr->array.element_descr); + case JSON_TOK_OBJECT_START: { + ptrdiff_t total = 0; + size_t i; + + for (i = 0; i < descr->object.sub_descr_len; i++) { + ptrdiff_t s = get_elem_size(&descr->object.sub_descr[i]); + + total += (ptrdiff_t)ROUND_UP(s, descr->object.sub_descr[i].align); + } + + return total; + } + default: + return -EINVAL; + } +} + +static int arr_parse(struct json_obj *obj, + const struct json_obj_descr *elem_descr, + size_t max_elements, + void *field, + void *val) +{ + ptrdiff_t elem_size = get_elem_size(elem_descr); + void *last_elem = (char *)field + elem_size * (ptrdiff_t)max_elements; + size_t *elements = (size_t *)((char *)val + elem_descr->offset); + struct token value; + + assert(elem_size > 0); + + *elements = 0; + + while (!arr_next(obj, &value)) { + if (value.type == JSON_TOK_LIST_END) { + return 0; + } + + if (UNLIKELY(field == last_elem)) { + return -ENOSPC; + } + + if (UNLIKELY(decode_value(obj, elem_descr, &value, field, val) < 0)) { + return -EINVAL; + } + + (*elements)++; + field = (char *)field + elem_size; + } + + return -EINVAL; +} + +static int obj_parse(struct json_obj *obj, + const struct json_obj_descr *descr, + size_t descr_len, + void *val) +{ + struct json_obj_key_value kv; + int32_t decoded_fields = 0; + size_t i; + int ret; + + while (!obj_next(obj, &kv)) { + if (kv.value.type == JSON_TOK_OBJECT_END) { + return decoded_fields; + } + + for (i = 0; i < descr_len; i++) { + void *decode_field = (char *)val + descr[i].offset; + + /* Field has been decoded already, skip */ + if (decoded_fields & (1 << i)) { + continue; + } + + /* Check if it's the i-th field */ + if (kv.key_len != descr[i].field_name_len) { + continue; + } + + if (memcmp(kv.key, descr[i].field_name, descr[i].field_name_len)) { + continue; + } + + /* Store the decoded value */ + ret = decode_value(obj, &descr[i], &kv.value, decode_field, val); + if (UNLIKELY(UNLIKELY(ret < 0))) { + return ret; + } + + decoded_fields |= 1 << i; + break; + } + } + + return -EINVAL; +} + +int json_obj_parse(char *payload, + size_t len, + const struct json_obj_descr *descr, + size_t descr_len, + void *val) +{ + struct json_obj obj; + int ret; + + assert(descr_len < (sizeof(ret) * CHAR_BIT - 1)); + + ret = obj_init(&obj, payload, len); + if (UNLIKELY(ret < 0)) { + return ret; + } + + return obj_parse(&obj, descr, descr_len, val); +} + +/* + * Routines has_zero() and has_value() are from + * https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord + */ +static ALWAYS_INLINE uint64_t has_zero(uint64_t v) +{ + return (v - 0x0101010101010101UL) & ~v & 0x8080808080808080UL; +} + +static ALWAYS_INLINE uint64_t has_value(uint64_t x, char n) +{ + return has_zero(x ^ (~0UL / 255 * (uint64_t)n)); +} + +static char escape_as(char chr) +{ + static const char escaped[] = {'"', '\\', 'b', 'f', 'n', 'r', 't', 't'}; + uint64_t mask = has_value(0x225c080c0a0d0909UL, chr); + return mask == 0 ? 0 : escaped[__builtin_clzl(mask) / 8]; +} + +static int json_escape_internal(const char *str, + json_append_bytes_t append_bytes, + void *data) +{ + const char *cur; + const char *unescaped; + int ret = 0; + + for (cur = unescaped = str; *cur; cur++) { + char escaped = escape_as(*cur); + + if (escaped) { + char bytes[2] = {'\\', escaped}; + + if (cur - unescaped) { + ret |= append_bytes(unescaped, (size_t)(cur - unescaped), data); + unescaped = cur + 1; + } + + ret |= append_bytes(bytes, 2, data); + } + } + + if (cur - unescaped) + ret |= append_bytes(unescaped, (size_t)(cur - unescaped), data); + + return ret; +} + +size_t json_calc_escaped_len(const char *str, size_t len) +{ + size_t escaped_len = len; + size_t pos; + + for (pos = 0; pos < len; pos++) { + if (escape_as(str[pos])) { + escaped_len++; + } + } + + return escaped_len; +} + +ssize_t json_escape(char *str, size_t *len, size_t buf_size) +{ + char *next; /* Points after next character to escape. */ + char *dest; /* Points after next place to write escaped character. */ + size_t escaped_len = json_calc_escaped_len(str, *len); + + if (escaped_len == *len) { + /* + * If no escape is necessary, there is nothing to do. + */ + return 0; + } + + if (UNLIKELY(escaped_len >= buf_size)) { + return -ENOMEM; + } + + /* + * By walking backwards in the buffer from the end positions + * of both the original and escaped strings, we avoid using + * extra space. Characters in the original string are + * overwritten only after they have already been escaped. + */ + str[escaped_len] = '\0'; + for (next = &str[*len], dest = &str[escaped_len]; next != str;) { + char next_c = *(--next); + char escape = escape_as(next_c); + + if (escape) { + *(--dest) = escape; + *(--dest) = '\\'; + } else { + *(--dest) = next_c; + } + } + *len = escaped_len; + + return 0; +} + +static int encode(const struct json_obj_descr *descr, + const void *val, + json_append_bytes_t append_bytes, + void *data, + bool escape_key); + +static int arr_encode(const struct json_obj_descr *elem_descr, + const void *field, + const void *val, + json_append_bytes_t append_bytes, + void *data, + bool escape_key) +{ + ptrdiff_t elem_size = get_elem_size(elem_descr); + /* + * NOTE: Since an element descriptor's offset isn't meaningful (array + * elements occur at multiple offsets in `val'), we use its space in + * elem_descr to store the offset to the field containing the number of + * elements. + */ + size_t n_elem = *(size_t *)((char *)val + elem_descr->offset); + int ret = append_bytes("[", 1, data); + + if (LIKELY(n_elem)) { + n_elem--; + for (size_t i = 0; i < n_elem; i++) { + /* + * Though "field" points at the next element in the array which we + * need to encode, the value in elem_descr->offset is actually the + * offset of the length field in the "parent" struct containing the + * array. + * + * To patch things up, we lie to encode() about where the field is + * by exactly the amount it will offset it. This is a size + * optimization for struct json_obj_descr: the alternative is to + * keep a separate field next to element_descr which is an offset to + * the length field in the parent struct, but that would add a + * size_t to every descriptor. + */ + ret |= encode(elem_descr, (char *)field - elem_descr->offset, + append_bytes, data, escape_key); + + ret |= append_bytes(",", 1, data); + + field = (char *)field + elem_size; + } + + ret |= encode(elem_descr, (char *)field - elem_descr->offset, + append_bytes, data, escape_key); + } + + return ret | append_bytes("]", 1, data); +} + +static int str_encode(const char **str, + json_append_bytes_t append_bytes, + void *data) +{ + int ret = append_bytes("\"", 1, data); + + ret |= json_escape_internal(*str, append_bytes, data); + + return ret | append_bytes("\"", 1, data); +} + +static int +num_encode(const int32_t *num, json_append_bytes_t append_bytes, void *data) +{ + char buf[INT_TO_STR_BUFFER_SIZE]; + size_t len; + char *as_string = int_to_string(*num, buf, &len); + + return append_bytes(as_string, len, data); +} + +static int +bool_encode(const bool *value, json_append_bytes_t append_bytes, void *data) +{ + if (*value) { + return append_bytes("true", 4, data); + } + + return append_bytes("false", 5, data); +} + +int json_arr_encode_full(const struct json_obj_descr *descr, + const void *val, + json_append_bytes_t append_bytes, + void *data, + bool escape_key) +{ + void *ptr = (char *)val + descr->offset; + + return arr_encode(descr->array.element_descr, ptr, val, append_bytes, data, + escape_key); +} + +static int encode(const struct json_obj_descr *descr, + const void *val, + json_append_bytes_t append_bytes, + void *data, + bool escape_key) +{ + void *ptr = (char *)val + descr->offset; + + switch (descr->type) { + case JSON_TOK_FALSE: + case JSON_TOK_TRUE: + return bool_encode(ptr, append_bytes, data); + case JSON_TOK_STRING: + return str_encode(ptr, append_bytes, data); + case JSON_TOK_LIST_START: + return arr_encode(descr->array.element_descr, ptr, val, + append_bytes, data, escape_key); + case JSON_TOK_OBJECT_START: + return json_obj_encode_full(descr->object.sub_descr, + descr->object.sub_descr_len, ptr, append_bytes, + data, escape_key); + case JSON_TOK_NUMBER: + return num_encode(ptr, append_bytes, data); + default: + return -EINVAL; + } +} + +static inline int encode_key(const struct json_obj_descr *descr, + json_append_bytes_t append_bytes, + void *data, + bool escape_key) +{ + int ret; + + if (!escape_key) { + /* Keys are encoded twice in the descriptor; once without quotes and + * the trailing comma, and one with. Doing it like so cuts some + * indirect calls to append_bytes(), which in turn also potentially + * cuts some branches in most implementations of it. */ + ret = append_bytes(descr->field_name + descr->field_name_len, + descr->field_name_len + 3 /* 3=len('"":') */, data); + } else { + ret = str_encode((const char **)&descr->field_name, append_bytes, data); + ret |= append_bytes(":", 1, data); + } + + return ret; +} + +int json_obj_encode_full(const struct json_obj_descr *descr, + size_t descr_len, + const void *val, + json_append_bytes_t append_bytes, + void *data, + bool escape_key) +{ + int ret = append_bytes("{", 1, data); + + if (LIKELY(descr_len)) { + /* To avoid checking if we're encoding the last element on each + * iteration of this loop, start at the second descriptor, and always + * write the comma. Then, after the loop, encode the first descriptor. + * If the descriptor array has only 1 element, this loop won't run. This + * is fine since order isn't important for objects, and we save some + * branches. */ + + for (size_t i = 1; i < descr_len; i++) { + ret |= encode_key(&descr[i], append_bytes, data, escape_key); + ret |= encode(&descr[i], val, append_bytes, data, escape_key); + ret |= append_bytes(",", 1, data); + } + + ret |= encode_key(&descr[0], append_bytes, data, escape_key); + ret |= encode(&descr[0], val, append_bytes, data, escape_key); + } + + return ret | append_bytes("}", 1, data); +} + +struct appender { + char *buffer; + size_t used; + size_t size; +}; + +static int append_bytes_to_buf(const char *bytes, size_t len, void *data) +{ + struct appender *appender = data; + + if (UNLIKELY(len > appender->size - appender->used)) { + return -ENOMEM; + } + + memcpy(appender->buffer + appender->used, bytes, len); + appender->used += len; + appender->buffer[appender->used] = '\0'; + + return 0; +} + +int json_obj_encode_buf(const struct json_obj_descr *descr, + size_t descr_len, + const void *val, + char *buffer, + size_t buf_size) +{ + struct appender appender = {.buffer = buffer, .size = buf_size}; + + return json_obj_encode(descr, descr_len, val, append_bytes_to_buf, + &appender); +} + +static int +measure_bytes(const char *bytes __attribute__((unused)), size_t len, void *data) +{ + ssize_t *total = data; + + *total += (ssize_t)len; + + return 0; +} + +ssize_t json_calc_encoded_len(const struct json_obj_descr *descr, + size_t descr_len, + const void *val) +{ + ssize_t total = 0; + int ret; + + ret = json_obj_encode(descr, descr_len, val, measure_bytes, &total); + if (UNLIKELY(ret < 0)) { + return ret; + } + + return total; +} diff --git a/src/samples/techempower/json.h b/src/samples/techempower/json.h new file mode 100644 index 000000000..daa02c986 --- /dev/null +++ b/src/samples/techempower/json.h @@ -0,0 +1,668 @@ +/* + * Copyright (c) 2017 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_DATA_JSON_H_ +#define ZEPHYR_INCLUDE_DATA_JSON_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define ROUND_UP(x, align) \ + (((unsigned long)(x) + ((unsigned long)(align)-1)) & \ + ~((unsigned long)(align)-1)) + +/** + * @brief Structured Data + * @defgroup structured_data Structured Data + */ + +/** + * @defgroup json JSON + * @ingroup structured_data + * @{ + */ + +enum json_tokens { + /* Before changing this enum, ensure that its maximum + * value is still within 7 bits. See comment next to the + * declaration of `type` in struct json_obj_descr. + */ + + JSON_TOK_NONE = '_', + JSON_TOK_OBJECT_START = '{', + JSON_TOK_OBJECT_END = '}', + JSON_TOK_LIST_START = '[', + JSON_TOK_LIST_END = ']', + JSON_TOK_STRING = '"', + JSON_TOK_COLON = ':', + JSON_TOK_COMMA = ',', + JSON_TOK_NUMBER = '0', + JSON_TOK_TRUE = 't', + JSON_TOK_FALSE = 'f', + JSON_TOK_NULL = 'n', + JSON_TOK_ERROR = '!', + JSON_TOK_EOF = '\0', +}; + +struct json_obj_descr { + const char *field_name; + + uint32_t align; + uint32_t field_name_len; + uint32_t type; + uint32_t offset; + + union { + struct { + const struct json_obj_descr *sub_descr; + size_t sub_descr_len; + } object; + struct { + const struct json_obj_descr *element_descr; + size_t n_elements; + } array; + }; +}; + +/** + * @brief Function pointer type to append bytes to a buffer while + * encoding JSON data. + * + * @param bytes Contents to write to the output + * @param len Number of bytes in @param bytes to append to output + * @param data User-provided pointer + * + * @return This callback function should return a negative number on + * error (which will be propagated to the return value of + * json_obj_encode()), or 0 on success. + */ +typedef int (*json_append_bytes_t)(const char *bytes, size_t len, void *data); + +#define JSON_FIELD_NAME(field_name_) \ + .field_name = #field_name_ "\"" #field_name_ "\":", \ + .field_name_len = sizeof(#field_name_) - 1 + +/** + * @brief Helper macro to declare a descriptor for supported primitive + * values. + * + * @param struct_ Struct packing the values + * + * @param field_name_ Field name in the struct + * + * @param type_ Token type for JSON value corresponding to a primitive + * type. Must be one of: JSON_TOK_STRING for strings, JSON_TOK_NUMBER + * for numbers, JSON_TOK_TRUE (or JSON_TOK_FALSE) for booleans. + * + * Here's an example of use: + * + * struct foo { + * int some_int; + * }; + * + * struct json_obj_descr foo[] = { + * JSON_OBJ_DESCR_PRIM(struct foo, some_int, JSON_TOK_NUMBER), + * }; + */ +#define JSON_OBJ_DESCR_PRIM(struct_, field_name_, type_) \ + { \ + JSON_FIELD_NAME(field_name_), \ + .align = __alignof__(struct_), .type = type_, \ + .offset = offsetof(struct_, field_name_), \ + } + +/** + * @brief Helper macro to declare a descriptor for an object value + * + * @param struct_ Struct packing the values + * + * @param field_name_ Field name in the struct + * + * @param sub_descr_ Array of json_obj_descr describing the subobject + * + * Here's an example of use: + * + * struct nested { + * int foo; + * struct { + * int baz; + * } bar; + * }; + * + * struct json_obj_descr nested_bar[] = { + * { ... declare bar.baz descriptor ... }, + * }; + * struct json_obj_descr nested[] = { + * { ... declare foo descriptor ... }, + * JSON_OBJ_DESCR_OBJECT(struct nested, bar, nested_bar), + * }; + */ +#define JSON_OBJ_DESCR_OBJECT(struct_, field_name_, sub_descr_) \ + { \ + JSON_FIELD_NAME(field_name_), \ + .align = __alignof__(struct_), .type = JSON_TOK_OBJECT_START, \ + .offset = offsetof(struct_, field_name_), \ + .object = { \ + .sub_descr = sub_descr_, \ + .sub_descr_len = ARRAY_SIZE(sub_descr_), \ + }, \ + } + +/** + * @brief Helper macro to declare a descriptor for an array of primitives + * + * @param struct_ Struct packing the values + * + * @param field_name_ Field name in the struct + * + * @param max_len_ Maximum number of elements in array + * + * @param len_field_ Field name in the struct for the number of elements + * in the array + * + * @param elem_type_ Element type, must be a primitive type + * + * Here's an example of use: + * + * struct example { + * int foo[10]; + * size_t foo_len; + * }; + * + * struct json_obj_descr array[] = { + * JSON_OBJ_DESCR_ARRAY(struct example, foo, 10, foo_len, + * JSON_TOK_NUMBER) + * }; + */ +#define JSON_OBJ_DESCR_ARRAY(struct_, field_name_, max_len_, len_field_, \ + elem_type_) \ + { \ + JSON_FIELD_NAME(field_name_), \ + .align = __alignof__(struct_), .type = JSON_TOK_LIST_START, \ + .offset = offsetof(struct_, field_name_), \ + .array = { \ + .element_descr = \ + &(struct json_obj_descr){ \ + .align = __alignof__(struct_), \ + .type = elem_type_, \ + .offset = offsetof(struct_, len_field_), \ + }, \ + .n_elements = (max_len_), \ + }, \ + } + +/** + * @brief Helper macro to declare a descriptor for an array of objects + * + * @param struct_ Struct packing the values + * + * @param field_name_ Field name in the struct containing the array + * + * @param max_len_ Maximum number of elements in the array + * + * @param len_field_ Field name in the struct for the number of elements + * in the array + * + * @param elem_descr_ Element descriptor, pointer to a descriptor array + * + * @param elem_descr_len_ Number of elements in elem_descr_ + * + * Here's an example of use: + * + * struct person_height { + * const char *name; + * int height; + * }; + * + * struct people_heights { + * struct person_height heights[10]; + * size_t heights_len; + * }; + * + * struct json_obj_descr person_height_descr[] = { + * JSON_OBJ_DESCR_PRIM(struct person_height, name, JSON_TOK_STRING), + * JSON_OBJ_DESCR_PRIM(struct person_height, height, JSON_TOK_NUMBER), + * }; + * + * struct json_obj_descr array[] = { + * JSON_OBJ_DESCR_OBJ_ARRAY(struct people_heights, heights, 10, + * heights_len, person_height_descr, + * ARRAY_SIZE(person_height_descr)), + * }; + */ +#define JSON_OBJ_DESCR_OBJ_ARRAY(struct_, field_name_, max_len_, len_field_, \ + elem_descr_, elem_descr_len_) \ + { \ + JSON_FIELD_NAME(field_name_), \ + .align = __alignof__(struct_), .type = JSON_TOK_LIST_START, \ + .offset = offsetof(struct_, field_name_), \ + .array = { \ + .element_descr = \ + &(struct json_obj_descr){ \ + .align = __alignof__(struct_), \ + .type = JSON_TOK_OBJECT_START, \ + .offset = offsetof(struct_, len_field_), \ + .object = \ + { \ + .sub_descr = elem_descr_, \ + .sub_descr_len = elem_descr_len_, \ + }, \ + }, \ + .n_elements = (max_len_), \ + }, \ + } + +/** + * @brief Helper macro to declare a descriptor for an array of array + * + * @param struct_ Struct packing the values + * + * @param field_name_ Field name in the struct containing the array + * + * @param max_len_ Maximum number of elements in the array + * + * @param len_field_ Field name in the struct for the number of elements + * in the array + * + * @param elem_descr_ Element descriptor, pointer to a descriptor array + * + * @param elem_descr_len_ Number of elements in elem_descr_ + * + * Here's an example of use: + * + * struct person_height { + * const char *name; + * int height; + * }; + * + * struct person_heights_array { + * struct person_height heights; + * } + * + * struct people_heights { + * struct person_height_array heights[10]; + * size_t heights_len; + * }; + * + * struct json_obj_descr person_height_descr[] = { + * JSON_OBJ_DESCR_PRIM(struct person_height, name, JSON_TOK_STRING), + * JSON_OBJ_DESCR_PRIM(struct person_height, height, JSON_TOK_NUMBER), + * }; + * + * struct json_obj_descr person_height_array_descr[] = { + * JSON_OBJ_DESCR_OBJECT(struct person_heights_array, + * heights, person_heigth_descr), + * }; + * + * struct json_obj_descr array_array[] = { + * JSON_OBJ_DESCR_ARRAY_ARRAY(struct people_heights, heights, 10, + * heights_len, person_height_array_descr, + * ARRAY_SIZE(person_height_array_descr)), + * }; + */ +#define JSON_OBJ_DESCR_ARRAY_ARRAY(struct_, field_name_, max_len_, len_field_, \ + elem_descr_, elem_descr_len_) \ + { \ + JSON_FIELD_NAME(field_name_), \ + .align = __alignof__(struct_), .type = JSON_TOK_LIST_START, \ + .offset = offsetof(struct_, field_name_), \ + .array = { \ + .element_descr = \ + &(struct json_obj_descr){ \ + .align = __alignof__(struct_), \ + .type = JSON_TOK_LIST_START, \ + .offset = offsetof(struct_, len_field_), \ + .object = \ + { \ + .sub_descr = elem_descr_, \ + .sub_descr_len = elem_descr_len_, \ + }, \ + }, \ + .n_elements = (max_len_), \ + }, \ + } + +/** + * @brief Variant of JSON_OBJ_DESCR_PRIM that can be used when the + * structure and JSON field names differ. + * + * This is useful when the JSON field is not a valid C identifier. + * + * @param struct_ Struct packing the values. + * + * @param json_field_name_ String, field name in JSON strings + * + * @param struct_field_name_ Field name in the struct + * + * @param type_ Token type for JSON value corresponding to a primitive + * type. + * + * @see JSON_OBJ_DESCR_PRIM + */ +#define JSON_OBJ_DESCR_PRIM_NAMED(struct_, json_field_name_, \ + struct_field_name_, type_) \ + { \ + JSON_FIELD_NAME(json_field_name_), \ + .align = __alignof__(struct_), .type = type_, \ + .offset = offsetof(struct_, struct_field_name_), \ + } + +/** + * @brief Variant of JSON_OBJ_DESCR_OBJECT that can be used when the + * structure and JSON field names differ. + * + * This is useful when the JSON field is not a valid C identifier. + * + * @param struct_ Struct packing the values + * + * @param json_field_name_ String, field name in JSON strings + * + * @param struct_field_name_ Field name in the struct + * + * @param sub_descr_ Array of json_obj_descr describing the subobject + * + * @see JSON_OBJ_DESCR_OBJECT + */ +#define JSON_OBJ_DESCR_OBJECT_NAMED(struct_, json_field_name_, \ + struct_field_name_, sub_descr_) \ + { \ + JSON_FIELD_NAME(json_field_name_), \ + .align = __alignof__(struct_), .type = JSON_TOK_OBJECT_START, \ + .offset = offsetof(struct_, struct_field_name_), \ + .object = { \ + .sub_descr = sub_descr_, \ + .sub_descr_len = ARRAY_SIZE(sub_descr_), \ + }, \ + } + +/** + * @brief Variant of JSON_OBJ_DESCR_ARRAY that can be used when the + * structure and JSON field names differ. + * + * This is useful when the JSON field is not a valid C identifier. + * + * @param struct_ Struct packing the values + * + * @param json_field_name_ String, field name in JSON strings + * + * @param struct_field_name_ Field name in the struct + * + * @param max_len_ Maximum number of elements in array + * + * @param len_field_ Field name in the struct for the number of elements + * in the array + * + * @param elem_type_ Element type, must be a primitive type + * + * @see JSON_OBJ_DESCR_ARRAY + */ +#define JSON_OBJ_DESCR_ARRAY_NAMED(struct_, json_field_name_, \ + struct_field_name_, max_len_, len_field_, \ + elem_type_) \ + { \ + JSON_FIELD_NAME(json_field_name_), \ + .align = __alignof__(struct_), .type = JSON_TOK_LIST_START, \ + .offset = offsetof(struct_, struct_field_name_), \ + .array = { \ + .element_descr = \ + &(struct json_obj_descr){ \ + .align = __alignof__(struct_), \ + .type = elem_type_, \ + .offset = offsetof(struct_, len_field_), \ + }, \ + .n_elements = (max_len_), \ + }, \ + } + +/** + * @brief Variant of JSON_OBJ_DESCR_OBJ_ARRAY that can be used when + * the structure and JSON field names differ. + * + * This is useful when the JSON field is not a valid C identifier. + * + * @param struct_ Struct packing the values + * + * @param json_field_name_ String, field name of the array in JSON strings + * + * @param struct_field_name_ Field name in the struct containing the array + * + * @param max_len_ Maximum number of elements in the array + * + * @param len_field_ Field name in the struct for the number of elements + * in the array + * + * @param elem_descr_ Element descriptor, pointer to a descriptor array + * + * @param elem_descr_len_ Number of elements in elem_descr_ + * + * Here's an example of use: + * + * struct person_height { + * const char *name; + * int height; + * }; + * + * struct people_heights { + * struct person_height heights[10]; + * size_t heights_len; + * }; + * + * struct json_obj_descr person_height_descr[] = { + * JSON_OBJ_DESCR_PRIM(struct person_height, name, JSON_TOK_STRING), + * JSON_OBJ_DESCR_PRIM(struct person_height, height, JSON_TOK_NUMBER), + * }; + * + * struct json_obj_descr array[] = { + * JSON_OBJ_DESCR_OBJ_ARRAY_NAMED(struct people_heights, + * "people-heights", heights, + * 10, heights_len, + * person_height_descr, + * ARRAY_SIZE(person_height_descr)), + * }; + */ +#define JSON_OBJ_DESCR_OBJ_ARRAY_NAMED( \ + struct_, json_field_name_, struct_field_name_, max_len_, len_field_, \ + elem_descr_, elem_descr_len_) \ + { \ + JSON_FIELD_NAME(json_field_name_), \ + .align = __alignof__(struct_), .type = JSON_TOK_LIST_START, \ + .offset = offsetof(struct_, struct_field_name_), \ + .element_descr = \ + &(struct json_obj_descr){ \ + .align = __alignof__(struct_), \ + .type = JSON_TOK_OBJECT_START, \ + .offset = offsetof(struct_, len_field_), \ + .object = \ + { \ + .sub_descr = elem_descr_, \ + .sub_descr_len = elem_descr_len_, \ + }, \ + }, \ + .n_elements = (max_len_), \ + } + +/** + * @brief Parses the JSON-encoded object pointer to by @a json, with + * size @a len, according to the descriptor pointed to by @a descr. + * Values are stored in a struct pointed to by @a val. Set up the + * descriptor like this: + * + * struct s { int foo; char *bar; } + * struct json_obj_descr descr[] = { + * JSON_OBJ_DESCR_PRIM(struct s, foo, JSON_TOK_NUMBER), + * JSON_OBJ_DESCR_PRIM(struct s, bar, JSON_TOK_STRING), + * }; + * + * Since this parser is designed for machine-to-machine communications, some + * liberties were taken to simplify the design: + * (1) strings are not unescaped (but only valid escape sequences are + * accepted); + * (2) no UTF-8 validation is performed; and + * (3) only integer numbers are supported (no strtod() in the minimal libc). + * + * @param json Pointer to JSON-encoded value to be parsed + * + * @param len Length of JSON-encoded value + * + * @param descr Pointer to the descriptor array + * + * @param descr_len Number of elements in the descriptor array. Must be less + * than 31 due to implementation detail reasons (if more fields are + * necessary, use two descriptors) + * + * @param val Pointer to the struct to hold the decoded values + * + * @return < 0 if error, bitmap of decoded fields on success (bit 0 + * is set if first field in the descriptor has been properly decoded, etc). + */ +int json_obj_parse(char *json, + size_t len, + const struct json_obj_descr *descr, + size_t descr_len, + void *val); + +/** + * @brief Escapes the string so it can be used to encode JSON objects + * + * @param str The string to escape; the escape string is stored the + * buffer pointed to by this parameter + * + * @param len Points to a size_t containing the size before and after + * the escaping process + * + * @param buf_size The size of buffer str points to + * + * @return 0 if string has been escaped properly, or -ENOMEM if there + * was not enough space to escape the buffer + */ +ssize_t json_escape(char *str, size_t *len, size_t buf_size); + +/** + * @brief Calculates the JSON-escaped string length + * + * @param str The string to analyze + * + * @param len String size + * + * @return The length str would have if it were escaped + */ +size_t json_calc_escaped_len(const char *str, size_t len); + +/** + * @brief Calculates the string length to fully encode an object + * + * @param descr Pointer to the descriptor array + * + * @param descr_len Number of elements in the descriptor array + * + * @param val Struct holding the values + * + * @return Number of bytes necessary to encode the values if >0, + * an error code is returned. + */ +ssize_t json_calc_encoded_len(const struct json_obj_descr *descr, + size_t descr_len, + const void *val); + +/** + * @brief Encodes an object in a contiguous memory location + * + * @param descr Pointer to the descriptor array + * + * @param descr_len Number of elements in the descriptor array + * + * @param val Struct holding the values + * + * @param buffer Buffer to store the JSON data + * + * @param buf_size Size of buffer, in bytes, with space for the terminating + * NUL character + * + * @return 0 if object has been successfully encoded. A negative value + * indicates an error (as defined on errno.h). + */ +int json_obj_encode_buf(const struct json_obj_descr *descr, + size_t descr_len, + const void *val, + char *buffer, + size_t buf_size); + +/** + * @brief Encodes an object using an arbitrary writer function + * + * @param descr Pointer to the descriptor array + * + * @param descr_len Number of elements in the descriptor array + * + * @param val Struct holding the values + * + * @param append_bytes Function to append bytes to the output + * + * @param data Data pointer to be passed to the append_bytes callback + * function. + * + * @return 0 if object has been successfully encoded. A negative value + * indicates an error. + */ +int json_obj_encode_full(const struct json_obj_descr *descr, + size_t descr_len, + const void *val, + json_append_bytes_t append_bytes, + void *data, + bool escape_key); +static inline int json_obj_encode(const struct json_obj_descr *descr, + size_t descr_len, + const void *val, + json_append_bytes_t append_bytes, + void *data) +{ + + return json_obj_encode_full(descr, descr_len, val, append_bytes, data, + true); +} +/** + * @brief Encodes an array using an arbitrary writer function + * + * @param descr Pointer to the descriptor array + * + * @param descr_len Number of elements in the descriptor array + * + * @param val Struct holding the values + * + * @param append_bytes Function to append bytes to the output + * + * @param data Data pointer to be passed to the append_bytes callback + * function. + * + * @return 0 if object has been successfully encoded. A negative value + * indicates an error. + */ +int json_arr_encode_full(const struct json_obj_descr *descr, + const void *val, + json_append_bytes_t append_bytes, + void *data, + bool escape_key); +static inline int json_arr_encode(const struct json_obj_descr *descr, + const void *val, + json_append_bytes_t append_bytes, + void *data) +{ + return json_arr_encode_full(descr, val, append_bytes, data, true); +} + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ +#endif /* ZEPHYR_INCLUDE_DATA_JSON_H_ */ diff --git a/src/samples/techempower/json.lua b/src/samples/techempower/json.lua new file mode 100644 index 000000000..098e7b29a --- /dev/null +++ b/src/samples/techempower/json.lua @@ -0,0 +1,400 @@ +-- +-- json.lua +-- +-- Copyright (c) 2019 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/src/samples/techempower/techempower.c b/src/samples/techempower/techempower.c new file mode 100644 index 000000000..6b9ed8efd --- /dev/null +++ b/src/samples/techempower/techempower.c @@ -0,0 +1,514 @@ +/* + * lwan - web server + * Copyright (c) 2014 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include +#include + +#include "lwan-private.h" +#include "lwan-cache.h" +#include "lwan-config.h" +#include "lwan-template.h" +#include "lwan-mod-lua.h" +#include "int-to-str.h" + +#include "database.h" +#include "json.h" + +enum db_connect_type { DB_CONN_MYSQL, DB_CONN_SQLITE }; + +static struct db_connection_params { + enum db_connect_type type; + union { + struct { + const char *user; + const char *password; + const char *database; + const char *hostname; + } mysql; + struct { + const char *path; + const char **pragmas; + } sqlite; + }; +} db_connection_params; + +static const char hello_world[] = "Hello, World!"; +static const char random_number_query[] = + "SELECT randomNumber, id FROM world WHERE id=?"; +static const char cached_random_number_query[] = + "SELECT randomNumber, id FROM world WHERE id=?"; + +struct Fortune { + struct { + coro_function_t generator; + + int id; + char *message; + } item; +}; + +DEFINE_ARRAY_TYPE_INLINEFIRST(fortune_array, struct Fortune) + +static const char fortunes_template_str[] = + "" + "" + "Fortunes" + "" + "" + "" + "{{#item}}" + "" + "{{/item}}" + "
idmessage
{{item.id}}{{item.message}}
" + "" + ""; + +static int fortune_list_generator(struct coro *coro, void *data); + +#undef TPL_STRUCT +#define TPL_STRUCT struct Fortune +static const struct lwan_var_descriptor fortune_desc[] = { + TPL_VAR_SEQUENCE(item, + fortune_list_generator, + ((const struct lwan_var_descriptor[]){ + TPL_VAR_INT(item.id), + TPL_VAR_STR_ESCAPE(item.message), + TPL_VAR_SENTINEL, + })), + TPL_VAR_SENTINEL, +}; + +static struct lwan_tpl *fortune_tpl; + +struct hello_world_json { + const char *message; +}; +static const struct json_obj_descr hello_world_json_desc[] = { + JSON_OBJ_DESCR_PRIM(struct hello_world_json, message, JSON_TOK_STRING), +}; + +struct db_json { + int id; + int randomNumber; +}; +static const struct json_obj_descr db_json_desc[] = { + JSON_OBJ_DESCR_PRIM(struct db_json, id, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM(struct db_json, randomNumber, JSON_TOK_NUMBER), +}; + +struct queries_json { + struct db_json queries[500]; + size_t queries_len; +}; +static const struct json_obj_descr queries_array_desc = + JSON_OBJ_DESCR_OBJ_ARRAY(struct queries_json, + queries, + 500, + queries_len, + db_json_desc, + N_ELEMENTS(db_json_desc)); + +LWAN_LAZY_THREAD_LOCAL(struct db *, get_db) +{ + struct db *db; + + switch (db_connection_params.type) { + case DB_CONN_MYSQL: + db = db_connect_mysql(db_connection_params.mysql.hostname, + db_connection_params.mysql.user, + db_connection_params.mysql.password, + db_connection_params.mysql.database); + break; + + case DB_CONN_SQLITE: + db = db_connect_sqlite(db_connection_params.sqlite.path, true, + db_connection_params.sqlite.pragmas); + break; + default: + __builtin_unreachable(); + } + + if (!db) { + lwan_status_critical("Could not connect to the database"); + __builtin_unreachable(); + } + + return db; +} + +static int append_to_strbuf(const char *bytes, size_t len, void *data) +{ + struct lwan_strbuf *strbuf = data; + + return !lwan_strbuf_append_str(strbuf, bytes, len); +} + +static enum lwan_http_status +json_response_obj(struct lwan_response *response, + const struct json_obj_descr *descr, + size_t descr_len, + const void *data) +{ + if (json_obj_encode_full(descr, descr_len, data, append_to_strbuf, + response->buffer, false) != 0) + return HTTP_INTERNAL_ERROR; + + response->mime_type = "application/json"; + return HTTP_OK; +} + +static enum lwan_http_status +json_response_arr(struct lwan_response *response, + const struct json_obj_descr *descr, + const void *data) +{ + if (json_arr_encode_full(descr, data, append_to_strbuf, response->buffer, + false) != 0) + return HTTP_INTERNAL_ERROR; + + response->mime_type = "application/json"; + return HTTP_OK; +} + +LWAN_HANDLER(json) +{ + struct hello_world_json j = {.message = hello_world}; + + request->flags |= RESPONSE_NO_EXPIRES; + + return json_response_obj(response, hello_world_json_desc, + N_ELEMENTS(hello_world_json_desc), &j); +} + +static bool db_query_key(struct db_stmt *stmt, struct db_json *out, int key) +{ + struct db_row row = {.u.i = key + 1}; + if (UNLIKELY(!db_stmt_bind(stmt, &row))) + return false; + + long random_number; + long id; + if (UNLIKELY(!db_stmt_step(stmt, &random_number, &id))) + return false; + + out->id = (int)id; + out->randomNumber = (int)random_number; + + return true; +} + +static inline bool db_query(struct db_stmt *stmt, struct db_json *out) +{ + uint64_t random_num = lwan_random_uint64() % 10000ull; + return db_query_key(stmt, out, (int)random_num); +} + +LWAN_HANDLER(db) +{ + struct db_stmt *stmt = db_prepare_stmt(get_db(), random_number_query, "i", "ii"); + struct db_json db_json; + + if (UNLIKELY(!stmt)) { + lwan_status_debug("preparing stmt failed"); + return HTTP_INTERNAL_ERROR; + } + + bool queried = db_query(stmt, &db_json); + + db_stmt_finalize(stmt); + + if (!queried) + return HTTP_INTERNAL_ERROR; + + request->flags |= RESPONSE_NO_EXPIRES; + + return json_response_obj(response, db_json_desc, N_ELEMENTS(db_json_desc), + &db_json); +} + +static long get_number_of_queries(struct lwan_request *request) +{ + const char *queries_str = lwan_request_get_query_param(request, "queries"); + return LIKELY(queries_str) + ? LWAN_MIN(500, LWAN_MAX(1, parse_long(queries_str, -1))) + : 1; +} + +LWAN_HANDLER(queries) +{ + enum lwan_http_status ret = HTTP_INTERNAL_ERROR; + long queries = get_number_of_queries(request); + + struct db_stmt *stmt = db_prepare_stmt(get_db(), random_number_query, "i", "ii"); + if (UNLIKELY(!stmt)) + return HTTP_INTERNAL_ERROR; + + struct queries_json qj = {.queries_len = (size_t)queries}; + for (long i = 0; i < queries; i++) { + if (!db_query(stmt, &qj.queries[i])) + goto out; + } + + /* Avoid reallocations/copies while building response. Each response + * has ~32bytes. 500 queries (max) should be less than 16384 bytes, + * so this is a good approximation. */ + lwan_strbuf_grow_to(response->buffer, (size_t)(32l * queries)); + + request->flags |= RESPONSE_NO_EXPIRES; + + ret = json_response_arr(response, &queries_array_desc, &qj); +out: + db_stmt_finalize(stmt); + + return ret; +} + +static struct cache *cached_queries_cache; +struct db_json_cached { + struct cache_entry base; + struct db_json db_json; +}; + +static struct cache_entry *cached_queries_new(const void *keyptr, + void *context, + void *create_ctx + __attribute__((unused))) +{ + struct db_json_cached *entry; + struct db_stmt *stmt; + int key = (int)(uintptr_t)keyptr; + + entry = malloc(sizeof(*entry)); + if (UNLIKELY(!entry)) + return NULL; + + stmt = db_prepare_stmt(get_db(), cached_random_number_query, "i", "ii"); + if (UNLIKELY(!stmt)) { + free(entry); + return NULL; + } + + if (!db_query_key(stmt, &entry->db_json, key)) { + free(entry); + entry = NULL; + } + + db_stmt_finalize(stmt); + + return (struct cache_entry *)entry; +} + +static void cached_queries_free(struct cache_entry *entry, void *context) +{ + free(entry); +} + +LWAN_HANDLER(cached_queries) +{ + long queries = get_number_of_queries(request); + + struct queries_json qj = {.queries_len = (size_t)queries}; + for (long i = 0; i < queries; i++) { + struct db_json_cached *jc; + int key = (int)lwan_random_uint64() % 10000; + int error; + + jc = (struct db_json_cached *)cache_get_and_ref_entry( + cached_queries_cache, (void *)(intptr_t)key, &error); + + qj.queries[i] = jc->db_json; + + /* No need to unref the cache entry here: cache is marked as read-only, + * so it would be a no-op. */ + } + + /* Avoid reallocations/copies while building response. Each response + * has ~32bytes. 500 queries (max) should be less than 16384 bytes, + * so this is a good approximation. */ + lwan_strbuf_grow_to(response->buffer, (size_t)(32l * queries)); + + request->flags |= RESPONSE_NO_EXPIRES; + return json_response_arr(response, &queries_array_desc, &qj); +} + +LWAN_HANDLER(plaintext) +{ + lwan_strbuf_set_static(response->buffer, hello_world, + sizeof(hello_world) - 1); + + request->flags |= RESPONSE_NO_EXPIRES; + response->mime_type = "text/plain"; + return HTTP_OK; +} + +static int fortune_compare(const void *a, const void *b) +{ + const struct Fortune *fortune_a = (const struct Fortune *)a; + const struct Fortune *fortune_b = (const struct Fortune *)b; + + return strcmp(fortune_a->item.message, fortune_b->item.message); +} + +static bool append_fortune(struct coro *coro, + struct fortune_array *fortunes, + int id, + const char *message) +{ + struct Fortune *fortune; + char *message_copy; + + message_copy = coro_strdup(coro, message); + if (UNLIKELY(!message_copy)) + return false; + + fortune = fortune_array_append(fortunes); + if (UNLIKELY(!fortune)) + return false; + + fortune->item.id = id; + fortune->item.message = message_copy; + + return true; +} + +static int fortune_list_generator(struct coro *coro, void *data) +{ + static const char fortune_query[] = "SELECT * FROM Fortune"; + struct Fortune *fortune = data; + struct fortune_array fortunes; + struct db_stmt *stmt; + + stmt = db_prepare_stmt(get_db(), fortune_query, "", "is"); + if (UNLIKELY(!stmt)) + return 0; + + fortune_array_init(&fortunes); + + long id; + char fortune_buffer[256]; + while (db_stmt_step(stmt, &id, &fortune_buffer, sizeof(fortune_buffer))) { + if (!append_fortune(coro, &fortunes, (int)id, fortune_buffer)) + goto out; + } + + if (!append_fortune(coro, &fortunes, 0, + "Additional fortune added at request time.")) + goto out; + + fortune_array_sort(&fortunes, fortune_compare); + + struct Fortune *iter; + LWAN_ARRAY_FOREACH (&fortunes, iter) { + fortune->item.id = iter->item.id; + fortune->item.message = iter->item.message; + coro_yield(coro, 1); + } + +out: + fortune_array_reset(&fortunes); + db_stmt_finalize(stmt); + return 0; +} + +LWAN_HANDLER(fortunes) +{ + struct Fortune fortune; + + lwan_strbuf_grow_to(response->buffer, 1500); + + if (UNLIKELY(!lwan_tpl_apply_with_buffer(fortune_tpl, response->buffer, + &fortune))) + return HTTP_INTERNAL_ERROR; + + request->flags |= RESPONSE_NO_EXPIRES; + response->mime_type = "text/html; charset=UTF-8"; + return HTTP_OK; +} + +LWAN_HANDLER(quit_lwan) +{ + exit(0); + return HTTP_OK; +} + +int main(void) +{ + struct lwan l; + + lwan_init(&l); + + if (getenv("USE_MYSQL")) { + db_connection_params = (struct db_connection_params){ + .type = DB_CONN_MYSQL, + .mysql.user = getenv("MYSQL_USER"), + .mysql.password = getenv("MYSQL_PASS"), + .mysql.hostname = getenv("MYSQL_HOST"), + .mysql.database = getenv("MYSQL_DB"), + }; + + if (!db_connection_params.mysql.user) + lwan_status_critical("No MySQL user provided"); + if (!db_connection_params.mysql.password) + lwan_status_critical("No MySQL password provided"); + if (!db_connection_params.mysql.hostname) + lwan_status_critical("No MySQL hostname provided"); + if (!db_connection_params.mysql.database) + lwan_status_critical("No MySQL database provided"); + } else { + static const char *pragmas[] = {"PRAGMA mmap_size=44040192", + "PRAGMA journal_mode=OFF", + "PRAGMA locking_mode=EXCLUSIVE", NULL}; + db_connection_params = (struct db_connection_params){ + .type = DB_CONN_SQLITE, + .sqlite.path = "techempower.db", + .sqlite.pragmas = pragmas, + }; + } + + fortune_tpl = lwan_tpl_compile_string_full( + fortunes_template_str, fortune_desc, LWAN_TPL_FLAG_CONST_TEMPLATE); + if (!fortune_tpl) + lwan_status_critical("Could not compile fortune templates"); + + cached_queries_cache = cache_create_full(cached_queries_new, + cached_queries_free, + hash_int_new, + NULL, + 3600 /* 1 hour */); + if (!cached_queries_cache) + lwan_status_critical("Could not create cached queries cache"); + /* Pre-populate the cache and make it read-only to avoid locking in the fast + * path. */ + for (int i = 0; i < 10000; i++) { + int error; + (void)cache_get_and_ref_entry(cached_queries_cache, (void *)(intptr_t)i, + &error); + } + cache_make_read_only(cached_queries_cache); + + lwan_main_loop(&l); + + cache_destroy(cached_queries_cache); + lwan_tpl_free(fortune_tpl); + lwan_shutdown(&l); + + return 0; +} diff --git a/src/samples/techempower/techempower.conf b/src/samples/techempower/techempower.conf new file mode 100644 index 000000000..f912bf219 --- /dev/null +++ b/src/samples/techempower/techempower.conf @@ -0,0 +1,31 @@ +listener *:8080 + +site { + # For main TWFB benchmarks + &plaintext /plaintext + &json /json + &db /db + &queries /queries + &cached_queries /cached-queries + &fortunes /fortunes + + # For Lua version of TWFB benchmarks + lua /lua. { + default_type = text/plain + cache period = 1h + script='''local json = require "json" + + function handle_get_plaintext(req) + req:set_response("Hello, World!") + end + + function handle_get_json(req) + req:set_headers({["Content-Type"]="application/json"}) + req:set_response(json.encode({message="Hello, World!"})) + end''' + } + + # For test harness + &quit_lwan /quit-lwan + &plaintext /hello +} diff --git a/techempower/techempower.db b/src/samples/techempower/techempower.db similarity index 100% rename from techempower/techempower.db rename to src/samples/techempower/techempower.db diff --git a/src/samples/websocket/CMakeLists.txt b/src/samples/websocket/CMakeLists.txt new file mode 100644 index 000000000..d85113d04 --- /dev/null +++ b/src/samples/websocket/CMakeLists.txt @@ -0,0 +1,22 @@ +add_executable(websocket + main.c +) + +target_link_libraries(websocket + ${LWAN_COMMON_LIBS} + ${ADDITIONAL_LIBRARIES} +) + +add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/websocket-sample.h + COMMAND bin2hex + ${CMAKE_SOURCE_DIR}/src/samples/websocket/index.html index_html > ${CMAKE_BINARY_DIR}/websocket-sample.h + DEPENDS ${CMAKE_SOURCE_DIR}/src/samples/websocket/index.html + bin2hex + COMMENT "Bundling websocket sample index" +) +add_custom_target(generate_websocket_sample + DEPENDS ${CMAKE_BINARY_DIR}/websocket-sample.h +) + +add_dependencies(websocket generate_websocket_sample) diff --git a/src/samples/websocket/index.html b/src/samples/websocket/index.html new file mode 100644 index 000000000..4f1f91266 --- /dev/null +++ b/src/samples/websocket/index.html @@ -0,0 +1,67 @@ + + + + + +

Lwan WebSocket demo!

+

Send-only sample: server is writing this continuously:

+

Disconnected

+

Echo server sample:

+

+

Server said this:

Disconnected

+

Chat sample:

+ Send message:

+ + + diff --git a/src/samples/websocket/main.c b/src/samples/websocket/main.c new file mode 100644 index 000000000..19a5a3d33 --- /dev/null +++ b/src/samples/websocket/main.c @@ -0,0 +1,219 @@ +/* + * lwan - web server + * Copyright (c) 2018 L. A. F. Pereira + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + */ + +#include +#include + +#include "lwan.h" +#include "lwan-pubsub.h" + +#include "websocket-sample.h" + +static struct lwan_pubsub_topic *chat; + +/* This is a write-only sample of the API: it just sends random integers + * over a WebSockets connection. */ +LWAN_HANDLER_ROUTE(ws_write, "/ws-write") +{ + enum lwan_http_status status = lwan_request_websocket_upgrade(request); + + if (status != HTTP_SWITCHING_PROTOCOLS) + return status; + + while (true) { + lwan_strbuf_printf(response->buffer, "Some random integer: %d", rand()); + lwan_response_websocket_write_text(request); + lwan_request_sleep(request, 1000); + } + + __builtin_unreachable(); +} + +static void free_strbuf(void *data) +{ + lwan_strbuf_free((struct lwan_strbuf *)data); +} + +/* This is a slightly more featured echo server that tells how many seconds + * passed since the last message has been received, and keeps sending it back + * again and again. */ +LWAN_HANDLER_ROUTE(ws_read, "/ws-read") +{ + enum lwan_http_status status = lwan_request_websocket_upgrade(request); + struct lwan_strbuf *last_msg_recv; + int seconds_since_last_msg = 0; + + if (status != HTTP_SWITCHING_PROTOCOLS) + return status; + + last_msg_recv = lwan_strbuf_new(); + if (!last_msg_recv) + return HTTP_INTERNAL_ERROR; + coro_defer(request->conn->coro, free_strbuf, last_msg_recv); + + while (true) { + switch (lwan_response_websocket_read(request)) { + case ENOTCONN: /* read() called before connection is websocket */ + case ECONNRESET: /* Client closed the connection */ + goto out; + + case EAGAIN: /* Nothing is available */ + lwan_strbuf_printf(response->buffer, + "Last message was received %d seconds ago: %.*s", + seconds_since_last_msg, + (int)lwan_strbuf_get_length(last_msg_recv), + lwan_strbuf_get_buffer(last_msg_recv)); + lwan_response_websocket_write_text(request); + + lwan_request_sleep(request, 1000); + seconds_since_last_msg++; + break; + + case 0: /* We got something! Copy it to echo it back */ + lwan_strbuf_set(last_msg_recv, + lwan_strbuf_get_buffer(response->buffer), + lwan_strbuf_get_length(response->buffer)); + + seconds_since_last_msg = 0; + + break; + } + } + +out: + /* We abort the coroutine here because there's not much we can do at this + * point as this isn't a HTTP connection anymore. */ + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); +} + +static void unsub_chat(void *data1, void *data2) +{ + lwan_pubsub_unsubscribe((struct lwan_pubsub_topic *)data1, + (struct lwan_pubsub_subscriber *)data2); +} + +static void pub_depart_message(void *data1, void *data2) +{ + char buffer[128]; + int r; + + r = snprintf(buffer, sizeof(buffer), "*** User%d has departed the chat!\n", + (int)(intptr_t)data2); + if (r < 0 || (size_t)r >= sizeof(buffer)) + return; + + lwan_pubsub_publish((struct lwan_pubsub_topic *)data1, buffer, (size_t)r); +} + +LWAN_HANDLER_ROUTE(ws_chat, "/ws-chat") +{ + struct lwan_pubsub_subscriber *sub; + struct lwan_pubsub_msg *msg; + enum lwan_http_status status; + static int total_user_count; + int user_id; + + sub = lwan_pubsub_subscribe(chat); + if (!sub) + return HTTP_INTERNAL_ERROR; + coro_defer2(request->conn->coro, unsub_chat, chat, sub); + + status = lwan_request_websocket_upgrade(request); + if (status != HTTP_SWITCHING_PROTOCOLS) + return status; + + user_id = ATOMIC_INC(total_user_count); + + lwan_strbuf_printf(response->buffer, "*** Welcome to the chat, User%d!\n", + user_id); + lwan_response_websocket_write_text(request); + + coro_defer2(request->conn->coro, pub_depart_message, chat, + (void *)(intptr_t)user_id); + lwan_pubsub_publishf(chat, "*** User%d has joined the chat!\n", user_id); + + const int websocket_fd = request->fd; + const int sub_fd = lwan_pubsub_get_notification_fd(sub); + while (true) { + int resumed_fd = + lwan_request_awaitv_any(request, websocket_fd, CONN_CORO_WANT_READ, + sub_fd, CONN_CORO_WANT_READ, -1); + + if (resumed_fd == sub_fd) { + while ((msg = lwan_pubsub_consume(sub))) { + const struct lwan_value *value = lwan_pubsub_msg_value(msg); + + lwan_strbuf_set(response->buffer, value->value, value->len); + + /* Mark as done before writing: websocket_write() can abort the + * coroutine and we want to drop the reference before this + * happens. */ + lwan_pubsub_msg_done(msg); + + lwan_response_websocket_write_text(request); + } + } else if (resumed_fd == websocket_fd) { + switch (lwan_response_websocket_read(request)) { + case ENOTCONN: /* read() called before connection is websocket */ + case ECONNRESET: /* Client closed the connection */ + goto out; + + case 0: /* We got something! Copy it to echo it back */ + lwan_pubsub_publishf( + chat, "User%d: %.*s\n", user_id, + (int)lwan_strbuf_get_length(response->buffer), + lwan_strbuf_get_buffer(response->buffer)); + } + } else if (resumed_fd < 0) { + lwan_status_error("error from fd %d", -resumed_fd); + goto out; + } else { + lwan_status_warning("not awaiting on fd %d, ignoring", resumed_fd); + } + } + +out: + /* We abort the coroutine here because there's not much we can do at this + * point as this isn't a HTTP connection anymore. */ + coro_yield(request->conn->coro, CONN_CORO_ABORT); + __builtin_unreachable(); +} + +LWAN_HANDLER_ROUTE(index, "/") +{ + request->response.mime_type = "text/html"; + lwan_strbuf_set_static(response->buffer, + index_html_value.value, + index_html_value.len); + + return HTTP_OK; +} + +int main(void) +{ + chat = lwan_pubsub_new_topic(); + + lwan_main(); + + lwan_pubsub_free_topic(chat); + + return 0; +} diff --git a/tools/benchmark.py b/src/scripts/benchmark.py similarity index 74% rename from tools/benchmark.py rename to src/scripts/benchmark.py index 9c0d7358d..71a21e2dd 100755 --- a/tools/benchmark.py +++ b/src/scripts/benchmark.py @@ -1,10 +1,24 @@ #!/usr/bin/python -import sys import json -import commands +import os +import subprocess +import sys import time +LWAN_PATH = './src/bin/testrunner/testrunner' +for arg in sys.argv[1:]: + if not arg.startswith('-') and os.path.exists(arg): + LWAN_PATH = arg + sys.argv.remove(arg) + +print('Using', LWAN_PATH, 'for lwan') + +lwan=subprocess.Popen( + [LWAN_PATH], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT +) + try: import matplotlib.pyplot as plt except ImportError: @@ -16,25 +30,25 @@ def clearstderrline(): def weighttp(url, n_threads, n_connections, n_requests, keep_alive): - keep_alive = '-k' if keep_alive else '' - command = 'weighttp %(keep_alive)s ' \ - '-t %(n_threads)d ' \ - '-c %(n_connections)d ' \ - '-n %(n_requests)d ' \ - '-j ' \ - '%(url)s 2> /dev/null' % locals() - clearstderrline() - sys.stderr.write('*** %s\r' % command) - - output = commands.getoutput(command) - - return json.loads(output) - - -def weighttp_has_json_output(): - output = commands.getoutput('weighttp -j') - return not 'unknown option: -j' in output + sys.stderr.write(f'*** Running weighttp on {url}: threads: {n_threads} ' \ + f'conns: {n_connections} reqs: {n_requests} ' \ + f'{"keep-alive" if keep_alive else ""} \r') + + command = [ + './src/bin/tools/weighttp', + '-k' if keep_alive else '', + '-t', n_threads, + '-c', n_connections, + '-n', n_requests, + '-j', + url + ] + print(command) + output = subprocess.run(command, capture_output=True) + output.check_returncode() + + return json.loads(output.stdout) def steprange(initial, final, steps=10): @@ -77,7 +91,7 @@ def cmdlineintarg(arg, default=0): try: value = int(sys.argv[index]) except ValueError: - print 'Argument is of invalid type for argument %s, assuming default (%d)' % (arg, default) + print('Argument is of invalid type for argument %s, assuming default (%d)' % (arg, default)) finally: del sys.argv[index] return value @@ -85,15 +99,15 @@ def cmdlineintarg(arg, default=0): class CSVOutput: def header(self): - print 'keep_alive,n_connections,rps,kbps,2xx,3xx,4xx,5xx' + print('keep_alive,n_connections,rps,kbps,2xx,3xx,4xx,5xx') def footer(self): clearstderrline() def log(self, keep_alive, n_connections, rps, kbps, _2xx, _3xx, _4xx, _5xx): clearstderrline() - print ','.join(str(token) for token in - (int(keep_alive), n_connections, rps, kbps, _2xx, _3xx, _4xx, _5xx)) + print(','.join(str(token) for token in + (int(keep_alive), n_connections, rps, kbps, _2xx, _3xx, _4xx, _5xx))) class MatplotlibOutput: @@ -134,11 +148,6 @@ def log(self, keep_alive, n_connections, rps, kbps, _2xx, _3xx, _4xx, _5xx): if __name__ == '__main__': - if not weighttp_has_json_output(): - print 'This script requires a special version of weighttp which supports JSON' - print 'output. Get it at http://github.com/lpereira/weighttp' - sys.exit(1) - plot = cmdlineboolarg('--plot') xkcd = cmdlineboolarg('--xkcd') n_threads = cmdlineintarg('--threads', 2) @@ -151,7 +160,7 @@ def log(self, keep_alive, n_connections, rps, kbps, _2xx, _3xx, _4xx, _5xx): if plt is None: if plot: - print 'Matplotlib not installed!' + print('Matplotlib not installed!') sys.exit(1) output = CSVOutput() elif plot: @@ -166,8 +175,9 @@ def log(self, keep_alive, n_connections, rps, kbps, _2xx, _3xx, _4xx, _5xx): status = results['status_codes'] output.log(keep_alive, n_connections, results['reqs_per_sec'], - results['kbyte_per_sec'], status['2xx'], status['3xx'], + results['kBps_per_sec'], status['2xx'], status['3xx'], status['4xx'], status['5xx']) sleepwithstatus('Waiting for keepalive connection timeout', keep_alive_timeout * 1.1) output.footer() + lwan.kill() diff --git a/src/scripts/gentables.py b/src/scripts/gentables.py new file mode 100755 index 000000000..195a496a0 --- /dev/null +++ b/src/scripts/gentables.py @@ -0,0 +1,393 @@ +#!/usr/bin/python +# Prototype decoder for Huffman-encoded HPACK data +# Copyright (c) 2022 L. A. F. Pereira +# +# How this works: This is a standard Huffman decoder, but instead of +# using a traditional binary tree and use each input bit to drive the +# traversal, going down a tree level for every input bit, it has a +# table where the index helps traverse this tree multiple levels in +# parallel. In practice, this means that for the shortest -- and thus +# common -- symbols, one can traverse the binary tree with a single +# memory access. This improves cache utilization and makes memory +# prefetching and branch prediction more useful. +# +# This is my first foray into data compression, so I'm sure I'm missing +# some obvious trick known by folks used to implementing these kinds of +# things. Some of the things I've identified that could be improved, +# so far: +# +# - Some branches could be removed, especially when processing +# incorrect inputs. +# - Some tables can be reduced and be converted to pure code. For +# instance, level2_11111010[] could be a tiny switch/case statement +# instead of 8 lines of L1 cache. Some tables, like +# level2_11110110[], do not have a lot of information stored in them +# (two entries, only 1 bit per entry is useful, etc.), which is a +# good candidate for something like this too. +# - GCC and Clang offer a way to say if a branch is likely or unlikely, +# and specify the probability for them. This is perfect because we +# know the probability -- it's exactly what the length of a Huffman +# code conveys. Might be a good idea to find a way to exploit this. +# +# The input for this script is a table obtained directly from the HPACK +# RFC. +# +# This script could potentially be cleaned up and written in a more +# generic way, but I just wanted to experiment with the idea without +# spending a lot of time on something that could potentially not work. + +from collections import defaultdict + +def pad_table(symbols): + table = {} + to_delete = set() + for symbol, bins in symbols.items(): + short_bin = int(bins[0], 2) + shift_by = len(bins[0]) + + if symbol == 256: # EOS + element = (0, -1) + else: + element = (symbol, shift_by) + + if len(bins) == 1: + if shift_by == 8: + table[(short_bin, short_bin)] = element + else: + short_bin <<= 8 - shift_by + from_code = short_bin + to_code = short_bin + (1 << (8 - shift_by)) - 1 + + table[(from_code, to_code)] = element + to_delete.add(symbol) + + next_table = defaultdict(lambda: {}) + for symbol, bins in symbols.items(): + if not symbol in to_delete: + next_table[bins[0]][symbol] = bins[1:] + + return table, next_table + +def print_table_contents(table): + for code in sorted(table.keys()): + symbol, length = table[code] + if length == 0: + continue + code_start, code_end = code + + # Instead of storing length as the total length of the symbol, we store + # the relative length from the current point. This simplifies the + # implementation of peek_byte() and (especially) consume(). + if code_start == code_end: + print(f"[{code_start}] = {{ {symbol}, {length} }},") + else: + print(f"[{code_start} ... {code_end}] = {{ {symbol}, {length} }},") + +def generate_level(level, next_table): + next_tables = [] + + print(f"static inline const struct h2_huffman_code *next_level{level}(uint8_t peeked_byte) {{") + + for bin, table in next_table.items(): + print(f"static const struct h2_huffman_code level{level}_{bin}[256] = {{") + table, next_table_ = pad_table(table) + print_table_contents(table) + if next_table_: + next_tables.append(next_table_) + print("};") + + generated = False + if len(next_table) == 2: + values = tuple(next_table.keys()) + value0 = int(values[0], 2) + value1 = int(values[1], 2) + + mask = value0 & ~value1 + if mask.bit_length() == 1: + print(f"return peeked_byte & {mask} ? level{level}_{values[0]} : level{level}_{values[1]};") + generated = True + + if not generated: + print("switch (peeked_byte) {") + for bin0 in next_table.keys(): + print(f"case 0b{bin0}: ") + print(f"return level{level}_{bin0};") + print("default: return NULL;") + print("}") + + print("}") + + return next_tables + +if __name__ == '__main__': + print("""#include +#include +#include +#include +#include +#include +#include + +#define LIKELY(x) x +#define UNLIKELY(x) x + +static inline uint64_t read64be(const void *ptr) { + uint64_t v; + memcpy(&v, ptr, 8); + return htobe64(v); +} + +static inline uint32_t read32be(const void *ptr) { + uint32_t v; + memcpy(&v, ptr, 4); + return htobe32(v); +} + +""") + + symbols = {} + for symbol, line in enumerate(open("huffman-table.txt")): + _, code = line.strip().split(") |", 1) + code, _ = code.split(" ", 1) + symbols[symbol] = code.split("|") + + first_level, next_table_first_level = pad_table(symbols) + + print("struct h2_huffman_code {") + print(" uint8_t symbol;") + print(" int8_t num_bits;") + print("};") + + print(f"static const struct h2_huffman_code level0[256] = {{") + print_table_contents(first_level) + print("};") + + for table in generate_level(0, next_table_first_level): + for table in generate_level(1, table): + # FIXME: some of the tables in level 2 are too big and convey very + # little information. maybe for these levels we could generate + # C code instead? these symbols should be really rare anyway + level2 = generate_level(2, table) + assert(not level2) + + # These have been inspired by Fabian Giesen's blog posts on "reading bits in + # far too many ways". Part 2, specifically: https://fgiesen.wordpress.com/2018/02/20/reading-bits-in-far-too-many-ways-part-2/ + print("""struct bit_reader { + const uint8_t *bitptr; + uint64_t bitbuf; + int64_t total_bitcount; + int bitcount; +}; + +static inline uint8_t peek_byte(struct bit_reader *reader) +{ + if (reader->bitcount < 8) { + if (reader->total_bitcount >= 64) { + reader->bitbuf |= read64be(reader->bitptr) >> reader->bitcount; + reader->bitptr += (63 - reader->bitcount + (reader->bitcount & 1)) >> 3; + reader->bitcount |= 56; + } else if (reader->total_bitcount >= 32) { + reader->bitbuf |= read32be(reader->bitptr) >> reader->bitcount; + reader->bitptr += (31 - reader->bitcount + (reader->bitcount & 1)) >> 3; + reader->bitcount |= 24; + } else { + reader->bitbuf |= *reader->bitptr >> reader->bitcount; + reader->bitptr += (7 - reader->bitcount + (reader->bitcount & 1)) >> 3; + reader->bitcount |= 8; + } + } + return reader->bitbuf >> 56; +} + +static inline bool consume(struct bit_reader *reader, int count) +{ + assert(count > 0); + reader->bitbuf <<= count; + reader->bitcount -= count; + reader->total_bitcount -= count; + return reader->total_bitcount > 0; +} +""") + + print("""DEFINE_RING_BUFFER_TYPE(uint8_ring_buffer, uint8_t, 64) + +struct lwan_h2_huffman_decoder { + struct bit_reader bit_reader; + struct uint8_ring_buffer buffer; +}; + +void lwan_h2_huffman_init(struct lwan_h2_huffman_decoder *huff, + const uint8_t *input, + size_t input_len) +{ + huff->bit_reader = (struct bit_reader){ + .bitptr = input, + .total_bitcount = (int64_t)input_len * 8, + }; + uint8_ring_buffer_init(&huff->buffer); +} + +ssize_t lwan_h2_huffman_next(struct lwan_h2_huffman_decoder *huff) +{ + struct bit_reader *reader = &huff->bit_reader; + struct uint8_ring_buffer *buffer = &huff->buffer; + + while (reader->total_bitcount > 7) { + if (uint8_ring_buffer_full(buffer)) + goto done; + + uint8_t peeked_byte = peek_byte(reader); + if (LIKELY(level0[peeked_byte].num_bits)) { + uint8_ring_buffer_put_copy(buffer, level0[peeked_byte].symbol); + consume(reader, level0[peeked_byte].num_bits); + assert(bit_reader.total_bitcount >= 0); + continue; + } + + if (!consume(reader, 8)) + return -1; + + const struct h2_huffman_code *level1 = next_level0(peeked_byte); + peeked_byte = peek_byte(reader); + if (level1[peeked_byte].num_bits) { + uint8_ring_buffer_put_copy(buffer, level1[peeked_byte].symbol); + if (!consume(reader, level1[peeked_byte].num_bits)) + return -1; + continue; + } + + if (!consume(reader, 8)) + return -1; + + const struct h2_huffman_code *level2 = next_level1(peeked_byte); + peeked_byte = peek_byte(reader); + if (level2[peeked_byte].num_bits) { + uint8_ring_buffer_put_copy(buffer, level2[peeked_byte].symbol); + if (!consume(reader, level2[peeked_byte].num_bits)) + return -1; + continue; + } + + if (!consume(reader, 8)) + goto fail; + + const struct h2_huffman_code *level3 = next_level2(peeked_byte); + if (LIKELY(level3)) { + peeked_byte = peek_byte(reader); + if (level3[peeked_byte].num_bits < 0) { + /* EOS found */ + goto done; + } + if (LIKELY(level3[peeked_byte].num_bits)) { + uint8_ring_buffer_put_copy(buffer, level3[peeked_byte].symbol); + if (!consume(reader, level3[peeked_byte].num_bits)) + return -1; + continue; + } + } + + return -1; + } + + /* FIXME: ensure we're not promoting types unnecessarily here */ + if (reader->total_bitcount) { + const uint8_t peeked_byte = peek_byte(reader); + const uint8_t eos_prefix = ((1 << bit_reader.total_bitcount) - 1) + << (8 - bit_reader.total_bitcount); + + if ((peeked_byte & eos_prefix) == eos_prefix) + goto done; + + if (level0[peeked_byte].num_bits == (int8_t)reader->total_bitcount) { + uint8_ring_buffer_put_copy(buffer, level0[peeked_byte].symbol); + goto done; + } + + /* If we get here, then the remaining bits are either: + * - Not a prefix of EOS + * - Incomplete sequence + * - Has overlong padding + */ + return -1; + } + +done: + return (ssize_t)uint8_ring_buffer_size(buffer); +} + +bool lwan_h2_huffman_decode_for_fuzzing(const uint8_t *input, size_t input_len) +{ + struct lwan_h2_huffman_decoder decoder; + + lwan_h2_huffman_init(&decoder, input, input_len); + + while (true) { + ssize_t n_decoded = lwan_h2_huffman_next(&decoder); + + if (UNLIKELY(n_decoded < 0)) + return false; + if (n_decoded < 64) + return true; + + uint8_ring_buffer_init(&decoder->buffer); + } +} + +bool test_decoder(unsigned char input[], size_t input_size, const char expected[], size_t expected_size) +{ + struct lwan_h2_huffman_decoder decoder; + const char *expected_ptr = expected; + + lwan_h2_huffman_init(&decoder, input, input_size); + + while (true) { + ssize_t n_decoded = lwan_h2_huffman_next(&decoder); + if (n_decoded < 0) + return false; + if (n_decoded == 0 && expected_size == 0) + return true; + if (n_decoded == 0 && expected_size != 0) + return false; + while (n_decoded && expected_size) { + uint8_t expected = *expected_ptr; + uint8_t got = uint8_ring_buffer_get(&decoder->buffer); + expected_ptr++; + expected_size--; + n_decoded--; + if (expected != got) { + fprintf(stderr, "expected %d, got %d\n", expected, got); + return false; + } + } + } +} + +int main(int argc, char *argv[]) +{ + /* "litespeed" */ + unsigned char litespeed_huff[] = {0xce, 0x64, 0x97, 0x75, 0x65, 0x2c, 0x9f}; + if (!test_decoder(litespeed_huff, sizeof(litespeed_huff), "LiteSpeed", + sizeof("LiteSpeed") - 1)) { + return 1; + } + + unsigned char x_fb_debug[128] = { + 0xa7, 0x06, 0xa7, 0x63, 0x97, 0xc6, 0x1d, 0xc9, 0xbb, 0xa3, 0xc6, 0x5e, + 0x52, 0xf2, 0x6a, 0xba, 0x66, 0x17, 0xe6, 0x71, 0x37, 0x0a, 0x3c, 0x74, + 0xb3, 0x8d, 0x12, 0x92, 0x5e, 0x71, 0xf9, 0xea, 0x4d, 0xc2, 0x42, 0x24, + 0xb7, 0xf6, 0x93, 0x66, 0x39, 0xab, 0xd1, 0x8d, 0xff, 0xcf, 0x07, 0xdf, + 0x8b, 0xac, 0x7f, 0xef, 0x65, 0x5d, 0x9f, 0x8c, 0x9d, 0x3c, 0x72, 0x8f, + 0xc5, 0xfd, 0x9e, 0xd0, 0x51, 0xb1, 0xdf, 0x46, 0xc8, 0x20, + }; + unsigned char x_fb_debug_decoded[] = + "mEO7bfwFStBMwJWfW4pmg2XL25AswjrVlfcfYbxkcS2ssduZmiKoipMH9XwoTGkb+" + "Qnq9bcjwWbwDQzsea/vMQ=="; + if (!test_decoder(x_fb_debug, sizeof(x_fb_debug), x_fb_debug_decoded, + sizeof(x_fb_debug_decoded) - 1)) { + return 1; + } + + puts("passed!"); + return 0; +} +""") diff --git a/src/scripts/huffman-table.txt b/src/scripts/huffman-table.txt new file mode 100644 index 000000000..05218ebf1 --- /dev/null +++ b/src/scripts/huffman-table.txt @@ -0,0 +1,257 @@ + ( 0) |11111111|11000 1ff8 [13] + ( 1) |11111111|11111111|1011000 7fffd8 [23] + ( 2) |11111111|11111111|11111110|0010 fffffe2 [28] + ( 3) |11111111|11111111|11111110|0011 fffffe3 [28] + ( 4) |11111111|11111111|11111110|0100 fffffe4 [28] + ( 5) |11111111|11111111|11111110|0101 fffffe5 [28] + ( 6) |11111111|11111111|11111110|0110 fffffe6 [28] + ( 7) |11111111|11111111|11111110|0111 fffffe7 [28] + ( 8) |11111111|11111111|11111110|1000 fffffe8 [28] + ( 9) |11111111|11111111|11101010 ffffea [24] + ( 10) |11111111|11111111|11111111|111100 3ffffffc [30] + ( 11) |11111111|11111111|11111110|1001 fffffe9 [28] + ( 12) |11111111|11111111|11111110|1010 fffffea [28] + ( 13) |11111111|11111111|11111111|111101 3ffffffd [30] + ( 14) |11111111|11111111|11111110|1011 fffffeb [28] + ( 15) |11111111|11111111|11111110|1100 fffffec [28] + ( 16) |11111111|11111111|11111110|1101 fffffed [28] + ( 17) |11111111|11111111|11111110|1110 fffffee [28] + ( 18) |11111111|11111111|11111110|1111 fffffef [28] + ( 19) |11111111|11111111|11111111|0000 ffffff0 [28] + ( 20) |11111111|11111111|11111111|0001 ffffff1 [28] + ( 21) |11111111|11111111|11111111|0010 ffffff2 [28] + ( 22) |11111111|11111111|11111111|111110 3ffffffe [30] + ( 23) |11111111|11111111|11111111|0011 ffffff3 [28] + ( 24) |11111111|11111111|11111111|0100 ffffff4 [28] + ( 25) |11111111|11111111|11111111|0101 ffffff5 [28] + ( 26) |11111111|11111111|11111111|0110 ffffff6 [28] + ( 27) |11111111|11111111|11111111|0111 ffffff7 [28] + ( 28) |11111111|11111111|11111111|1000 ffffff8 [28] + ( 29) |11111111|11111111|11111111|1001 ffffff9 [28] + ( 30) |11111111|11111111|11111111|1010 ffffffa [28] + ( 31) |11111111|11111111|11111111|1011 ffffffb [28] +' ' ( 32) |010100 14 [ 6] +'!' ( 33) |11111110|00 3f8 [10] +'"' ( 34) |11111110|01 3f9 [10] +'#' ( 35) |11111111|1010 ffa [12] +'$' ( 36) |11111111|11001 1ff9 [13] +'%' ( 37) |010101 15 [ 6] +'&' ( 38) |11111000 f8 [ 8] +''' ( 39) |11111111|010 7fa [11] +'(' ( 40) |11111110|10 3fa [10] +')' ( 41) |11111110|11 3fb [10] +'*' ( 42) |11111001 f9 [ 8] +'+' ( 43) |11111111|011 7fb [11] +',' ( 44) |11111010 fa [ 8] +'-' ( 45) |010110 16 [ 6] +'.' ( 46) |010111 17 [ 6] +'/' ( 47) |011000 18 [ 6] +'0' ( 48) |00000 0 [ 5] +'1' ( 49) |00001 1 [ 5] +'2' ( 50) |00010 2 [ 5] +'3' ( 51) |011001 19 [ 6] +'4' ( 52) |011010 1a [ 6] +'5' ( 53) |011011 1b [ 6] +'6' ( 54) |011100 1c [ 6] +'7' ( 55) |011101 1d [ 6] +'8' ( 56) |011110 1e [ 6] +'9' ( 57) |011111 1f [ 6] +':' ( 58) |1011100 5c [ 7] +';' ( 59) |11111011 fb [ 8] +'<' ( 60) |11111111|1111100 7ffc [15] +'=' ( 61) |100000 20 [ 6] +'>' ( 62) |11111111|1011 ffb [12] +'?' ( 63) |11111111|00 3fc [10] +'@' ( 64) |11111111|11010 1ffa [13] +'A' ( 65) |100001 21 [ 6] +'B' ( 66) |1011101 5d [ 7] +'C' ( 67) |1011110 5e [ 7] +'D' ( 68) |1011111 5f [ 7] +'E' ( 69) |1100000 60 [ 7] +'F' ( 70) |1100001 61 [ 7] +'G' ( 71) |1100010 62 [ 7] +'H' ( 72) |1100011 63 [ 7] +'I' ( 73) |1100100 64 [ 7] +'J' ( 74) |1100101 65 [ 7] +'K' ( 75) |1100110 66 [ 7] +'L' ( 76) |1100111 67 [ 7] +'M' ( 77) |1101000 68 [ 7] +'N' ( 78) |1101001 69 [ 7] +'O' ( 79) |1101010 6a [ 7] +'P' ( 80) |1101011 6b [ 7] +'Q' ( 81) |1101100 6c [ 7] +'R' ( 82) |1101101 6d [ 7] +'S' ( 83) |1101110 6e [ 7] +'T' ( 84) |1101111 6f [ 7] +'U' ( 85) |1110000 70 [ 7] +'V' ( 86) |1110001 71 [ 7] +'W' ( 87) |1110010 72 [ 7] +'X' ( 88) |11111100 fc [ 8] +'Y' ( 89) |1110011 73 [ 7] +'Z' ( 90) |11111101 fd [ 8] +'[' ( 91) |11111111|11011 1ffb [13] +'\' ( 92) |11111111|11111110|000 7fff0 [19] +']' ( 93) |11111111|11100 1ffc [13] +'^' ( 94) |11111111|111100 3ffc [14] +'_' ( 95) |100010 22 [ 6] +'`' ( 96) |11111111|1111101 7ffd [15] +'a' ( 97) |00011 3 [ 5] +'b' ( 98) |100011 23 [ 6] +'c' ( 99) |00100 4 [ 5] +'d' (100) |100100 24 [ 6] +'e' (101) |00101 5 [ 5] +'f' (102) |100101 25 [ 6] +'g' (103) |100110 26 [ 6] +'h' (104) |100111 27 [ 6] +'i' (105) |00110 6 [ 5] +'j' (106) |1110100 74 [ 7] +'k' (107) |1110101 75 [ 7] +'l' (108) |101000 28 [ 6] +'m' (109) |101001 29 [ 6] +'n' (110) |101010 2a [ 6] +'o' (111) |00111 7 [ 5] +'p' (112) |101011 2b [ 6] +'q' (113) |1110110 76 [ 7] +'r' (114) |101100 2c [ 6] +'s' (115) |01000 8 [ 5] +'t' (116) |01001 9 [ 5] +'u' (117) |101101 2d [ 6] +'v' (118) |1110111 77 [ 7] +'w' (119) |1111000 78 [ 7] +'x' (120) |1111001 79 [ 7] +'y' (121) |1111010 7a [ 7] +'z' (122) |1111011 7b [ 7] +'{' (123) |11111111|1111110 7ffe [15] +'|' (124) |11111111|100 7fc [11] +'}' (125) |11111111|111101 3ffd [14] +'~' (126) |11111111|11101 1ffd [13] + (127) |11111111|11111111|11111111|1100 ffffffc [28] + (128) |11111111|11111110|0110 fffe6 [20] + (129) |11111111|11111111|010010 3fffd2 [22] + (130) |11111111|11111110|0111 fffe7 [20] + (131) |11111111|11111110|1000 fffe8 [20] + (132) |11111111|11111111|010011 3fffd3 [22] + (133) |11111111|11111111|010100 3fffd4 [22] + (134) |11111111|11111111|010101 3fffd5 [22] + (135) |11111111|11111111|1011001 7fffd9 [23] + (136) |11111111|11111111|010110 3fffd6 [22] + (137) |11111111|11111111|1011010 7fffda [23] + (138) |11111111|11111111|1011011 7fffdb [23] + (139) |11111111|11111111|1011100 7fffdc [23] + (140) |11111111|11111111|1011101 7fffdd [23] + (141) |11111111|11111111|1011110 7fffde [23] + (142) |11111111|11111111|11101011 ffffeb [24] + (143) |11111111|11111111|1011111 7fffdf [23] + (144) |11111111|11111111|11101100 ffffec [24] + (145) |11111111|11111111|11101101 ffffed [24] + (146) |11111111|11111111|010111 3fffd7 [22] + (147) |11111111|11111111|1100000 7fffe0 [23] + (148) |11111111|11111111|11101110 ffffee [24] + (149) |11111111|11111111|1100001 7fffe1 [23] + (150) |11111111|11111111|1100010 7fffe2 [23] + (151) |11111111|11111111|1100011 7fffe3 [23] + (152) |11111111|11111111|1100100 7fffe4 [23] + (153) |11111111|11111110|11100 1fffdc [21] + (154) |11111111|11111111|011000 3fffd8 [22] + (155) |11111111|11111111|1100101 7fffe5 [23] + (156) |11111111|11111111|011001 3fffd9 [22] + (157) |11111111|11111111|1100110 7fffe6 [23] + (158) |11111111|11111111|1100111 7fffe7 [23] + (159) |11111111|11111111|11101111 ffffef [24] + (160) |11111111|11111111|011010 3fffda [22] + (161) |11111111|11111110|11101 1fffdd [21] + (162) |11111111|11111110|1001 fffe9 [20] + (163) |11111111|11111111|011011 3fffdb [22] + (164) |11111111|11111111|011100 3fffdc [22] + (165) |11111111|11111111|1101000 7fffe8 [23] + (166) |11111111|11111111|1101001 7fffe9 [23] + (167) |11111111|11111110|11110 1fffde [21] + (168) |11111111|11111111|1101010 7fffea [23] + (169) |11111111|11111111|011101 3fffdd [22] + (170) |11111111|11111111|011110 3fffde [22] + (171) |11111111|11111111|11110000 fffff0 [24] + (172) |11111111|11111110|11111 1fffdf [21] + (173) |11111111|11111111|011111 3fffdf [22] + (174) |11111111|11111111|1101011 7fffeb [23] + (175) |11111111|11111111|1101100 7fffec [23] + (176) |11111111|11111111|00000 1fffe0 [21] + (177) |11111111|11111111|00001 1fffe1 [21] + (178) |11111111|11111111|100000 3fffe0 [22] + (179) |11111111|11111111|00010 1fffe2 [21] + (180) |11111111|11111111|1101101 7fffed [23] + (181) |11111111|11111111|100001 3fffe1 [22] + (182) |11111111|11111111|1101110 7fffee [23] + (183) |11111111|11111111|1101111 7fffef [23] + (184) |11111111|11111110|1010 fffea [20] + (185) |11111111|11111111|100010 3fffe2 [22] + (186) |11111111|11111111|100011 3fffe3 [22] + (187) |11111111|11111111|100100 3fffe4 [22] + (188) |11111111|11111111|1110000 7ffff0 [23] + (189) |11111111|11111111|100101 3fffe5 [22] + (190) |11111111|11111111|100110 3fffe6 [22] + (191) |11111111|11111111|1110001 7ffff1 [23] + (192) |11111111|11111111|11111000|00 3ffffe0 [26] + (193) |11111111|11111111|11111000|01 3ffffe1 [26] + (194) |11111111|11111110|1011 fffeb [20] + (195) |11111111|11111110|001 7fff1 [19] + (196) |11111111|11111111|100111 3fffe7 [22] + (197) |11111111|11111111|1110010 7ffff2 [23] + (198) |11111111|11111111|101000 3fffe8 [22] + (199) |11111111|11111111|11110110|0 1ffffec [25] + (200) |11111111|11111111|11111000|10 3ffffe2 [26] + (201) |11111111|11111111|11111000|11 3ffffe3 [26] + (202) |11111111|11111111|11111001|00 3ffffe4 [26] + (203) |11111111|11111111|11111011|110 7ffffde [27] + (204) |11111111|11111111|11111011|111 7ffffdf [27] + (205) |11111111|11111111|11111001|01 3ffffe5 [26] + (206) |11111111|11111111|11110001 fffff1 [24] + (207) |11111111|11111111|11110110|1 1ffffed [25] + (208) |11111111|11111110|010 7fff2 [19] + (209) |11111111|11111111|00011 1fffe3 [21] + (210) |11111111|11111111|11111001|10 3ffffe6 [26] + (211) |11111111|11111111|11111100|000 7ffffe0 [27] + (212) |11111111|11111111|11111100|001 7ffffe1 [27] + (213) |11111111|11111111|11111001|11 3ffffe7 [26] + (214) |11111111|11111111|11111100|010 7ffffe2 [27] + (215) |11111111|11111111|11110010 fffff2 [24] + (216) |11111111|11111111|00100 1fffe4 [21] + (217) |11111111|11111111|00101 1fffe5 [21] + (218) |11111111|11111111|11111010|00 3ffffe8 [26] + (219) |11111111|11111111|11111010|01 3ffffe9 [26] + (220) |11111111|11111111|11111111|1101 ffffffd [28] + (221) |11111111|11111111|11111100|011 7ffffe3 [27] + (222) |11111111|11111111|11111100|100 7ffffe4 [27] + (223) |11111111|11111111|11111100|101 7ffffe5 [27] + (224) |11111111|11111110|1100 fffec [20] + (225) |11111111|11111111|11110011 fffff3 [24] + (226) |11111111|11111110|1101 fffed [20] + (227) |11111111|11111111|00110 1fffe6 [21] + (228) |11111111|11111111|101001 3fffe9 [22] + (229) |11111111|11111111|00111 1fffe7 [21] + (230) |11111111|11111111|01000 1fffe8 [21] + (231) |11111111|11111111|1110011 7ffff3 [23] + (232) |11111111|11111111|101010 3fffea [22] + (233) |11111111|11111111|101011 3fffeb [22] + (234) |11111111|11111111|11110111|0 1ffffee [25] + (235) |11111111|11111111|11110111|1 1ffffef [25] + (236) |11111111|11111111|11110100 fffff4 [24] + (237) |11111111|11111111|11110101 fffff5 [24] + (238) |11111111|11111111|11111010|10 3ffffea [26] + (239) |11111111|11111111|1110100 7ffff4 [23] + (240) |11111111|11111111|11111010|11 3ffffeb [26] + (241) |11111111|11111111|11111100|110 7ffffe6 [27] + (242) |11111111|11111111|11111011|00 3ffffec [26] + (243) |11111111|11111111|11111011|01 3ffffed [26] + (244) |11111111|11111111|11111100|111 7ffffe7 [27] + (245) |11111111|11111111|11111101|000 7ffffe8 [27] + (246) |11111111|11111111|11111101|001 7ffffe9 [27] + (247) |11111111|11111111|11111101|010 7ffffea [27] + (248) |11111111|11111111|11111101|011 7ffffeb [27] + (249) |11111111|11111111|11111111|1110 ffffffe [28] + (250) |11111111|11111111|11111101|100 7ffffec [27] + (251) |11111111|11111111|11111101|101 7ffffed [27] + (252) |11111111|11111111|11111101|110 7ffffee [27] + (253) |11111111|11111111|11111101|111 7ffffef [27] + (254) |11111111|11111111|11111110|000 7fffff0 [27] + (255) |11111111|11111111|11111011|10 3ffffee [26] +EOS (256) |11111111|11111111|11111111|111111 3fffffff [30] diff --git a/src/scripts/testsuite.py b/src/scripts/testsuite.py new file mode 100755 index 000000000..44571f752 --- /dev/null +++ b/src/scripts/testsuite.py @@ -0,0 +1,1062 @@ +#!/usr/bin/python +# TODO: Use tracy (https://github.com/MerlijnWajer/tracy) to see if lwan +# performs certain system calls. This should speed up the mmap tests +# considerably and make it possible to perform more low-level tests. + +import hashlib +import os +import random +import re +import requests +import shutil +import signal +import socket +import string +import subprocess +import sys +import time +import unittest +import logging + +BUILD_DIR = './build' +for arg in sys.argv[1:]: + if not arg.startswith('-') and os.path.exists(arg): + BUILD_DIR = arg + sys.argv.remove(arg) + +if os.getenv('REQUESTS_DEBUG'): + logging.basicConfig() + logging.getLogger().setLevel(logging.DEBUG) + requests_log = logging.getLogger("urllib3") + requests_log.setLevel(logging.DEBUG) + requests_log.propagate = True + +class LwanTest(unittest.TestCase): + harness_paths = { + 'testrunner': os.path.join(BUILD_DIR, 'src/bin/testrunner/testrunner'), + 'techempower': os.path.join(BUILD_DIR, 'src/samples/techempower/techempower'), + } + files_to_copy = { + 'testrunner': ('src/bin/testrunner/testrunner.conf', + 'src/bin/testrunner/test.lua'), + 'techempower': ('src/samples/techempower/techempower.db', + 'src/samples/techempower/techempower.conf', + 'src/samples/techempower/json.lua'), + } + + def ensureHighlander(self): + def pgrep(process_name): + try: + out = subprocess.check_output(('pgrep', process_name), universal_newlines=True) + return (int(pid) for pid in str(out).rstrip().split('\n')) + except subprocess.CalledProcessError: + yield from () + + for typ in self.harness_paths.keys(): + for pid in pgrep(typ): + os.kill(pid, 2) + + def setUp(self, env=None, harness='testrunner'): + self.ensureHighlander() + + self.files_to_remove = [] + for file_to_copy in self.files_to_copy[harness]: + base = os.path.basename(file_to_copy) + shutil.copyfile(file_to_copy, base) + self.files_to_remove.append(base) + + open('htpasswd', 'w').close() + self.files_to_remove.append('htpasswd') + + for spawn_try in range(20): + self.lwan = subprocess.Popen([self.harness_paths[harness]], env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + + if self.lwan.poll() is not None: + raise Exception('It seems that %s is not starting up' % harness) + + for request_try in range(20): + try: + r = requests.get('http://127.0.0.1:8080/hello') + self.assertEqual(r.status_code, 200) + return + except requests.ConnectionError: + time.sleep(0.1) + + time.sleep(0.1) + + raise Exception('Timeout waiting for lwan') + + def tearDown(self): + try: + requests.get('http://127.0.0.1:8080/quit-lwan') + except requests.exceptions.ConnectionError: + # Requesting /quit-lwan will make testrunner exit(0), closing the + # connection without sending a response, raising this exception. + # That's expected here. + return + finally: + with self.lwan as l: + l.communicate(timeout=1.0) + l.kill() + + self.ensureHighlander() + + for file_to_remove in self.files_to_remove: + try: + os.remove(file_to_remove) + except FileNotFoundError: + pass + + def assertHttpResponseValid(self, request, status_code, content_type): + self.assertEqual(request.status_code, status_code) + self.assertTrue('Content-Type' in request.headers) + self.assertEqual(request.headers['Content-Type'], content_type) + + def assertResponse404(self, request): + self.assertHttpResponseValid(request, 404, 'text/html') + + def assertResponseHtml(self, request, status_code=200): + self.assertHttpResponseValid(request, status_code, 'text/html') + + def assertResponsePlain(self, request, status_code=200): + self.assertHttpResponseValid(request, status_code, 'text/plain') + + +class TestTWFB(LwanTest): + def setUp(self, env=None): + super().setUp(env, harness='techempower') + + def test_plaintext(self): + for endpoint in ('lua.plaintext', 'plaintext'): + r = requests.get('http://127.0.0.1:8080/' + endpoint) + + self.assertResponsePlain(r) + self.assertEqual(r.text, 'Hello, World!') + + def assertSingleQueryResultIsValid(self, single): + self.assertTrue(isinstance(single, dict)) + self.assertEqual({'randomNumber', 'id'}, set(single.keys())) + self.assertEqual(type(single['randomNumber']), type(0)) + self.assertEqual(type(single['id']), type(0)) + self.assertTrue(0 <= single['randomNumber'] <= 9999) + self.assertTrue(1 <= single['id'] <= 10000) + + def test_fortunes(self): + r = requests.get('http://127.0.0.1:8080/fortunes') + + self.assertHttpResponseValid(r, 200, 'text/html; charset=UTF-8') + self.assertEqual(hashlib.md5(bytes(r.text, 'utf-8')).hexdigest(), + '352e66abf97b5a07c76a8b3c9e3e6339') + + def test_json(self): + for endpoint in ('lua.json', 'json'): + r = requests.get('http://127.0.0.1:8080/' + endpoint) + + self.assertHttpResponseValid(r, 200, 'application/json') + self.assertEqual(r.json(), {'message': 'Hello, World!'}) + + def test_single_query(self): + r = requests.get('http://127.0.0.1:8080/db') + + self.assertHttpResponseValid(r, 200, 'application/json') + self.assertSingleQueryResultIsValid(r.json()) + + def test_multiple_queries(self): + def assertMultipleQueriesValid(r, queries): + self.assertHttpResponseValid(r, 200, 'application/json') + self.assertTrue(isinstance(r.json(), list)) + self.assertEqual(len(r.json()), min(500, max(queries, 1))) + for query in r.json(): + self.assertSingleQueryResultIsValid(query) + + for queries in (1, 10, 100, 500, 1000, 0, -1): + r = requests.get('http://127.0.0.1:8080/queries?queries=%d' % queries) + assertMultipleQueriesValid(r, queries) + + r = requests.get('http://127.0.0.1:8080/queries') + assertMultipleQueriesValid(r, 1) + +class TestPost(LwanTest): + def test_will_it_blend(self): + r = requests.post('http://127.0.0.1:8080/post/blend', json={'will-it-blend': True}) + self.assertHttpResponseValid(r, 200, 'application/json') + self.assertEqual(r.json(), {'did-it-blend': 'oh-hell-yeah'}) + + def make_request_with_size(self, size): + random.seed(size) + + data = "".join(random.choice(string.printable) for c in range(size * 2)) + + r = requests.post('http://127.0.0.1:8080/post/big', data=data, + headers={'Content-Type': 'x-test/trololo'}) + + self.assertHttpResponseValid(r, 200, 'application/json') + self.assertEqual(r.json(), { + 'received': len(data), + 'sum': sum(ord(b) for b in data) + }) + + def test_small_request(self): self.make_request_with_size(10) + def test_medium_request(self): self.make_request_with_size(100) + def test_large_request(self): self.make_request_with_size(1000) + + # These two tests are supposed to fail, with Lwan aborting the connection. + def test_huge_request(self): + try: + self.make_request_with_size(10000) + except requests.exceptions.ChunkedEncodingError: + pass + except requests.exceptions.ConnectionError: + pass + def test_gigantic_request(self): + try: + self.make_request_with_size(100000) + except requests.exceptions.ChunkedEncodingError: + pass + except requests.exceptions.ConnectionError: + pass + + +class TestFileServing(LwanTest): + def test_mime_type_is_correct(self): + table = ( + ('/', 'text/html'), + ('/icons/back.gif', 'image/gif'), + ('/icons', 'text/plain'), + ('/icons/', 'text/html'), + ('/zero', 'application/octet-stream') + ) + + for path, expected_mime in table: + r = requests.head('http://127.0.0.1:8080%s' % path) + self.assertEqual(r.headers['content-type'], expected_mime) + + + def test_non_existent_file_yields_404(self): + r = requests.get('http://127.0.0.1:8080/icons/non-existent-file.png') + + self.assertResponse404(r) + + + def test_dot_dot_slash_yields_404(self): + r = requests.get('http://127.0.0.1:8080/etc/passwd') + + self.assertResponse404(r) + + + def test_slash_slash_slash_does_not_matter_200(self): + r = requests.get('http://127.0.0.1:8080//////////100.html') + + self.assertHttpResponseValid(r, 200, 'text/html') + self.assertEqual(r.text, 'X' * 100) + + + def test_range_half(self): + r = requests.get('http://127.0.0.1:8080/zero', + headers={'Range': 'bytes=0-50'}) + + self.assertHttpResponseValid(r, 206, 'application/octet-stream') + + self.assertTrue('content-length' in r.headers) + self.assertEqual(r.headers['content-length'], '50') + + self.assertEqual(r.text, '\0' * 50) + + def test_range_half_inverted(self): + r = requests.get('http://127.0.0.1:8080/zero', + headers={'Range': 'bytes=50-0'}) + + self.assertHttpResponseValid(r, 416, 'text/html') + + + def test_range_half_equal(self): + r = requests.get('http://127.0.0.1:8080/zero', + headers={'Range': 'bytes=50-50'}) + + self.assertHttpResponseValid(r, 416, 'text/html') + + + def test_range_too_big(self): + r = requests.get('http://127.0.0.1:8080/zero', + headers={'Range': 'bytes=0-40000'}) + + self.assertHttpResponseValid(r, 416, 'text/html') + + + def test_range_no_from(self): + r = requests.get('http://127.0.0.1:8080/zero', + headers={'Range': 'bytes=-100'}) + + self.assertHttpResponseValid(r, 206, 'application/octet-stream') + + self.assertTrue('content-length' in r.headers) + self.assertEqual(r.headers['content-length'], '100') + + self.assertEqual(r.text, '\0' * 100) + + def test_range_no_to(self): + r = requests.get('http://127.0.0.1:8080/zero', + headers={'Range': 'bytes=50-'}) + + self.assertHttpResponseValid(r, 206, 'application/octet-stream') + + self.assertTrue('content-length' in r.headers) + self.assertEqual(r.headers['content-length'], '32718') + + self.assertEqual(r.text, '\0' * 32718) + + + def test_slash_slash_slash_does_not_matter_404(self): + r = requests.get('http://127.0.0.1:8080//////////etc/passwd') + + self.assertResponse404(r) + + + def test_head_request_small_file(self): + r = requests.head('http://127.0.0.1:8080/100.html', + headers={'Accept-Encoding': 'foobar'}) + + self.assertResponseHtml(r) + + self.assertTrue('content-length' in r.headers) + self.assertEqual(r.headers['content-length'], '100') + + self.assertEqual(r.text, '') + + + def test_head_request_larger_file(self): + r = requests.head('http://127.0.0.1:8080/zero', + headers={'Accept-Encoding': 'foobar'}) + + self.assertHttpResponseValid(r, 200, 'application/octet-stream') + + self.assertTrue('content-length' in r.headers) + self.assertEqual(r.headers['content-length'], '32768') + + self.assertEqual(r.text, '') + + + def test_uncompressed_small_file(self): + r = requests.get('http://127.0.0.1:8080/100.html', + headers={'Accept-Encoding': 'foobar'}) + + self.assertResponseHtml(r) + + self.assertTrue('content-length' in r.headers) + self.assertEqual(r.headers['content-length'], '100') + + self.assertEqual(r.text, 'X' * 100) + + + def test_get_root(self): + r = requests.get('http://127.0.0.1:8080/') + + self.assertResponseHtml(r) + + self.assertTrue('It works!' in r.text) + + + def test_compressed_small_file(self): + encodings = ( + 'deflate', + 'foo,bar,deflate', + 'foo, bar, deflate', + 'deflote' # This should fail, but won't in our current implementation + ) + + for encoding in encodings: + r = requests.get('http://127.0.0.1:8080/100.html', + headers={'Accept-Encoding': encoding}) + + self.assertResponseHtml(r) + + self.assertTrue('content-length' in r.headers) + self.assertLess(int(r.headers['content-length']), 100) + + self.assertTrue('content-encoding' in r.headers) + self.assertEqual(r.headers['content-encoding'], 'deflate') + + self.assertEqual(r.text, 'X' * 100) + + + def test_get_larger_file(self): + r = requests.get('http://127.0.0.1:8080/zero', + headers={'Accept-Encoding': 'foobar'}) + + self.assertHttpResponseValid(r, 200, 'application/octet-stream') + + self.assertTrue('content-length' in r.headers) + self.assertEqual(r.headers['content-length'], '32768') + + self.assertEqual(r.text, '\0' * 32768) + + + def test_directory_listing(self): + r = requests.get('http://127.0.0.1:8080/icons', + headers={'Accept-Encoding': 'foobar'}) + + self.assertResponseHtml(r) + + self.assertTrue('

Index of icons

' in r.text) + + def assertHasImage(name): + imgtag = "%s.gif" % (name, name) + self.assertTrue(imgtag in r.text) + + assertHasImage('back') + assertHasImage('file') + assertHasImage('folder') + + with open('wwwroot/icons/README.TXT', 'r') as readme: + readme = readme.read() + readme = readme.replace('"', """) + readme = readme.replace('/', "/") + readme = readme.replace("'", "'") + + self.assertTrue(readme in r.text) + + self.assertTrue(r.text.startswith('')) + self.assertTrue(r.text.endswith('\n')) + + + def test_has_lwan_server_header(self): + r = requests.get('http://127.0.0.1:8080/100.html') + self.assertTrue('server' in r.headers) + self.assertEqual(r.headers['server'], 'lwan/testrunner') + + + def test_directory_without_trailing_slash_redirects(self): + r = requests.get('http://127.0.0.1:8080/icons', allow_redirects=False) + + self.assertResponsePlain(r, 301) + self.assertTrue('location' in r.headers) + self.assertEqual(r.headers['location'], '/icons/') + +class TestRedirect(LwanTest): + def test_redirect_default(self): + r = requests.get('http://127.0.0.1:8080/elsewhere', allow_redirects=False) + + self.assertResponseHtml(r, 301) + self.assertTrue('location' in r.headers) + self.assertEqual(r.headers['location'], 'http://lwan.ws/') + + def test_redirect_307(self): + r = requests.get('http://127.0.0.1:8080/redirect307', allow_redirects=False) + + self.assertResponseHtml(r, 307) + self.assertTrue('location' in r.headers) + self.assertEqual(r.headers['location'], 'http://lwan.ws/') + +class TestRewrite(LwanTest): + def test_conditional_rewrite_with_cookie(self): + for key in ('style', 'something-else', ''): + for value in ('dark', 'dork', '', None): + r = requests.get('http://localhost:8080/css/test.css', cookies={key: value}) + + self.assertResponsePlain(r, 200) + + if (key, value) == ('style', 'dark'): + self.assertEqual(r.text, 'Hello, dark!') + else: + self.assertEqual(r.text, 'Hello, light!') + + def test_conditional_rewrite_without_cookie(self): + r = requests.get('http://localhost:8080/css/test.css') + + self.assertResponsePlain(r, 200) + self.assertEqual(r.text, 'Hello, light!') + + def test_conditional_rewrite_backref(self): + r = requests.get('http://localhost:8080/pattern/42/backref') + + self.assertResponsePlain(r, 200) + self.assertEqual(r.text, 'Hello, fourtytwo!') + + r = requests.get('http://localhost:8080/pattern/420/backref') + self.assertNotEqual(r.text, 'Hello, fourtytwo!') + + def test_pattern_redirect_to(self): + r = requests.get('http://127.0.0.1:8080/pattern/foo/1234x5678', allow_redirects=False) + + self.assertResponseHtml(r, 301) + self.assertTrue('location' in r.headers) + self.assertEqual(r.headers['location'], '/hello?name=prexmiddle5678othermiddle1234post') + + def test_pattern_rewrite_as(self): + r = requests.get('http://127.0.0.1:8080/pattern/bar/42/test', allow_redirects=False) + + self.assertResponsePlain(r, 200) + self.assertFalse('location' in r.headers) + self.assertEqual(r.text, 'Hello, rewritten42!') + + def test_lua_redirect_to(self): + r = requests.get('http://127.0.0.1:8080/pattern/lua/redir/6x7', allow_redirects=False) + + self.assertResponseHtml(r, 301) + self.assertTrue('location' in r.headers) + self.assertEqual(r.headers['location'], '/hello?name=redirected42') + + def test_lua_rewrite_as(self): + r = requests.get('http://127.0.0.1:8080/pattern/lua/rewrite/7x6', allow_redirects=False) + + self.assertResponsePlain(r, 200) + self.assertFalse('location' in r.headers) + self.assertEqual(r.text, 'Hello, rewritten42!') + +class SocketTest(LwanTest): + class WrappedSock: + def __init__(self, sock): + self._wrapped_sock = sock + + def send(self, stuff): + return self._wrapped_sock.send(bytes(stuff, 'UTF-8')) + + def recv(self, n_bytes): + return str(self._wrapped_sock.recv(n_bytes), 'UTF-8') + + def __enter__(self): + return self + + def __exit__(self, *args): + return self._wrapped_sock.close() + + def __getattr__(self, attr): + if attr in self.__dict__: + return getattr(self, attr) + return getattr(self._wrapped_sock, attr) + + def connect(self, host='127.0.0.1', port=8080): + def _connect(host, port): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + except socket.error: + if sock: + sock.close() + del sock + + return None + finally: + return sock + + sock = _connect(host, port) + self.assertNotEqual(sock, None) + return SocketTest.WrappedSock(sock) + + +class TestMinimalRequests(SocketTest): + def assertHttpCode(self, sock, code): + contents = sock.recv(128) + + self.assertRegex(contents, r'^HTTP/1\.[01] ' + str(code) + r' ') + + def test_http1_0_request(self): + with self.connect() as sock: + sock.send("GET / HTTP/1.0\r\n\r\n") + self.assertHttpCode(sock, 200) + + + def test_http1_1_request(self): + with self.connect() as sock: + sock.send("GET / HTTP/1.1\r\n\r\n") + self.assertHttpCode(sock, 200) + + +class TestMalformedRequests(SocketTest): + def assertHttpCode(self, sock, code): + contents = sock.recv(128) + + self.assertRegex(contents, r'^HTTP/1\.[01] ' + str(code) + r' ') + + + def test_cat_sleeping_on_keyboard(self): + with self.connect() as sock: + sock.send('asldkfjg238045tgqwdcjv1li 2u4ftw dfjkb12345t\r\n\r\n') + + self.assertHttpCode(sock, 405) + + + def test_no_http_version_fails(self): + with self.connect() as sock: + sock.send('GET /some-long-url-that-is-longer-than-version-string\r\n\r\n') + + self.assertHttpCode(sock, 400) + + + def test_proxy_get_fails(self): + with self.connect() as sock: + sock.send('GET http://example.com HTTP/1.0\r\n\r\n') + + self.assertHttpCode(sock, 400) + + + def test_get_not_http(self): + with self.connect() as sock: + sock.send('GET / FROG/1.0\r\n\r\n') + + self.assertHttpCode(sock, 400) + + + def test_get_http_not_1_x(self): + with self.connect() as sock: + sock.send('GET / HTTP/2.0\r\n\r\n') + + self.assertHttpCode(sock, 400) + + + def test_request_too_large(self): + try: + r = requests.get('http://127.0.0.1:8080/' + 'X' * 100000) + + self.assertResponseHtml(r, 413) + except requests.exceptions.ChunkedEncodingError: + pass + except requests.exceptions.ConnectionError: + pass + +class TestChunkedEncoding(LwanTest): + def test_chunked_encoding(self): + r = requests.get('http://localhost:8080/chunked') + self.assertResponsePlain(r) + self.assertFalse('Content-Length' in r.headers) + self.assertTrue('Transfer-Encoding' in r.headers) + self.assertTrue(r.headers['Transfer-Encoding'], 'chunked') + self.assertEqual(r.text, + 'Testing chunked encoding! First chunk\n' + + ''.join('*This is chunk %d*\n' % i for i in range(11)) + + 'Last chunk\n') + +class TestLua(LwanTest): + def test_brew_coffee(self): + r = requests.get('http://127.0.0.1:8080/lua/brew_coffee') + + self.assertEqual(r.status_code, 418) + + def test_invalid_Code(self): + r = requests.get('http://127.0.0.1:8080/lua/invalid_code') + + self.assertEqual(r.status_code, 500) + + def test_inline(self): + r = requests.get('http://localhost:8080/inline') + self.assertResponseHtml(r) + self.assertEqual(r.text, 'Hello') + + def test_hello(self): + r = requests.get('http://localhost:8080/lua/hello') + self.assertResponseHtml(r) + self.assertEqual(r.text, 'Hello, World!') + + def test_hello_param(self): + r = requests.get('http://localhost:8080/lua/hello?name=foo') + self.assertResponseHtml(r) + self.assertEqual(r.text, 'Hello, foo!') + + def test_cookies(self): + cookies_to_send = { + 'FOO': 'BAR' + } + cookies_to_receive = { + 'SESSION_ID': '1234', + 'LANG': 'pt_BR', + } + r = requests.get('http://localhost:8080/lua/cookie', cookies=cookies_to_send) + self.assertResponseHtml(r) + self.assertEqual(r.text, 'Cookie FOO has value: BAR') + + for cookie, value in list(cookies_to_receive.items()): + self.assertTrue(cookie in r.cookies) + self.assertEqual(r.cookies[cookie], value) + + +class TestAuthentication(LwanTest): + class TempHtpasswd: + def __init__(self, users): + self._users = users + + def __enter__(self): + with open('htpasswd', 'w') as f: + for u, p in self._users.items(): + f.write('%s = %s\n' % (u, p)) + + def __exit__(self, type, value, traceback): + os.remove('htpasswd') + + def test_no_creds(self): + r = requests.get('http://127.0.0.1:8080/admin') + self.assertResponseHtml(r, status_code=401) + + def test_unknown_user(self): + with TestAuthentication.TempHtpasswd({'foo': 'bar', 'foobar': 'test123'}): + r = requests.get('http://127.0.0.1:8080/admin', auth=requests.auth.HTTPBasicAuth('nosuch', 'user')) + self.assertResponseHtml(r, status_code=401) + + def test_invalid_creds(self): + with TestAuthentication.TempHtpasswd({'foo': 'bar', 'foobar': 'test123'}): + r = requests.get('http://127.0.0.1:8080/admin', auth=requests.auth.HTTPBasicAuth('foo', 'test123')) + self.assertResponseHtml(r, status_code=401) + + def test_valid_creds(self): + with TestAuthentication.TempHtpasswd({'foo': 'bar', 'foobar': 'test123'}): + r = requests.get('http://127.0.0.1:8080/admin', auth=requests.auth.HTTPBasicAuth('foobar', 'test123')) + self.assertResponsePlain(r) + + self.assertTrue('content-length' in r.headers) + self.assertEqual(int(r.headers['content-length']), len('Hello, world!')) + + self.assertEqual(r.text, 'Hello, world!') + + +class TestQueryString(LwanTest): + def test_query_string(self): + r = requests.get('http://localhost:8080/get-query-string') + self.assertEqual(r.status_code, 400) + + r = requests.get('http://localhost:8080/get-query-string?q=a') + self.assertResponse404(r) + + r = requests.get('http://localhost:8080/get-query-string?q=a&a=10&b=20') + self.assertHttpResponseValid(r, 200, 'text/plain') + self.assertEqual(r.text, 'a = 10') + + r = requests.get('http://localhost:8080/get-query-string?b=20&q=b&a=10') + self.assertHttpResponseValid(r, 200, 'text/plain') + self.assertEqual(r.text, 'b = 20') + + r = requests.get('http://localhost:8080/get-query-string?b=20&q=a&c=30') + self.assertResponse404(r) + + +class TestHelloWorld(LwanTest): + def test_request_id(self): + all_request_ids = set() + for i in range(20): + r = requests.get('http://127.0.0.1:8080/hello?dump_request_id=1&dump_vars=1') + self.assertResponsePlain(r) + request_id = r.headers['x-request-id'] + self.assertFalse(request_id in all_request_ids) + self.assertTrue(re.match(r'^[a-f0-9]{16}$', request_id)) + self.assertTrue('Request ID: <<%s>>' % request_id in r.text) + all_request_ids.add(request_id) + + def test_cookies(self): + c = { + 'SOMECOOKIE': '1c330301-89e4-408a-bf6c-ce107efe8a27', + 'OTHERCOOKIE': 'some cookie value', + 'foo': 'bar' + } + r = requests.get('http://127.0.0.1:8080/hello?dump_vars=1', cookies=c) + + self.assertResponsePlain(r) + + self.assertTrue('\n\nCookies\n' in r.text) + for k, v in list(c.items()): + self.assertTrue('Key = "%s"; Value = "%s"\n' % (k, v) in r.text) + + def test_global_headers_are_present(self): + r = requests.get('http://127.0.0.1:8080/hello') + + self.assertTrue('x-global-header' in r.headers) + self.assertEqual(r.headers['x-global-header'], 'present') + + def test_head_request_hello(self): + r = requests.head('http://127.0.0.1:8080/hello', + headers={'Accept-Encoding': 'foobar'}) + + self.assertResponsePlain(r) + + self.assertTrue('content-length' in r.headers) + self.assertEqual(int(r.headers['content-length']), len('Hello, world!')) + + self.assertEqual(r.text, '') + + + def test_has_custom_header(self): + r = requests.get('http://127.0.0.1:8080/hello') + + self.assertTrue('x-the-answer-to-the-universal-question' in r.headers) + self.assertEqual(r.headers['x-the-answer-to-the-universal-question'], '42') + + + def test_no_param(self): + r = requests.get('http://127.0.0.1:8080/hello') + + self.assertResponsePlain(r) + + self.assertTrue('content-length' in r.headers) + self.assertEqual(int(r.headers['content-length']), len('Hello, world!')) + + self.assertEqual(r.text, 'Hello, world!') + + def test_with_empty_param(self): + r = requests.get('http://127.0.0.1:8080/hello?key=&otherkey=&name=testsuite&dump_vars=1') + + self.assertResponsePlain(r) + + self.assertTrue('Query String Variables' in r.text) + self.assertTrue('Key = "key"; Value = ""\n' in r.text) + self.assertTrue('Key = "otherkey"; Value = ""\n' in r.text) + self.assertTrue('Key = "dump_vars"; Value = "1"\n' in r.text) + self.assertTrue('Key = "name"; Value = "testsuite"\n' in r.text) + + + def test_with_param(self): + r = requests.get('http://127.0.0.1:8080/hello?name=testsuite') + + self.assertResponsePlain(r) + + self.assertTrue('content-length' in r.headers) + self.assertEqual(int(r.headers['content-length']), + len('Hello, testsuite!')) + + self.assertEqual(r.text, 'Hello, testsuite!') + + + def test_with_param_and_fragment(self): + r = requests.get('http://127.0.0.1:8080/hello?name=testsuite#fragment') + + self.assertResponsePlain(r) + + self.assertTrue('content-length' in r.headers) + self.assertEqual(int(r.headers['content-length']), + len('Hello, testsuite!')) + + self.assertEqual(r.text, 'Hello, testsuite!') + + + def test_post_request(self): + data = { + 'answer': 'fourty-two', + 'foo': 'bar' + } + r = requests.post('http://127.0.0.1:8080/hello?dump_vars=1', data=data) + + self.assertResponsePlain(r) + + self.assertTrue('POST data' in r.text) + for k, v in list(data.items()): + self.assertTrue('Key = "%s"; Value = "%s"\n' % (k, v) in r.text) + + + def test_read_env(self): + r = requests.get('http://127.0.0.1:8080/read-env/user') + + self.assertResponsePlain(r) + self.assertEqual(r.text, 'Hello, %s!' % os.getenv('USER')) + +class TestCache(LwanTest): + def setUp(self): + self._cache_for = 3 + + new_environment = os.environ.copy() + new_environment.update({ + 'KEEP_ALIVE_TIMEOUT': '3', + 'CACHE_FOR': str(self._cache_for), + }) + + super().setUp(env=new_environment) + + @classmethod + def setUpClass(cls): + if os.uname().sysname != 'Linux': + raise unittest.SkipTest + + def mmaps(self, f): + with open('/proc/%d/maps' % self.lwan.pid) as map_file: + f = f + '\n' + return [l.endswith(f) for l in map_file] + + + def count_mmaps(self, f): + return sum(self.mmaps(f)) + + + def is_mmapped(self, f): + return any(self.mmaps(f)) + + + def wait_munmap(self, f, timeout=20.0): + while self.is_mmapped(f) and timeout >= 0: + time.sleep(0.05) + timeout -= 0.05 + + + def test_cache_munmaps_conn_close(self): + r = requests.get('http://127.0.0.1:8080/100.html') + + self.assertTrue(self.is_mmapped('/100.html')) + self.wait_munmap('/100.html') + self.assertFalse(self.is_mmapped('/100.html')) + + + def test_cache_munmaps_conn_keep_alive(self): + with requests.Session() as s: + r = s.get('http://127.0.0.1:8080/100.html') + + self.assertTrue(self.is_mmapped('/100.html')) + self.wait_munmap('/100.html') + self.assertFalse(self.is_mmapped('/100.html')) + + + def test_cache_does_not_mmap_large_files(self): + r = requests.get('http://127.0.0.1:8080/zero') + self.assertFalse(self.is_mmapped('/zero')) + + + def test_cache_mmaps_once_conn_keep_alive(self): + with requests.Session() as s: + for request in range(5): + r = s.get('http://127.0.0.1:8080/100.html') + self.assertEqual(self.count_mmaps('/100.html'), 1) + + + def test_cache_mmaps_once_conn_close(self): + for request in range(5): + requests.get('http://127.0.0.1:8080/100.html') + self.assertEqual(self.count_mmaps('/100.html'), 1) + + + def test_cache_mmaps_once_even_after_timeout(self): + for request in range(5): + requests.get('http://127.0.0.1:8080/100.html') + self.assertEqual(self.count_mmaps('/100.html'), 1) + + # 10% over `cache_for` just to be conservative (job thread isn't that precise) + time.sleep(self._cache_for * 1.10) + + requests.get('http://127.0.0.1:8080/100.html') + self.assertEqual(self.count_mmaps('/100.html'), 1) + + +class TestHeaderParsing(SocketTest): + def test_check_for_crlf(self): + # https://github.com/lpereira/lwan/issues/368 + req = '''GET /hello HTTP/1.1\r\nHost: a\rXContent-Length: 31\r\n\r\nGET /evil HTTP/1.1\r\nHost: a\r\n\r\n''' + + with self.connect() as sock: + sock.send(req) + response = sock.recv(4096) + self.assertTrue(response.startswith('HTTP/1.1 400 Bad request')) + +class TestProxyProtocolRequests(SocketTest): + def test_proxy_version1(self): + proxy = "PROXY TCP4 192.168.242.221 192.168.242.242 56324 31337\r\n" + req = '''GET /proxy HTTP/1.1\r +Connection: keep-alive\r +Host: 192.168.0.11\r\n\r\n''' + + with self.connect() as sock: + for request in range(5): + sock.send(proxy + req if request == 0 else req) + response = sock.recv(4096) + self.assertTrue(response.startswith('HTTP/1.1 200 OK')) + self.assertTrue('X-Proxy: 192.168.242.221' in response) + + def test_proxy_version2(self): + proxy = ( + "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A" + "\x21\x11\x00\x0B" + "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B" + ) + req = '''GET /proxy HTTP/1.1\r +Connection: keep-alive\r +Host: 192.168.0.11\r\n\r\n''' + + with self.connect() as sock: + for request in range(5): + sock.send(proxy + req if request == 0 else req) + response = sock.recv(4096) + self.assertTrue(response.startswith('HTTP/1.1 200 OK')) + self.assertTrue('X-Proxy: 1.2.3.4' in response) + +class TestPipelinedRequests(SocketTest): + def test_pipelined_requests(self): + response_separator = re.compile('\r\n\r\n') + names = ['name%04x' % x for x in range(256)] + reqs = '\r\n\r\n'.join('''GET /hello?name=%s HTTP/1.1\r +Host: localhost\r +Connection: keep-alive\r +Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7''' % name for name in names) + reqs += '\r\n\r\n' + + with self.connect() as sock: + sock.send(reqs) + + responses = '' + while len(response_separator.findall(responses)) != len(names): + response = sock.recv(4096) + if response: + responses += response + else: + break + + for name in names: + s = 'Hello, %s!' % name + self.assertTrue(s in responses) + responses = responses.replace(s, '') + + +class TestArtificialResponse(LwanTest): + def test_brew_coffee(self): + r = requests.get('http://127.0.0.1:8080/brew-coffee') + + self.assertEqual(r.status_code, 418) + + +class TestSleep(LwanTest): + def test_sleep(self): + now = time.time() + requests.get('http://127.0.0.1:8080/sleep?ms=1500') + diff = time.time() - now + + self.assertTrue(1.450 < diff < 1.550) + + +class TestRequest(LwanTest): + def test_custom_header_exists(self): + h = {'Marco': 'Polo'} + r = requests.get('http://127.0.0.1:8080/customhdr?hdr=Marco', headers = h) + + self.assertEqual(r.text, "Header value: 'Polo'") + + def test_custom_header_does_not_exist(self): + h = {'Marco': 'Polo'} + r = requests.get('http://127.0.0.1:8080/customhdr?hdr=Polo', headers = h) + + self.assertEqual(r.status_code, 404) + + +class TestFuzzRegressionBase(SocketTest): + def setUp(self): + new_environment = os.environ.copy() + new_environment.update({'KEEP_ALIVE_TIMEOUT': '0'}) + super(SocketTest, self).setUp(env=new_environment) + + def run_test(self, contents): + with self.connect() as sock: + sock.send(contents) + first_8 = sock.recv(8) + self.assertTrue(first_8 in ("HTTP/1.1", "HTTP/1.0", "")) + + @staticmethod + def wrap(name): + with open(os.path.join("fuzz", "regression", name), "rb") as f: + contents = str(f.read(), "latin-1") + def run_test_wrapped(self): + return self.run_test(contents) + return run_test_wrapped + +def only_request_fuzzer_regression(): + for path in os.listdir("fuzz/regression"): + if not "request_fuzzer" in path: + continue + if path.startswith(("clusterfuzz-", "crash-")): + yield path + +TestFuzzRegression = type('TestFuzzRegression', (TestFuzzRegressionBase,), { + "test_" + name.replace("-", "_"): TestFuzzRegressionBase.wrap(name) for name in only_request_fuzzer_regression() +}) + +if __name__ == '__main__': + unittest.main() diff --git a/techempower/CMakeLists.txt b/techempower/CMakeLists.txt deleted file mode 100644 index 65525a9ae..000000000 --- a/techempower/CMakeLists.txt +++ /dev/null @@ -1,41 +0,0 @@ -include(FindPkgConfig) -pkg_check_modules(SQLITE sqlite3>=3.6.20) - -find_path(MYSQL_INCLUDE_DIR mysql.h - /usr/local/include/mysql - /usr/include/mysql -) - -if (MYSQL_INCLUDE_DIR AND SQLITE_FOUND) - message(STATUS "Found MySQL includes at ${MYSQL_INCLUDE_DIR}") - include_directories(AFTER ${MYSQL_INCLUDE_DIR}) - - set(MYSQL_NAMES mysqlclient mysqlclient_r) - find_library(MYSQL_LIBRARY - NAMES ${MYSQL_NAMES} - PATH_SUFFIXES mysql - ) - - if (MYSQL_LIBRARY) - message(STATUS "Found MySQL client library at ${MYSQL_LIBRARY}") - - add_executable(techempower - techempower.c - json.c - array.c - database.c - ) - - target_link_libraries(techempower - -Wl,-whole-archive lwan-common -Wl,-no-whole-archive - dl - ${ADDITIONAL_LIBRARIES} - ${SQLITE_LIBRARIES} - ${MYSQL_LIBRARY} - ) - endif () -endif () - -if (NOT MYSQL_LIBRARY) - message(STATUS "Not building benchmark suite: database libraries not found.") -endif () diff --git a/techempower/array.c b/techempower/array.c deleted file mode 100644 index 7b963e319..000000000 --- a/techempower/array.c +++ /dev/null @@ -1,116 +0,0 @@ -/* - * libkmod - interface to kernel module operations - * - * Copyright (C) 2011-2013 ProFUSION embedded systems - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "array.h" -#include "reallocarray.h" - -#include -#include -#include -#include -#include - -/* basic pointer array growing in steps */ - - -static int array_realloc(struct array *array, size_t new_total) -{ - void *tmp = reallocarray(array->array, sizeof(void *), new_total); - if (tmp == NULL) - return -ENOMEM; - array->array = tmp; - array->total = new_total; - return 0; -} - -void array_init(struct array *array, size_t step) -{ - assert(step > 0); - array->array = NULL; - array->count = 0; - array->total = 0; - array->step = step; -} - -int array_append(struct array *array, const void *element) -{ - size_t idx; - - if (array->count + 1 >= array->total) { - int r = array_realloc(array, array->total + array->step); - if (r < 0) - return r; - } - idx = array->count; - array->array[idx] = (void *)element; - array->count++; - return (int)idx; -} - -int array_append_unique(struct array *array, const void *element) -{ - void **itr = array->array; - void **itr_end = itr + array->count; - for (; itr < itr_end; itr++) - if (*itr == element) - return -EEXIST; - return array_append(array, element); -} - -void array_pop(struct array *array) { - array->count--; - if (array->count + array->step < array->total) { - int r = array_realloc(array, array->total - array->step); - if (r < 0) - return; - } -} - -void array_free_array(struct array *array) { - free(array->array); - array->count = 0; - array->total = 0; -} - - -void array_sort(struct array *array, int (*cmp)(const void *a, const void *b)) -{ - qsort(array->array, array->count, sizeof(void *), cmp); -} - -int array_remove_at(struct array *array, unsigned int pos) -{ - if (array->count <= pos) - return -ENOENT; - - array->count--; - if (pos < array->count) - memmove(array->array + pos, array->array + pos + 1, - sizeof(void *) * (array->count - pos)); - - if (array->count + array->step < array->total) { - int r = array_realloc(array, array->total - array->step); - /* ignore error */ - if (r < 0) - return 0; - } - - return 0; -} diff --git a/techempower/array.h b/techempower/array.h deleted file mode 100644 index b88482fe4..000000000 --- a/techempower/array.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -/* - * Declaration of struct array is in header because we may want to embed the - * structure into another, so we need to know its size - */ -struct array { - void **array; - size_t count; - size_t total; - size_t step; -}; - -void array_init(struct array *array, size_t step); -int array_append(struct array *array, const void *element); -int array_append_unique(struct array *array, const void *element); -void array_pop(struct array *array); -void array_free_array(struct array *array); -void array_sort(struct array *array, int (*cmp)(const void *a, const void *b)); -int array_remove_at(struct array *array, unsigned int pos); diff --git a/techempower/json.c b/techempower/json.c deleted file mode 100644 index 203474e81..000000000 --- a/techempower/json.c +++ /dev/null @@ -1,1384 +0,0 @@ -/* - Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) - All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -#include "json.h" - -#include -#include -#include -#include -#include - -#define out_of_memory() do { \ - fprintf(stderr, "Out of memory.\n"); \ - exit(EXIT_FAILURE); \ - } while (0) - -/* String buffer */ - -typedef struct -{ - char *cur; - char *end; - char *start; -} SB; - -static void sb_init(SB *sb) -{ - sb->start = (char*) malloc(17); - if (sb->start == NULL) - out_of_memory(); - sb->cur = sb->start; - sb->end = sb->start + 16; -} - -/* sb and need may be evaluated multiple times. */ -#define sb_need(sb, need) do { \ - if ((size_t)((sb)->end - (sb)->cur) < (need)) \ - sb_grow(sb, need); \ - } while (0) - -static void sb_grow(SB *sb, size_t need) -{ - size_t length = (size_t)(sb->cur - sb->start); - size_t alloc = (size_t)(sb->end - sb->start); - - do { - alloc *= 2; - } while (alloc < length + need); - - sb->start = (char*) realloc(sb->start, alloc + 1); - if (sb->start == NULL) - out_of_memory(); - sb->cur = sb->start + length; - sb->end = sb->start + alloc; -} - -static void sb_put(SB *sb, const char *bytes, size_t count) -{ - sb_need(sb, count); - memcpy(sb->cur, bytes, count); - sb->cur += count; -} - -#define sb_putc(sb, c) do { \ - if ((sb)->cur >= (sb)->end) \ - sb_grow(sb, 1); \ - *(sb)->cur++ = (c); \ - } while (0) - -static void sb_puts(SB *sb, const char *str) -{ - sb_put(sb, str, strlen(str)); -} - -static char *sb_finish(SB *sb) -{ - *sb->cur = 0; - assert(sb->start <= sb->cur && strlen(sb->start) == (size_t)(sb->cur - sb->start)); - return sb->start; -} - -static char *sb_finish_length(SB *sb, size_t *length) -{ - *sb->cur = 0; - assert(sb->start <= sb->cur && strlen(sb->start) == (size_t)(sb->cur - sb->start)); - *length = (size_t)(sb->cur - sb->start); - return sb->start; -} - -static void sb_free(SB *sb) -{ - free(sb->start); -} - -/* - * Unicode helper functions - * - * These are taken from the ccan/charset module and customized a bit. - * Putting them here means the compiler can (choose to) inline them, - * and it keeps ccan/json from having a dependency. - */ - -/* - * Type for Unicode codepoints. - * We need our own because wchar_t might be 16 bits. - */ -typedef uint32_t uchar_t; - -/* - * Validate a single UTF-8 character starting at @s. - * The string must be null-terminated. - * - * If it's valid, return its length (1 thru 4). - * If it's invalid or clipped, return 0. - * - * This function implements the syntax given in RFC3629, which is - * the same as that given in The Unicode Standard, Version 6.0. - * - * It has the following properties: - * - * * All codepoints U+0000..U+10FFFF may be encoded, - * except for U+D800..U+DFFF, which are reserved - * for UTF-16 surrogate pair encoding. - * * UTF-8 byte sequences longer than 4 bytes are not permitted, - * as they exceed the range of Unicode. - * * The sixty-six Unicode "non-characters" are permitted - * (namely, U+FDD0..U+FDEF, U+xxFFFE, and U+xxFFFF). - */ -static size_t utf8_validate_cz(const char *s) -{ - unsigned char c = (unsigned char)*s++; - - if (c <= 0x7F) { /* 00..7F */ - return 1; - } else if (c <= 0xC1) { /* 80..C1 */ - /* Disallow overlong 2-byte sequence. */ - return 0; - } else if (c <= 0xDF) { /* C2..DF */ - /* Make sure subsequent byte is in the range 0x80..0xBF. */ - if (((unsigned char)*s++ & 0xC0) != 0x80) - return 0; - - return 2; - } else if (c <= 0xEF) { /* E0..EF */ - /* Disallow overlong 3-byte sequence. */ - if (c == 0xE0 && (unsigned char)*s < 0xA0) - return 0; - - /* Disallow U+D800..U+DFFF. */ - if (c == 0xED && (unsigned char)*s > 0x9F) - return 0; - - /* Make sure subsequent bytes are in the range 0x80..0xBF. */ - if (((unsigned char)*s++ & 0xC0) != 0x80) - return 0; - if (((unsigned char)*s++ & 0xC0) != 0x80) - return 0; - - return 3; - } else if (c <= 0xF4) { /* F0..F4 */ - /* Disallow overlong 4-byte sequence. */ - if (c == 0xF0 && (unsigned char)*s < 0x90) - return 0; - - /* Disallow codepoints beyond U+10FFFF. */ - if (c == 0xF4 && (unsigned char)*s > 0x8F) - return 0; - - /* Make sure subsequent bytes are in the range 0x80..0xBF. */ - if (((unsigned char)*s++ & 0xC0) != 0x80) - return 0; - if (((unsigned char)*s++ & 0xC0) != 0x80) - return 0; - if (((unsigned char)*s++ & 0xC0) != 0x80) - return 0; - - return 4; - } else { /* F5..FF */ - return 0; - } -} - -/* Validate a null-terminated UTF-8 string. */ -static bool utf8_validate(const char *s) -{ - size_t len; - - for (; *s != 0; s += len) { - len = utf8_validate_cz(s); - if (len == 0) - return false; - } - - return true; -} - -/* - * Read a single UTF-8 character starting at @s, - * returning the length, in bytes, of the character read. - * - * This function assumes input is valid UTF-8, - * and that there are enough characters in front of @s. - */ -static int utf8_read_char(const char *s, uchar_t *out) -{ - const unsigned char *c = (const unsigned char*) s; - - assert(utf8_validate_cz(s)); - - if (c[0] <= 0x7F) { - /* 00..7F */ - *out = c[0]; - return 1; - } else if (c[0] <= 0xDF) { - /* C2..DF (unless input is invalid) */ - *out = ((uchar_t)c[0] & 0x1F) << 6 | - ((uchar_t)c[1] & 0x3F); - return 2; - } else if (c[0] <= 0xEF) { - /* E0..EF */ - *out = ((uchar_t)c[0] & 0xF) << 12 | - ((uchar_t)c[1] & 0x3F) << 6 | - ((uchar_t)c[2] & 0x3F); - return 3; - } else { - /* F0..F4 (unless input is invalid) */ - *out = ((uchar_t)c[0] & 0x7) << 18 | - ((uchar_t)c[1] & 0x3F) << 12 | - ((uchar_t)c[2] & 0x3F) << 6 | - ((uchar_t)c[3] & 0x3F); - return 4; - } -} - -/* - * Write a single UTF-8 character to @s, - * returning the length, in bytes, of the character written. - * - * @unicode must be U+0000..U+10FFFF, but not U+D800..U+DFFF. - * - * This function will write up to 4 bytes to @out. - */ -static int utf8_write_char(uchar_t unicode, char *out) -{ - unsigned char *o = (unsigned char*) out; - - assert(unicode <= 0x10FFFF && !(unicode >= 0xD800 && unicode <= 0xDFFF)); - - if (unicode <= 0x7F) { - /* U+0000..U+007F */ - *o++ = (unsigned char)unicode; - return 1; - } else if (unicode <= 0x7FF) { - /* U+0080..U+07FF */ - *o++ = (unsigned char)(0xC0 | unicode >> 6); - *o++ = (unsigned char)(0x80 | (unicode & 0x3F)); - return 2; - } else if (unicode <= 0xFFFF) { - /* U+0800..U+FFFF */ - *o++ = (unsigned char)(0xE0 | unicode >> 12); - *o++ = (unsigned char)(0x80 | (unicode >> 6 & 0x3F)); - *o++ = (unsigned char)(0x80 | (unicode & 0x3F)); - return 3; - } else { - /* U+10000..U+10FFFF */ - *o++ = (unsigned char)(0xF0 | unicode >> 18); - *o++ = (unsigned char)(0x80 | (unicode >> 12 & 0x3F)); - *o++ = (unsigned char)(0x80 | (unicode >> 6 & 0x3F)); - *o++ = (unsigned char)(0x80 | (unicode & 0x3F)); - return 4; - } -} - -/* - * Compute the Unicode codepoint of a UTF-16 surrogate pair. - * - * @uc should be 0xD800..0xDBFF, and @lc should be 0xDC00..0xDFFF. - * If they aren't, this function returns false. - */ -static bool from_surrogate_pair(uint16_t uc, uint16_t lc, uchar_t *unicode) -{ - if (uc >= 0xD800 && uc <= 0xDBFF && lc >= 0xDC00 && lc <= 0xDFFF) { - *unicode = 0x10000 + ((((uchar_t)uc & 0x3FF) << 10) | (lc & 0x3FF)); - return true; - } else { - return false; - } -} - -/* - * Construct a UTF-16 surrogate pair given a Unicode codepoint. - * - * @unicode must be U+10000..U+10FFFF. - */ -static void to_surrogate_pair(uchar_t unicode, uint16_t *uc, uint16_t *lc) -{ - uchar_t n; - - assert(unicode >= 0x10000 && unicode <= 0x10FFFF); - - n = unicode - 0x10000; - *uc = (uint16_t)(((n >> 10) & 0x3FF) | 0xD800); - *lc = (uint16_t)((n & 0x3FF) | 0xDC00); -} - -#define is_space(c) ((c) == '\t' || (c) == '\n' || (c) == '\r' || (c) == ' ') -#define is_digit(c) ((c) >= '0' && (c) <= '9') - -static bool parse_value (const char **sp, JsonNode **out); -static bool parse_string (const char **sp, char **out); -static bool parse_number (const char **sp, double *out); -static bool parse_array (const char **sp, JsonNode **out); -static bool parse_object (const char **sp, JsonNode **out); -static bool parse_hex16 (const char **sp, uint16_t *out); - -static bool expect_literal (const char **sp, const char *str); -static void skip_space (const char **sp); - -static void emit_value (SB *out, const JsonNode *node); -static void emit_value_indented (SB *out, const JsonNode *node, const char *space, int indent_level); -static void emit_string (SB *out, const char *str); -static void emit_number (SB *out, double num); -static void emit_array (SB *out, const JsonNode *array); -static void emit_array_indented (SB *out, const JsonNode *array, const char *space, int indent_level); -static void emit_object (SB *out, const JsonNode *object); -static void emit_object_indented (SB *out, const JsonNode *object, const char *space, int indent_level); - -static int write_hex16(char *out, uint16_t val); - -static JsonNode *mknode(JsonTag tag); -static void append_node(JsonNode *parent, JsonNode *child); -static void prepend_node(JsonNode *parent, JsonNode *child); -static void append_member(JsonNode *object, char *key, JsonNode *value); - -/* Assertion-friendly validity checks */ -static bool tag_is_valid(unsigned int tag); -static bool number_is_valid(const char *num); - -JsonNode *json_decode(const char *json) -{ - const char *s = json; - JsonNode *ret; - - skip_space(&s); - if (!parse_value(&s, &ret)) - return NULL; - - skip_space(&s); - if (*s != 0) { - json_delete(ret); - return NULL; - } - - return ret; -} - -char *json_encode(const JsonNode *node) -{ - return json_stringify(node, NULL); -} - -char *json_encode_string(const char *str) -{ - SB sb; - sb_init(&sb); - - emit_string(&sb, str); - - return sb_finish(&sb); -} - -char *json_stringify(const JsonNode *node, const char *space) -{ - SB sb; - sb_init(&sb); - - if (space != NULL) - emit_value_indented(&sb, node, space, 0); - else - emit_value(&sb, node); - - return sb_finish(&sb); -} - -char *json_stringify_length(const JsonNode *node, const char *space, size_t *length) -{ - SB sb; - sb_init(&sb); - - if (space != NULL) - emit_value_indented(&sb, node, space, 0); - else - emit_value(&sb, node); - - return sb_finish_length(&sb, length); -} - -void json_delete(JsonNode *node) -{ - if (node != NULL) { - json_remove_from_parent(node); - - switch (node->tag) { - case JSON_STRING: - free(node->string_); - break; - case JSON_ARRAY: - case JSON_OBJECT: - { - JsonNode *child, *next; - for (child = node->children.head; child != NULL; child = next) { - next = child->next; - json_delete(child); - } - break; - } - default:; - } - - free(node); - } -} - -bool json_validate(const char *json) -{ - const char *s = json; - - skip_space(&s); - if (!parse_value(&s, NULL)) - return false; - - skip_space(&s); - if (*s != 0) - return false; - - return true; -} - -JsonNode *json_find_element(JsonNode *array, int index) -{ - JsonNode *element; - int i = 0; - - if (array == NULL || array->tag != JSON_ARRAY) - return NULL; - - json_foreach(element, array) { - if (i == index) - return element; - i++; - } - - return NULL; -} - -JsonNode *json_find_member(JsonNode *object, const char *name) -{ - JsonNode *member; - - if (object == NULL || object->tag != JSON_OBJECT) - return NULL; - - json_foreach(member, object) - if (strcmp(member->key, name) == 0) - return member; - - return NULL; -} - -JsonNode *json_first_child(const JsonNode *node) -{ - if (node != NULL && (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT)) - return node->children.head; - return NULL; -} - -static JsonNode *mknode(JsonTag tag) -{ - JsonNode *ret = (JsonNode*) calloc(1, sizeof(JsonNode)); - if (ret == NULL) - out_of_memory(); - ret->tag = tag; - return ret; -} - -JsonNode *json_mknull(void) -{ - return mknode(JSON_NULL); -} - -JsonNode *json_mkbool(bool b) -{ - JsonNode *ret = mknode(JSON_BOOL); - ret->bool_ = b; - return ret; -} - -static JsonNode *mkstring(char *s) -{ - JsonNode *ret = mknode(JSON_STRING); - ret->string_ = s; - return ret; -} - -JsonNode *json_mkstring(const char *s) -{ - return mkstring(strdup(s)); -} - -JsonNode *json_mknumber(double n) -{ - JsonNode *node = mknode(JSON_NUMBER); - node->number_ = n; - return node; -} - -JsonNode *json_mkarray(void) -{ - return mknode(JSON_ARRAY); -} - -JsonNode *json_mkobject(void) -{ - return mknode(JSON_OBJECT); -} - -static void append_node(JsonNode *parent, JsonNode *child) -{ - child->parent = parent; - child->prev = parent->children.tail; - child->next = NULL; - - if (parent->children.tail != NULL) - parent->children.tail->next = child; - else - parent->children.head = child; - parent->children.tail = child; -} - -static void prepend_node(JsonNode *parent, JsonNode *child) -{ - child->parent = parent; - child->prev = NULL; - child->next = parent->children.head; - - if (parent->children.head != NULL) - parent->children.head->prev = child; - else - parent->children.tail = child; - parent->children.head = child; -} - -static void append_member(JsonNode *object, char *key, JsonNode *value) -{ - value->key = key; - append_node(object, value); -} - -void json_append_element(JsonNode *array, JsonNode *element) -{ - assert(array->tag == JSON_ARRAY); - assert(element->parent == NULL); - - append_node(array, element); -} - -void json_prepend_element(JsonNode *array, JsonNode *element) -{ - assert(array->tag == JSON_ARRAY); - assert(element->parent == NULL); - - prepend_node(array, element); -} - -void json_append_member(JsonNode *object, const char *key, JsonNode *value) -{ - assert(object->tag == JSON_OBJECT); - assert(value->parent == NULL); - - append_member(object, strdup(key), value); -} - -void json_prepend_member(JsonNode *object, const char *key, JsonNode *value) -{ - assert(object->tag == JSON_OBJECT); - assert(value->parent == NULL); - - value->key = strdup(key); - prepend_node(object, value); -} - -void json_remove_from_parent(JsonNode *node) -{ - JsonNode *parent = node->parent; - - if (parent != NULL) { - if (node->prev != NULL) - node->prev->next = node->next; - else - parent->children.head = node->next; - if (node->next != NULL) - node->next->prev = node->prev; - else - parent->children.tail = node->prev; - - free(node->key); - - node->parent = NULL; - node->prev = node->next = NULL; - node->key = NULL; - } -} - -static bool parse_value(const char **sp, JsonNode **out) -{ - const char *s = *sp; - - switch (*s) { - case 'n': - if (expect_literal(&s, "null")) { - if (out) - *out = json_mknull(); - *sp = s; - return true; - } - return false; - - case 'f': - if (expect_literal(&s, "false")) { - if (out) - *out = json_mkbool(false); - *sp = s; - return true; - } - return false; - - case 't': - if (expect_literal(&s, "true")) { - if (out) - *out = json_mkbool(true); - *sp = s; - return true; - } - return false; - - case '"': { - char *str; - if (parse_string(&s, out ? &str : NULL)) { - if (out) - *out = mkstring(str); - *sp = s; - return true; - } - return false; - } - - case '[': - if (parse_array(&s, out)) { - *sp = s; - return true; - } - return false; - - case '{': - if (parse_object(&s, out)) { - *sp = s; - return true; - } - return false; - - default: { - double num; - if (parse_number(&s, out ? &num : NULL)) { - if (out) - *out = json_mknumber(num); - *sp = s; - return true; - } - return false; - } - } -} - -static bool parse_array(const char **sp, JsonNode **out) -{ - const char *s = *sp; - JsonNode *ret = out ? json_mkarray() : NULL; - JsonNode *element; - - if (*s++ != '[') - goto failure; - skip_space(&s); - - if (*s == ']') { - s++; - goto success; - } - - for (;;) { - if (!parse_value(&s, out ? &element : NULL)) - goto failure; - skip_space(&s); - - if (out) - json_append_element(ret, element); - - if (*s == ']') { - s++; - goto success; - } - - if (*s++ != ',') - goto failure; - skip_space(&s); - } - -success: - *sp = s; - if (out) - *out = ret; - return true; - -failure: - json_delete(ret); - return false; -} - -static bool parse_object(const char **sp, JsonNode **out) -{ - const char *s = *sp; - JsonNode *ret = out ? json_mkobject() : NULL; - char *key; - JsonNode *value; - - if (*s++ != '{') - goto failure; - skip_space(&s); - - if (*s == '}') { - s++; - goto success; - } - - for (;;) { - if (!parse_string(&s, out ? &key : NULL)) - goto failure; - skip_space(&s); - - if (*s++ != ':') - goto failure_free_key; - skip_space(&s); - - if (!parse_value(&s, out ? &value : NULL)) - goto failure_free_key; - skip_space(&s); - - if (out) - append_member(ret, key, value); - - if (*s == '}') { - s++; - goto success; - } - - if (*s++ != ',') - goto failure; - skip_space(&s); - } - -success: - *sp = s; - if (out) - *out = ret; - return true; - -failure_free_key: - if (out) - free(key); -failure: - json_delete(ret); - return false; -} - -bool parse_string(const char **sp, char **out) -{ - const char *s = (char *)*sp; - SB sb = {0}; - char throwaway_buffer[4]; - /* enough space for a UTF-8 character */ - char *b; - - if (*s++ != '"') - return false; - - if (out) { - sb_init(&sb); - sb_need(&sb, 4); - b = sb.cur; - } else { - b = throwaway_buffer; - } - - while (*s != '"') { - char c = *s++; - - /* Parse next character, and write it to b. */ - if (c == '\\') { - c = *s++; - switch (c) { - case '"': - case '\\': - case '/': - *b++ = (char)c; - break; - case 'b': - *b++ = '\b'; - break; - case 'f': - *b++ = '\f'; - break; - case 'n': - *b++ = '\n'; - break; - case 'r': - *b++ = '\r'; - break; - case 't': - *b++ = '\t'; - break; - case 'u': - { - uint16_t uc, lc; - uchar_t unicode; - - if (!parse_hex16(&s, &uc)) - goto failed; - - if (uc >= 0xD800 && uc <= 0xDFFF) { - /* Handle UTF-16 surrogate pair. */ - if (*s++ != '\\' || *s++ != 'u' || !parse_hex16(&s, &lc)) - goto failed; /* Incomplete surrogate pair. */ - if (!from_surrogate_pair(uc, lc, &unicode)) - goto failed; /* Invalid surrogate pair. */ - } else if (uc == 0) { - /* Disallow "\u0000". */ - goto failed; - } else { - unicode = uc; - } - - b += utf8_write_char(unicode, b); - break; - } - default: - /* Invalid escape */ - goto failed; - } - } else if (c <= 0x1F) { - /* Control characters are not allowed in string literals. */ - goto failed; - } else { - /* Validate and echo a UTF-8 character. */ - size_t len; - - s--; - len = utf8_validate_cz(s); - if (len == 0) - goto failed; /* Invalid UTF-8 character. */ - - while (len--) - *b++ = *s++; - } - - /* - * Update sb to know about the new bytes, - * and set up b to write another character. - */ - if (out) { - sb.cur = b; - sb_need(&sb, 4); - b = sb.cur; - } else { - b = throwaway_buffer; - } - } - s++; - - if (out) - *out = sb_finish(&sb); - *sp = s; - return true; - -failed: - if (out) - sb_free(&sb); - return false; -} - -/* - * The JSON spec says that a number shall follow this precise pattern - * (spaces and quotes added for readability): - * '-'? (0 | [1-9][0-9]*) ('.' [0-9]+)? ([Ee] [+-]? [0-9]+)? - * - * However, some JSON parsers are more liberal. For instance, PHP accepts - * '.5' and '1.'. JSON.parse accepts '+3'. - * - * This function takes the strict approach. - */ -bool parse_number(const char **sp, double *out) -{ - const char *s = *sp; - - /* '-'? */ - if (*s == '-') - s++; - - /* (0 | [1-9][0-9]*) */ - if (*s == '0') { - s++; - } else { - if (!is_digit(*s)) - return false; - do { - s++; - } while (is_digit(*s)); - } - - /* ('.' [0-9]+)? */ - if (*s == '.') { - s++; - if (!is_digit(*s)) - return false; - do { - s++; - } while (is_digit(*s)); - } - - /* ([Ee] [+-]? [0-9]+)? */ - if (*s == 'E' || *s == 'e') { - s++; - if (*s == '+' || *s == '-') - s++; - if (!is_digit(*s)) - return false; - do { - s++; - } while (is_digit(*s)); - } - - if (out) - *out = strtod(*sp, NULL); - - *sp = s; - return true; -} - -static void skip_space(const char **sp) -{ - const char *s = *sp; - while (is_space(*s)) - s++; - *sp = s; -} - -static void emit_value(SB *out, const JsonNode *node) -{ - assert(tag_is_valid(node->tag)); - switch (node->tag) { - case JSON_NULL: - sb_puts(out, "null"); - break; - case JSON_BOOL: - sb_puts(out, node->bool_ ? "true" : "false"); - break; - case JSON_STRING: - emit_string(out, node->string_); - break; - case JSON_NUMBER: - emit_number(out, node->number_); - break; - case JSON_ARRAY: - emit_array(out, node); - break; - case JSON_OBJECT: - emit_object(out, node); - break; - default: - assert(false); - } -} - -void emit_value_indented(SB *out, const JsonNode *node, const char *space, int indent_level) -{ - assert(tag_is_valid(node->tag)); - switch (node->tag) { - case JSON_NULL: - sb_puts(out, "null"); - break; - case JSON_BOOL: - sb_puts(out, node->bool_ ? "true" : "false"); - break; - case JSON_STRING: - emit_string(out, node->string_); - break; - case JSON_NUMBER: - emit_number(out, node->number_); - break; - case JSON_ARRAY: - emit_array_indented(out, node, space, indent_level); - break; - case JSON_OBJECT: - emit_object_indented(out, node, space, indent_level); - break; - default: - assert(false); - } -} - -static void emit_array(SB *out, const JsonNode *array) -{ - const JsonNode *element; - - sb_putc(out, '['); - json_foreach(element, array) { - emit_value(out, element); - if (element->next != NULL) - sb_putc(out, ','); - } - sb_putc(out, ']'); -} - -static void emit_array_indented(SB *out, const JsonNode *array, const char *space, int indent_level) -{ - const JsonNode *element = array->children.head; - int i; - - if (element == NULL) { - sb_puts(out, "[]"); - return; - } - - sb_puts(out, "[\n"); - while (element != NULL) { - for (i = 0; i < indent_level + 1; i++) - sb_puts(out, space); - emit_value_indented(out, element, space, indent_level + 1); - - element = element->next; - sb_puts(out, element != NULL ? ",\n" : "\n"); - } - for (i = 0; i < indent_level; i++) - sb_puts(out, space); - sb_putc(out, ']'); -} - -static void emit_object(SB *out, const JsonNode *object) -{ - const JsonNode *member; - - sb_putc(out, '{'); - json_foreach(member, object) { - emit_string(out, member->key); - sb_putc(out, ':'); - emit_value(out, member); - if (member->next != NULL) - sb_putc(out, ','); - } - sb_putc(out, '}'); -} - -static void emit_object_indented(SB *out, const JsonNode *object, const char *space, int indent_level) -{ - const JsonNode *member = object->children.head; - int i; - - if (member == NULL) { - sb_puts(out, "{}"); - return; - } - - sb_puts(out, "{\n"); - while (member != NULL) { - for (i = 0; i < indent_level + 1; i++) - sb_puts(out, space); - emit_string(out, member->key); - sb_puts(out, ": "); - emit_value_indented(out, member, space, indent_level + 1); - - member = member->next; - sb_puts(out, member != NULL ? ",\n" : "\n"); - } - for (i = 0; i < indent_level; i++) - sb_puts(out, space); - sb_putc(out, '}'); -} - -void emit_string(SB *out, const char *str) -{ - const char *s = str; - char *b; - - assert(utf8_validate(str)); - - /* - * 14 bytes is enough space to write up to two - * \uXXXX escapes and two quotation marks. - */ - sb_need(out, 14); - b = out->cur; - - *b++ = '"'; - while (*s != 0) { - char c = *s++; - - /* Encode the next character, and write it to b. */ - switch (c) { - case '"': - *b++ = '\\'; - *b++ = '"'; - break; - case '\\': - *b++ = '\\'; - *b++ = '\\'; - break; - case '\b': - *b++ = '\\'; - *b++ = 'b'; - break; - case '\f': - *b++ = '\\'; - *b++ = 'f'; - break; - case '\n': - *b++ = '\\'; - *b++ = 'n'; - break; - case '\r': - *b++ = '\\'; - *b++ = 'r'; - break; - case '\t': - *b++ = '\\'; - *b++ = 't'; - break; - default: { - size_t len; - - s--; - len = utf8_validate_cz(s); - - if (len == 0) { - /* - * Handle invalid UTF-8 character gracefully in production - * by writing a replacement character (U+FFFD) - * and skipping a single byte. - * - * This should never happen when assertions are enabled - * due to the assertion at the beginning of this function. - */ - assert(false); - *b++ = (char)0xEF; - *b++ = (char)0xBF; - *b++ = (char)0xBD; - s++; - } else if (c < 0x1F) { - /* Encode using \u.... */ - uint32_t unicode; - - s += utf8_read_char(s, &unicode); - - if (unicode <= 0xFFFF) { - *b++ = '\\'; - *b++ = 'u'; - b += write_hex16(b, (uint16_t)unicode); - } else { - /* Produce a surrogate pair. */ - uint16_t uc, lc; - assert(unicode <= 0x10FFFF); - to_surrogate_pair(unicode, &uc, &lc); - *b++ = '\\'; - *b++ = 'u'; - b += write_hex16(b, uc); - *b++ = '\\'; - *b++ = 'u'; - b += write_hex16(b, lc); - } - } else { - /* Write the character directly. */ - while (len--) - *b++ = *s++; - } - - break; - } - } - - /* - * Update *out to know about the new bytes, - * and set up b to write another encoded character. - */ - out->cur = b; - sb_need(out, 14); - b = out->cur; - } - *b++ = '"'; - - out->cur = b; -} - -static void emit_number(SB *out, double num) -{ - /* - * This isn't exactly how JavaScript renders numbers, - * but it should produce valid JSON for reasonable numbers - * preserve precision well enough, and avoid some oddities - * like 0.3 -> 0.299999999999999988898 . - */ - char buf[64]; - sprintf(buf, "%.16g", num); - - if (number_is_valid(buf)) - sb_puts(out, buf); - else - sb_puts(out, "null"); -} - -static bool tag_is_valid(unsigned int tag) -{ - return (/* tag >= JSON_NULL && */ tag <= JSON_OBJECT); -} - -static bool number_is_valid(const char *num) -{ - return (parse_number(&num, NULL) && *num == '\0'); -} - -static bool expect_literal(const char **sp, const char *str) -{ - const char *s = *sp; - - while (*str != '\0') - if (*s++ != *str++) - return false; - - *sp = s; - return true; -} - -/* - * Parses exactly 4 hex characters (capital or lowercase). - * Fails if any input chars are not [0-9A-Fa-f]. - */ -static bool parse_hex16(const char **sp, uint16_t *out) -{ - const char *s = *sp; - uint16_t ret = 0; - uint16_t i; - uint16_t tmp; - - for (i = 0; i < 4; i++) { - char c = *s++; - if (c >= '0' && c <= '9') - tmp = (uint16_t)(c - '0'); - else if (c >= 'A' && c <= 'F') - tmp = (uint16_t)(c - 'A' + 10); - else if (c >= 'a' && c <= 'f') - tmp = (uint16_t)(c - 'a' + 10); - else - return false; - - ret = (uint16_t)((ret << 4) + tmp); - } - - if (out) - *out = ret; - *sp = s; - return true; -} - -/* - * Encodes a 16-bit number into hexadecimal, - * writing exactly 4 hex chars. - */ -static int write_hex16(char *out, uint16_t val) -{ - const char *hex = "0123456789ABCDEF"; - - *out++ = hex[(val >> 12) & 0xF]; - *out++ = hex[(val >> 8) & 0xF]; - *out++ = hex[(val >> 4) & 0xF]; - *out++ = hex[ val & 0xF]; - - return 4; -} - -bool json_check(const JsonNode *node, char errmsg[256]) -{ - #define problem(...) do { \ - if (errmsg != NULL) \ - snprintf(errmsg, 256, __VA_ARGS__); \ - return false; \ - } while (0) - - if (node->key != NULL && !utf8_validate(node->key)) - problem("key contains invalid UTF-8"); - - if (!tag_is_valid(node->tag)) - problem("tag is invalid (%u)", node->tag); - - if (node->tag == JSON_BOOL) { - if (node->bool_ != false && node->bool_ != true) - problem("bool_ is neither false (%d) nor true (%d)", (int)false, (int)true); - } else if (node->tag == JSON_STRING) { - if (node->string_ == NULL) - problem("string_ is NULL"); - if (!utf8_validate(node->string_)) - problem("string_ contains invalid UTF-8"); - } else if (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT) { - JsonNode *head = node->children.head; - JsonNode *tail = node->children.tail; - - if (head == NULL || tail == NULL) { - if (head != NULL) - problem("tail is NULL, but head is not"); - if (tail != NULL) - problem("head is NULL, but tail is not"); - } else { - JsonNode *child; - JsonNode *last = NULL; - - if (head->prev != NULL) - problem("First child's prev pointer is not NULL"); - - for (child = head; child != NULL; last = child, child = child->next) { - if (child == node) - problem("node is its own child"); - if (child->next == child) - problem("child->next == child (cycle)"); - if (child->next == head) - problem("child->next == head (cycle)"); - - if (child->parent != node) - problem("child does not point back to parent"); - if (child->next != NULL && child->next->prev != child) - problem("child->next does not point back to child"); - - if (node->tag == JSON_ARRAY && child->key != NULL) - problem("Array element's key is not NULL"); - if (node->tag == JSON_OBJECT && child->key == NULL) - problem("Object member's key is NULL"); - - if (!json_check(child, errmsg)) - return false; - } - - if (last != tail) - problem("tail does not match pointer found by starting at head and following next links"); - } - } - - return true; - - #undef problem -} diff --git a/techempower/json.h b/techempower/json.h deleted file mode 100644 index 5a48b55ab..000000000 --- a/techempower/json.h +++ /dev/null @@ -1,116 +0,0 @@ -/* - Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) - All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -#pragma once - -#include -#include - -typedef enum { - JSON_NULL, - JSON_BOOL, - JSON_STRING, - JSON_NUMBER, - JSON_ARRAY, - JSON_OBJECT, -} JsonTag; - -typedef struct JsonNode JsonNode; - -struct JsonNode -{ - /* only if parent is an object or array (NULL otherwise) */ - JsonNode *parent; - JsonNode *prev, *next; - - /* only if parent is an object (NULL otherwise) */ - char *key; /* Must be valid UTF-8. */ - - JsonTag tag; - union { - /* JSON_BOOL */ - bool bool_; - - /* JSON_STRING */ - char *string_; /* Must be valid UTF-8. */ - - /* JSON_NUMBER */ - double number_; - - /* JSON_ARRAY */ - /* JSON_OBJECT */ - struct { - JsonNode *head, *tail; - } children; - }; -}; - -/*** Encoding, decoding, and validation ***/ - -JsonNode *json_decode (const char *json); -char *json_encode (const JsonNode *node); -char *json_encode_string (const char *str); -char *json_stringify (const JsonNode *node, const char *space); -char *json_stringify_length(const JsonNode *node, const char *space, size_t *length); -void json_delete (JsonNode *node); - -bool json_validate (const char *json); - -/*** Lookup and traversal ***/ - -JsonNode *json_find_element (JsonNode *array, int index); -JsonNode *json_find_member (JsonNode *object, const char *key); - -JsonNode *json_first_child (const JsonNode *node); - -#define json_foreach(i, object_or_array) \ - for ((i) = json_first_child(object_or_array); \ - (i) != NULL; \ - (i) = (i)->next) - -/*** Construction and manipulation ***/ - -JsonNode *json_mknull(void); -JsonNode *json_mkbool(bool b); -JsonNode *json_mkstring(const char *s); -JsonNode *json_mknumber(double n); -JsonNode *json_mkarray(void); -JsonNode *json_mkobject(void); - -void json_append_element(JsonNode *array, JsonNode *element); -void json_prepend_element(JsonNode *array, JsonNode *element); -void json_append_member(JsonNode *object, const char *key, JsonNode *value); -void json_prepend_member(JsonNode *object, const char *key, JsonNode *value); - -void json_remove_from_parent(JsonNode *node); - -/*** Debugging ***/ - -/* - * Look for structure and encoding problems in a JsonNode or its descendents. - * - * If a problem is detected, return false, writing a description of the problem - * to errmsg (unless errmsg is NULL). - */ -bool json_check(const JsonNode *node, char errmsg[256]); - diff --git a/techempower/techempower.c b/techempower/techempower.c deleted file mode 100644 index ce17021db..000000000 --- a/techempower/techempower.c +++ /dev/null @@ -1,361 +0,0 @@ -/* - * lwan - simple web server - * Copyright (c) 2014 Leandro A. F. Pereira - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include - -#include "lwan.h" -#include "lwan-config.h" -#include "lwan-template.h" - -#include "array.h" -#include "database.h" -#include "json.h" - -static const char hello_world[] = "Hello, World!"; -static const char random_number_query[] = "SELECT randomNumber FROM World WHERE id=?"; - -struct Fortune { - struct { - lwan_tpl_list_generator_t generator; - - int id; - char *message; - } item; -}; - -static const char fortunes_template_str[] = "" \ -"" \ -"Fortunes" \ -"" \ -"" \ -"" \ -"{{#item}}" \ -"" \ -"{{/item}}" \ -"
idmessage
{{item.id}}{{item.message}}
" \ -"" \ -""; - -static int fortune_list_generator(coro_t *coro); - -static const lwan_var_descriptor_t fortune_item_desc[] = { - TPL_VAR_INT(struct Fortune, item.id), - TPL_VAR_STR_ESCAPE(struct Fortune, item.message), - TPL_VAR_SENTINEL -}; - -static const lwan_var_descriptor_t fortune_desc[] = { - TPL_VAR_SEQUENCE(struct Fortune, item, - fortune_list_generator, fortune_item_desc), - TPL_VAR_SENTINEL -}; - -static struct db *database; -static lwan_tpl_t *fortune_tpl; - -static lwan_http_status_t -json_response(lwan_response_t *response, JsonNode *node) -{ - size_t length; - char *serialized; - - serialized = json_stringify_length(node, NULL, &length); - json_delete(node); - if (UNLIKELY(!serialized)) - return HTTP_INTERNAL_ERROR; - - strbuf_set(response->buffer, serialized, length); - free(serialized); - - response->mime_type = "application/json"; - return HTTP_OK; -} - -static lwan_http_status_t -json(lwan_request_t *request __attribute__((unused)), - lwan_response_t *response, - void *data __attribute__((unused))) -{ - JsonNode *hello = json_mkobject(); - if (UNLIKELY(!hello)) - return HTTP_INTERNAL_ERROR; - - json_append_member(hello, "message", json_mkstring(hello_world)); - - return json_response(response, hello); -} - -static JsonNode * -db_query(struct db_stmt *stmt, struct db_row rows[], struct db_row results[]) -{ - JsonNode *object = NULL; - int id = rand() % 10000; - - rows[0].u.i = id; - - if (UNLIKELY(!db_stmt_bind(stmt, rows, 1))) - goto out; - - if (UNLIKELY(!db_stmt_step(stmt, results))) - goto out; - - object = json_mkobject(); - if (UNLIKELY(!object)) - goto out; - - json_append_member(object, "id", json_mknumber(id)); - json_append_member(object, "randomNumber", json_mknumber(results[0].u.i)); - -out: - return object; -} - -static lwan_http_status_t -db(lwan_request_t *request __attribute__((unused)), - lwan_response_t *response, - void *data __attribute__((unused))) -{ - struct db_row rows[1] = {{ .kind = 'i' }}; - struct db_row results[] = {{ .kind = 'i' }, { .kind = '\0' }}; - struct db_stmt *stmt = db_prepare_stmt(database, random_number_query, - sizeof(random_number_query) - 1); - if (UNLIKELY(!stmt)) - return HTTP_INTERNAL_ERROR; - - JsonNode *object = db_query(stmt, rows, results); - db_stmt_finalize(stmt); - - if (UNLIKELY(!object)) - return HTTP_INTERNAL_ERROR; - - return json_response(response, object); -} - -static lwan_http_status_t -queries(lwan_request_t *request, - lwan_response_t *response, - void *data __attribute__((unused))) -{ - const char *queries_str = lwan_request_get_query_param(request, "queries"); - - if (UNLIKELY(!queries_str)) - return HTTP_BAD_REQUEST; - - long queries = parse_long(queries_str, -1); - if (UNLIKELY(queries <= 0)) - queries = 1; - else if (UNLIKELY(queries > 500)) - queries = 500; - - struct db_stmt *stmt = db_prepare_stmt(database, random_number_query, - sizeof(random_number_query) - 1); - if (UNLIKELY(!stmt)) - return HTTP_INTERNAL_ERROR; - - JsonNode *array = json_mkarray(); - if (UNLIKELY(!array)) - goto out_no_array; - - struct db_row rows[1] = {{ .kind = 'i' }}; - struct db_row results[] = {{ .kind = 'i' }, { .kind = '\0' }}; - while (queries--) { - JsonNode *object = db_query(stmt, rows, results); - - if (UNLIKELY(!object)) - goto out_array; - - json_append_element(array, object); - } - - db_stmt_finalize(stmt); - return json_response(response, array); - -out_array: - json_delete(array); -out_no_array: - db_stmt_finalize(stmt); - return HTTP_INTERNAL_ERROR; -} - -static lwan_http_status_t -plaintext(lwan_request_t *request __attribute__((unused)), - lwan_response_t *response, - void *data __attribute__((unused))) -{ - strbuf_set_static(response->buffer, hello_world, sizeof(hello_world) - 1); - - response->mime_type = "text/plain"; - return HTTP_OK; -} - -static int fortune_compare(const void *a, const void *b) -{ - const struct Fortune *fortune_a = *(const struct Fortune **)a; - const struct Fortune *fortune_b = *(const struct Fortune **)b; - size_t a_len = strlen(fortune_a->item.message); - size_t b_len = strlen(fortune_b->item.message); - - if (!a_len || !b_len) - return a_len > b_len; - - size_t min_len = a_len < b_len ? a_len : b_len; - int cmp = memcmp(fortune_a->item.message, fortune_b->item.message, min_len); - if (cmp == 0) - return a_len > b_len; - - return cmp > 0; -} - -static bool append_fortune(coro_t *coro, struct array *fortunes, - int id, const char *message) -{ - struct Fortune *fortune; - - fortune = coro_malloc(coro, sizeof(*fortune)); - if (UNLIKELY(!fortune)) - return false; - - fortune->item.id = id; - fortune->item.message = coro_strdup(coro, message); - if (UNLIKELY(!fortune->item.message)) - return false; - - return array_append(fortunes, fortune) >= 0; -} - -static int fortune_list_generator(coro_t *coro) -{ - static const char fortune_query[] = "SELECT * FROM Fortune"; - char fortune_buffer[256]; - struct Fortune *fortune; - struct array fortunes; - struct db_stmt *stmt; - size_t i; - - stmt = db_prepare_stmt(database, fortune_query, sizeof(fortune_query) - 1); - if (UNLIKELY(!stmt)) - return 0; - - array_init(&fortunes, 16); - - struct db_row results[] = { - { .kind = 'i' }, - { .kind = 's', .u.s = fortune_buffer, .buffer_length = sizeof(fortune_buffer) }, - { .kind = '\0' } - }; - while (db_stmt_step(stmt, results)) { - if (!append_fortune(coro, &fortunes, results[0].u.i, results[1].u.s)) - goto out; - } - - if (!append_fortune(coro, &fortunes, 0, - "Additional fortune added at request time.")) - goto out; - - array_sort(&fortunes, fortune_compare); - - fortune = coro_get_data(coro); - for (i = 0; i < fortunes.count; i++) { - struct Fortune *f = fortunes.array[i]; - fortune->item.id = f->item.id; - fortune->item.message = f->item.message; - coro_yield(coro, 1); - } - -out: - array_free_array(&fortunes); - db_stmt_finalize(stmt); - return 0; -} - -static lwan_http_status_t -fortunes(lwan_request_t *request __attribute__((unused)), - lwan_response_t *response, - void *data __attribute__((unused))) -{ - struct Fortune fortune; - - if (UNLIKELY(!lwan_tpl_apply_with_buffer(fortune_tpl, - response->buffer, &fortune))) - return HTTP_INTERNAL_ERROR; - - response->mime_type = "text/html; charset=UTF-8"; - return HTTP_OK; -} - -int -main(void) -{ - static const lwan_url_map_t url_map[] = { - { .prefix = "/json", .handler = json }, - { .prefix = "/db", .handler = db }, - { .prefix = "/queries", .handler = queries }, - { .prefix = "/plaintext", .handler = plaintext }, - { .prefix = "/fortunes", .handler = fortunes }, - { .prefix = NULL } - }; - lwan_t l; - - lwan_init(&l); - - srand((unsigned int)time(NULL)); - - if (getenv("USE_MYSQL")) { - const char *user = getenv("MYSQL_USER"); - const char *password = getenv("MYSQL_PASS"); - const char *hostname = getenv("MYSQL_HOST"); - const char *db = getenv("MYSQL_DB"); - - if (!user) - lwan_status_critical("No MySQL user provided"); - if (!password) - lwan_status_critical("No MySQL password provided"); - if (!hostname) - lwan_status_critical("No MySQL hostname provided"); - if (!db) - lwan_status_critical("No MySQL database provided"); - - database = db_connect_mysql(hostname, user, password, db); - } else { - const char *pragmas[] = { - "PRAGMA mmap_size=44040192", - "PRAGMA journal_mode=OFF", - "PRAGMA locking_mode=EXCLUSIVE", - NULL - }; - database = db_connect_sqlite("techempower.db", true, pragmas); - } - - if (!database) - lwan_status_critical("Could not connect to the database"); - - fortune_tpl = lwan_tpl_compile_string(fortunes_template_str, fortune_desc); - if (!fortune_tpl) - lwan_status_critical("Could not compile fortune templates"); - - lwan_set_url_map(&l, url_map); - lwan_main_loop(&l); - - lwan_tpl_free(fortune_tpl); - db_disconnect(database); - lwan_shutdown(&l); - - return 0; -} diff --git a/techempower/techempower.conf b/techempower/techempower.conf deleted file mode 100644 index a0a46dfd3..000000000 --- a/techempower/techempower.conf +++ /dev/null @@ -1,2 +0,0 @@ -listener *:8080 { -} diff --git a/tools/generate-mime-types-table.py b/tools/generate-mime-types-table.py deleted file mode 100755 index 3b45ec951..000000000 --- a/tools/generate-mime-types-table.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/python - -import sys -import struct -try: - import zopfli as zlib - zlib.compress -except: - try: - from zopfli import zopfli as zlib - zlib.compress - except: - import zlib -from operator import itemgetter - -def to_bytes(s): - try: - return bytes(s, 'ascii') - except TypeError: - return bytes(s) - -def pack_string(s): - return struct.pack('%dsb' % len(s), to_bytes(s), 0) - -known_exts = set() -types = [] -for l in open(sys.argv[1]): - l = l.strip() - if l.startswith('#'): - continue - if '#' in l: - l = l.split('#')[0].strip() - try: - last_tab = l.rindex('\t') - except ValueError: - continue - mime_type = l[:last_tab].strip() - for extension in l[last_tab:].split(): - if not extension in known_exts: - known_exts.add(extension) - types.append((mime_type, extension)) - -out = b'' -entries = 0 -for typ, ext in sorted(types, key = itemgetter(1)): - entries += 1 - out += pack_string(ext) - out += pack_string(typ) - -compressed_out = zlib.compress(out, 9) - -print('#pragma once') -print('/* Auto generated from generate-mime-types-table.py, do not modify */') - -print('#define MIME_UNCOMPRESSED_LEN %d' % len(out)) -print('#define MIME_COMPRESSED_LEN %d' % len(compressed_out)) -print('#define MIME_ENTRIES %d' % entries) - -print('struct mime_entry {') -print(' const char *extension;') -print(' const char *type;') -print('};') - -print('static const unsigned char mime_entries_compressed[] = {') -line = [] -for index, b in enumerate(compressed_out): - if index > 0 and index % 13 == 0: - print(' '.join(line)) - line = [] - - if isinstance(b, str): - b = ord(b) - - line.append('0x%x,' % b) - -if line: - print(' '.join(line)) - -print('};') diff --git a/tools/testsuite.py b/tools/testsuite.py deleted file mode 100755 index 9497e255f..000000000 --- a/tools/testsuite.py +++ /dev/null @@ -1,509 +0,0 @@ -#!/usr/bin/python -# TODO: Use tracy (https://github.com/MerlijnWajer/tracy) to see if lwan -# performs certain system calls. This should speed up the mmap tests -# considerably and make it possible to perform more low-level tests. - -import subprocess -import time -import unittest -import requests -import socket -import sys -import os -import re - -LWAN_PATH = './build/lwan/lwan' -for arg in sys.argv[1:]: - if not arg.startswith('-') and os.path.exists(arg): - LWAN_PATH = arg - sys.argv.remove(arg) - -print 'Using', LWAN_PATH, 'for lwan' - -class LwanTest(unittest.TestCase): - def setUp(self): - for spawn_try in range(20): - self.lwan=subprocess.Popen([LWAN_PATH], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - for request_try in range(20): - try: - requests.get('http://127.0.0.1:8080/hello') - return - except requests.ConnectionError: - time.sleep(0.1) - - time.sleep(0.1) - - raise Exception('Timeout waiting for lwan') - - def tearDown(self): - self.lwan.poll() - if self.lwan.returncode is not None: - self.assertEqual(self.lwan.returncode, 0) - else: - self.lwan.kill() - - def assertHttpResponseValid(self, request, status_code, content_type): - self.assertEqual(request.status_code, status_code) - self.assertTrue('Content-Type' in request.headers) - self.assertEqual(request.headers['Content-Type'], content_type) - - def assertResponse404(self, request): - self.assertHttpResponseValid(request, 404, 'text/html') - - def assertResponseHtml(self, request, status_code=200): - self.assertHttpResponseValid(request, status_code, 'text/html') - - def assertResponsePlain(self, request, status_code=200): - self.assertHttpResponseValid(request, status_code, 'text/plain') - - -class TestFileServing(LwanTest): - def test_mime_type_is_correct(self): - table = ( - ('/', 'text/html'), - ('/icons/back.png', 'image/png'), - ('/icons', 'text/plain'), - ('/icons/', 'text/html'), - ('/zero', 'application/octet-stream') - ) - - for path, expected_mime in table: - r = requests.head('http://127.0.0.1:8080%s' % path) - self.assertEqual(r.headers['content-type'], expected_mime) - - - def test_non_existent_file_yields_404(self): - r = requests.get('http://127.0.0.1:8080/icons/non-existent-file.png') - - self.assertResponse404(r) - - - def test_dot_dot_slash_yields_404(self): - r = requests.get('http://127.0.0.1:8080/etc/passwd') - - self.assertResponse404(r) - - - def test_slash_slash_slash_does_not_matter_200(self): - r = requests.get('http://127.0.0.1:8080//////////icons/file.png') - - self.assertHttpResponseValid(r, 200, 'image/png') - - - def test_slash_slash_slash_does_not_matter_404(self): - r = requests.get('http://127.0.0.1:8080//////////etc/passwd') - - self.assertResponse404(r) - - - def test_head_request_small_file(self): - r = requests.head('http://127.0.0.1:8080/100.html', - headers={'Accept-Encoding': 'foobar'}) - - self.assertResponseHtml(r) - - self.assertTrue('content-length' in r.headers) - self.assertEqual(r.headers['content-length'], '100') - - self.assertEqual(r.text, '') - - - def test_head_request_larger_file(self): - r = requests.head('http://127.0.0.1:8080/zero', - headers={'Accept-Encoding': 'foobar'}) - - self.assertHttpResponseValid(r, 200, 'application/octet-stream') - - self.assertTrue('content-length' in r.headers) - self.assertEqual(r.headers['content-length'], '32768') - - self.assertEqual(r.text, '') - - - def test_uncompressed_small_file(self): - r = requests.get('http://127.0.0.1:8080/100.html', - headers={'Accept-Encoding': 'foobar'}) - - self.assertResponseHtml(r) - - self.assertTrue('content-length' in r.headers) - self.assertEqual(r.headers['content-length'], '100') - - self.assertEqual(r.text, 'X' * 100) - - - def test_get_root(self): - r = requests.get('http://127.0.0.1:8080/') - - self.assertResponseHtml(r) - - self.assertTrue('It works!' in r.text) - - - def test_compressed_small_file(self): - encodings = ( - 'deflate', - ' deflate', - 'foo,bar,deflate', - 'foo, bar, deflate', - 'deflote' # This should fail, but won't in our current implementation - ) - - for encoding in encodings: - r = requests.get('http://127.0.0.1:8080/100.html', - headers={'Accept-Encoding': encoding}) - - self.assertResponseHtml(r) - - self.assertTrue('content-length' in r.headers) - self.assertLess(int(r.headers['content-length']), 100) - - self.assertTrue('content-encoding' in r.headers) - self.assertEqual(r.headers['content-encoding'], 'deflate') - - self.assertEqual(r.text, 'X' * 100) - - - def test_get_larger_file(self): - r = requests.get('http://127.0.0.1:8080/zero', - headers={'Accept-Encoding': 'foobar'}) - - self.assertHttpResponseValid(r, 200, 'application/octet-stream') - - self.assertTrue('content-length' in r.headers) - self.assertEqual(r.headers['content-length'], '32768') - - self.assertEqual(r.text, '\0' * 32768) - - - def test_directory_listing(self): - r = requests.get('http://127.0.0.1:8080/icons', - headers={'Accept-Encoding': 'foobar'}) - - self.assertResponseHtml(r) - - self.assertTrue('

Index of /icons

' in r.text) - - def assertHasImage(name): - imgtag = "%s.png" % (name, name) - self.assertTrue(imgtag in r.text) - - assertHasImage('back') - assertHasImage('file') - assertHasImage('folder') - - self.assertTrue('' in r.text) - - - def test_has_lwan_server_header(self): - r = requests.get('http://127.0.0.1:8080/100.html') - self.assertTrue('server' in r.headers) - self.assertEqual(r.headers['server'], 'lwan') - - - def test_directory_without_trailing_slash_redirects(self): - r = requests.get('http://127.0.0.1:8080/icons', allow_redirects=False) - - self.assertResponsePlain(r, 301) - self.assertTrue('location' in r.headers) - self.assertEqual(r.headers['location'], '/icons/') - -class TestRewrite(LwanTest): - def test_pattern_redirect_to(self): - r = requests.get('http://127.0.0.1:8080/pattern/foo/1234x5678', allow_redirects=False) - - self.assertResponseHtml(r, 301) - self.assertTrue('location' in r.headers) - self.assertEqual(r.headers['location'], '/hello?name=prexmiddle5678othermiddle1234post') - - def test_pattern_rewrite_as(self): - r = requests.get('http://127.0.0.1:8080/pattern/bar/42/test', allow_redirects=False) - - self.assertResponsePlain(r, 200) - self.assertFalse('location' in r.headers) - self.assertEqual(r.text, 'Hello, rewritten42!') - - -class SocketTest(LwanTest): - def connect(self, host='127.0.0.1', port=8080): - def _connect(host, port): - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((host, port)) - except socket.error: - return None - return sock - - sock = _connect(host, port) - self.assertNotEqual(sock, None) - return sock - -class TestMalformedRequests(SocketTest): - def assertHttpCode(self, sock, code): - contents = sock.recv(128) - - self.assertRegexpMatches(contents, r'^HTTP/1\.[01] ' + str(code) + r' ') - - - def test_random_flood(self): - with open('/dev/urandom', 'rb') as urandom: - for step in range(10): - - buffer = b'' - while len(buffer) < 8192: - buffer += urandom.read(8192 - len(buffer)) - - sock = self.connect() - sock.send(buffer) - self.assertHttpCode(sock, 413) - - - def test_cat_sleeping_on_keyboard(self): - sock = self.connect() - sock.send('asldkfjg238045tgqwdcjv1li 2u4ftw dfjkb12345t\r\n\r\n') - - self.assertHttpCode(sock, 405) - - - def test_no_http_version_fails(self): - sock = self.connect() - sock.send('GET /\r\n\r\n') - - self.assertHttpCode(sock, 400) - - - def test_proxy_get_fails(self): - sock = self.connect() - sock.send('GET http://example.com HTTP/1.0\r\n\r\n') - - self.assertHttpCode(sock, 400) - - - def test_get_not_http(self): - sock = self.connect() - sock.send('GET / FROG/1.0\r\n\r\n') - - self.assertHttpCode(sock, 400) - - - def test_get_http_not_1_x(self): - sock = self.connect() - sock.send('GET / HTTP/2.0\r\n\r\n') - - self.assertHttpCode(sock, 400) - - - def test_request_too_large(self): - r = requests.get('http://127.0.0.1:8080/' + 'X' * 100000) - - self.assertResponseHtml(r, 413) - - -class TestLua(LwanTest): - def test_hello(self): - r = requests.get('http://localhost:8080/lua/hello') - self.assertResponseHtml(r) - self.assertEqual(r.text, 'Hello, World!') - - def test_hello_param(self): - r = requests.get('http://localhost:8080/lua/hello?name=foo') - self.assertResponseHtml(r) - self.assertEqual(r.text, 'Hello, foo!') - - def test_cookies(self): - cookies_to_send = { - 'FOO': 'BAR' - } - cookies_to_receive = { - 'SESSION_ID': '1234', - 'LANG': 'pt_BR', - } - r = requests.get('http://localhost:8080/lua/cookie', cookies=cookies_to_send) - self.assertResponseHtml(r) - self.assertEqual(r.text, 'Cookie FOO has value: BAR') - - for cookie, value in cookies_to_receive.items(): - self.assertTrue(cookie in r.cookies) - self.assertEqual(r.cookies[cookie], value) - - -class TestHelloWorld(LwanTest): - def test_cookies(self): - c = { - 'SOMECOOKIE': '1c330301-89e4-408a-bf6c-ce107efe8a27', - 'OTHERCOOKIE': 'some cookie value', - 'foo': 'bar' - } - r = requests.get('http://127.0.0.1:8080/hello?dump_vars=1', cookies=c) - - self.assertResponsePlain(r) - - self.assertTrue('\n\nCookies\n' in r.text) - for k, v in c.items(): - self.assertTrue('Key = "%s"; Value = "%s"\n' % (k, v) in r.text) - - def test_head_request_hello(self): - r = requests.head('http://127.0.0.1:8080/hello', - headers={'Accept-Encoding': 'foobar'}) - - self.assertResponsePlain(r) - - self.assertTrue('content-length' in r.headers) - self.assertEqual(int(r.headers['content-length']), len('Hello, world!')) - - self.assertEqual(r.text, '') - - - def test_has_custom_header(self): - r = requests.get('http://127.0.0.1:8080/hello') - - self.assertTrue('x-the-answer-to-the-universal-question' in r.headers) - self.assertEqual(r.headers['x-the-answer-to-the-universal-question'], '42') - - - def test_no_param(self): - r = requests.get('http://127.0.0.1:8080/hello') - - self.assertResponsePlain(r) - - self.assertTrue('content-length' in r.headers) - self.assertEqual(int(r.headers['content-length']), len('Hello, world!')) - - self.assertEqual(r.text, 'Hello, world!') - - - def test_with_param(self): - r = requests.get('http://127.0.0.1:8080/hello?name=testsuite') - - self.assertResponsePlain(r) - - self.assertTrue('content-length' in r.headers) - self.assertEqual(int(r.headers['content-length']), - len('Hello, testsuite!')) - - self.assertEqual(r.text, 'Hello, testsuite!') - - - def test_with_param_and_fragment(self): - r = requests.get('http://127.0.0.1:8080/hello?name=testsuite#fragment') - - self.assertResponsePlain(r) - - self.assertTrue('content-length' in r.headers) - self.assertEqual(int(r.headers['content-length']), - len('Hello, testsuite!')) - - self.assertEqual(r.text, 'Hello, testsuite!') - - - def test_post_request(self): - data = { - 'answer': 'fourty-two', - 'foo': 'bar' - } - r = requests.post('http://127.0.0.1:8080/hello?dump_vars=1', data=data) - - self.assertResponsePlain(r) - - self.assertTrue('POST data' in r.text) - for k, v in data.items(): - self.assertTrue('Key = "%s"; Value = "%s"\n' % (k, v) in r.text) - - -class TestCache(LwanTest): - def mmaps(self, f): - f = f + '\n' - return (l.endswith(f) for l in - file('/proc/%d/maps' % self.lwan.pid)) - - - def count_mmaps(self, f): - return sum(self.mmaps(f)) - - - def is_mmapped(self, f): - return any(self.mmaps(f)) - - - def wait_munmap(self, f, timeout=20.0): - while self.is_mmapped(f) and timeout >= 0: - time.sleep(0.1) - timeout -= 0.1 - - - def test_cache_munmaps_conn_close(self): - r = requests.get('http://127.0.0.1:8080/100.html') - - self.assertTrue(self.is_mmapped('/100.html')) - self.wait_munmap('/100.html') - self.assertFalse(self.is_mmapped('/100.html')) - - - def test_cache_munmaps_conn_keep_alive(self): - s = requests.Session() - r = s.get('http://127.0.0.1:8080/100.html') - - self.assertTrue(self.is_mmapped('/100.html')) - self.wait_munmap('/100.html') - self.assertFalse(self.is_mmapped('/100.html')) - - - def test_cache_does_not_mmap_large_files(self): - r = requests.get('http://127.0.0.1:8080/zero') - self.assertFalse(self.is_mmapped('/zero')) - - - def test_cache_mmaps_once_conn_keep_alive(self): - s = requests.Session() - - for request in range(5): - r = s.get('http://127.0.0.1:8080/100.html') - self.assertEqual(self.count_mmaps('/100.html'), 1) - - - def test_cache_mmaps_once_conn_close(self): - for request in range(5): - requests.get('http://127.0.0.1:8080/100.html') - self.assertEqual(self.count_mmaps('/100.html'), 1) - - - def test_cache_mmaps_once_even_after_timeout(self): - for request in range(5): - requests.get('http://127.0.0.1:8080/100.html') - self.assertEqual(self.count_mmaps('/100.html'), 1) - - time.sleep(10) - - requests.get('http://127.0.0.1:8080/100.html') - self.assertEqual(self.count_mmaps('/100.html'), 1) - -class TestPipelinedRequests(SocketTest): - def test_pipelined_requests(self): - response_separator = re.compile('\r\n\r\n') - names = ['name%04x' % x for x in range(16)] - reqs = '\r\n\r\n'.join('''GET /hello?name=%s HTTP/1.1\r -Host: localhost\r -Connection: keep-alive\r -Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7''' % name for name in names) - reqs += '\r\n\r\n' - - sock = self.connect() - sock.send(reqs) - - responses = '' - while len(response_separator.findall(responses)) != 16: - response = sock.recv(32) - if response: - responses += response - else: - break - - for name in names: - s = 'Hello, %s!' % name - self.assertTrue(s in responses) - responses = responses.replace(s, '') - -if __name__ == '__main__': - unittest.main() diff --git a/wwwroot/icons/README.TXT b/wwwroot/icons/README.TXT new file mode 100644 index 000000000..04b4a1daf --- /dev/null +++ b/wwwroot/icons/README.TXT @@ -0,0 +1,15 @@ +Public Domain Icons +------------------- + +These icons were originally made for Mosaic for X and have been included in +the NCSA httpd and Apache server distributions in the past. They are in the +public domain and may be freely included in any application. The originals +were done by Kevin Hughes (kevinh@kevcom.com). Andy Polyakov tuned the icon +colors and added few new images. + +If you'd like to contribute additions to this set, contact the httpd +documentation project http://httpd.apache.org/docs-project/. + +Almost all of these icons are 20x22 pixels in size. There are alternative +icons in the "small" directory that are 16x16 in size, provided by Mike +Brown (mike@hyperreal.org). diff --git a/wwwroot/icons/back.gif b/wwwroot/icons/back.gif new file mode 100644 index 000000000..426836604 Binary files /dev/null and b/wwwroot/icons/back.gif differ diff --git a/wwwroot/icons/back.png b/wwwroot/icons/back.png deleted file mode 100644 index 7f68351b8..000000000 Binary files a/wwwroot/icons/back.png and /dev/null differ diff --git a/wwwroot/icons/file.gif b/wwwroot/icons/file.gif new file mode 100644 index 000000000..f8da6ff92 Binary files /dev/null and b/wwwroot/icons/file.gif differ diff --git a/wwwroot/icons/file.png b/wwwroot/icons/file.png deleted file mode 100644 index 94087e1fc..000000000 Binary files a/wwwroot/icons/file.png and /dev/null differ diff --git a/wwwroot/icons/folder.gif b/wwwroot/icons/folder.gif new file mode 100644 index 000000000..7b37b0991 Binary files /dev/null and b/wwwroot/icons/folder.gif differ diff --git a/wwwroot/icons/folder.png b/wwwroot/icons/folder.png deleted file mode 100644 index fd4b90d4e..000000000 Binary files a/wwwroot/icons/folder.png and /dev/null differ