diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..b02159e --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,39 @@ +# iOS CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/ios-migrating-from-1-2/ for more details +# +version: 2 +jobs: + build: + + # Specify the Xcode version to use + macos: + xcode: "8.3.3" + + steps: + - checkout + + # Install CocoaPods + - run: + name: Install CocoaPods + command: pod install + + # Build the app and run tests + - run: + name: Build and run tests + command: fastlane scan + environment: + SCAN_DEVICE: iPhone 6 + SCAN_SCHEME: WebTests + + # Collect XML test results data to show in the UI, + # and save the same XML files under test-results folder + # in the Artifacts tab + - store_test_results: + path: test_output/report.xml + - store_artifacts: + path: /tmp/test-results + destination: scan-test-results + - store_artifacts: + path: ~/Library/Logs/scan + destination: scan-logs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c2d1a4c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +build/ +DIST/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..1a118f4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,142 @@ +on: + push: + branches: + - master + tags: + - '*' + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + QT_VERSION: "6.5" + +jobs: + + #======================================================================== + debian12_qhttpserver: + name: "Build the package for Debian 12" + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@v1.2.0 + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: false + # all of these default to true, but feel free to set to + # "false" if necessary for your workflow + android: true + dotnet: true + haskell: true + large-packages: false + swap-storage: true + - name: Build image + run: docker build -f ./Dockerfile-debian12 -t aymara/qhttpserver-debian12:build . + - name: Extract package and version number from docker image + id: extract + shell: bash + run: | + dockerImage="aymara/qhttpserver-debian12:build" + packageDir="/usr/share/apps/qhttpserver/packages/" + cicd/extract_package.sh $dockerImage $packageDir $GITHUB_OUTPUT + - name: Upload nightly build package + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + shell: bash + run: | + tagName=${{ steps.extract.outputs.version }}-nightly + gh release create --prerelease ${tagName} || /bin/true + gh release upload ${tagName} --clobber ./${{ steps.extract.outputs.filename }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload release package + uses: softprops/action-gh-release@v1 + if: ${{ startsWith(github.ref, 'refs/tags/') }} + with: + name: ${{ steps.extract.outputs.version }} + files: | + ./${{ steps.extract.outputs.filename }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + #======================================================================== + ubuntu22_04_qhttpserver: + name: "Build the package for Ubuntu 22.04" + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@v1.2.0 + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: false + # all of these default to true, but feel free to set to + # "false" if necessary for your workflow + android: true + dotnet: true + haskell: true + large-packages: false + swap-storage: true + - name: Build image + run: docker build -f ./Dockerfile-ubuntu22.04 -t aymara/qhttpserver-ubuntu22.04:build . + - name: Extract package and version number from docker image + id: extract + shell: bash + run: | + dockerImage="aymara/qhttpserver-ubuntu22.04:build" + packageDir="/usr/share/apps/qhttpserver/packages/" + cicd/extract_package.sh $dockerImage $packageDir $GITHUB_OUTPUT + - name: Upload nightly build package + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + shell: bash + run: | + tagName=${{ steps.extract.outputs.version }}-nightly + gh release create --prerelease ${tagName} || /bin/true + gh release upload ${tagName} --clobber ./${{ steps.extract.outputs.filename }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload release package + uses: softprops/action-gh-release@v1 + if: ${{ startsWith(github.ref, 'refs/tags/') }} + with: + name: ${{ steps.extract.outputs.version }} + files: | + ./${{ steps.extract.outputs.filename }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + #======================================================================== + manylinux_2_28_qhttpserver: + name: "Build the package for Manylinux 2.28" + runs-on: ubuntu-latest + steps: + - + name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.AYMARA_DOCKERHUB_USERNAME }} + password: ${{ secrets.AYMARA_DOCKERHUB_TOKEN }} + + - name: Checkout code + uses: actions/checkout@v3 + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@v1.2.0 + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: false + # all of these default to true, but feel free to set to + # "false" if necessary for your workflow + android: true + dotnet: true + haskell: true + large-packages: false + swap-storage: true + - name: Build image + run: docker build -f ./Dockerfile-manylinux2.28 -t aymara/qhttpserver-manylinux2.28-qt${QT_VERSION}:latest . + - name: "Push image" + run: docker push aymara/qhttpserver-manylinux2.28-qt${QT_VERSION}:latest diff --git a/.gitignore b/.gitignore index e7540b8..2bb01ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ +*~ +*.kdev4 + + # Folders -build +DIST/ +build/ +pack/ lib +.kdev4 # Generated Makefile diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c2c9bdf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,69 @@ +sudo: required +language: cpp +cache: ccache +branches: + except: + - /^\d\d\d\d\d\d\d\d\d\d\d\d\d\d-[\da-f]+$/ + +services: +- docker + +jobs: + include: + - stage: test + script: docker build --tag ubuntu18.04 --build-arg JOB_NUMBER=$TRAVIS_JOB_NUMBER -f ./Dockerfile-ubuntu18.04 . && docker run -t -d ubuntu18.04 && docker cp $(docker ps -n 1 -q):/usr/share/apps/qhttpserver/packages/ $HOME && ls $HOME/packages/ + before_deploy: + git config --local user.name "Gael de Chalendar" + git config --local user.email "kleag@free.fr" + git tag "$(git log --date=iso -1 | grep Date | sed -e 's/Date:\ *//' -e 's/\+.*//' -e 's/[- :]//g')-$(git log --format=%h --abbrev=8 -1)" || true + deploy: + provider: releases + api_key: + secure: F16J7J9a+VNEBwSiAksWxnF7c1M8Zg0+Tg8WGRRYy2p5aqR83Gj8/3M+SIPNw/Dzxj8+Sid1QkIZ42jISzoDKO3bJ0cX7ymqGy3bwqMMsue6l2TAa/heJKpaGroyLtjZRgD2pGtHigC8W1BOEz+5KhVBwWAYDF92TpluvyklGoA= + file_glob: true + file: $HOME/packages/*.deb + skip_cleanup: true + on: + repo: aymara/qhttpserver + tags: false + branches: + except: + - /^\d\d\d\d\d\d\d\d\d\d\d\d\d\d-[\da-f]+$/ + - + script: docker build --tag ubuntu16.04 --build-arg JOB_NUMBER=$TRAVIS_JOB_NUMBER -f ./Dockerfile-ubuntu16.04 . && docker run -t -d ubuntu16.04 && docker cp $(docker ps -n 1 -q):/usr/share/apps/qhttpserver/packages/ $HOME && ls $HOME/packages/ + before_deploy: + git config --local user.name "Gael de Chalendar" + git config --local user.email "kleag@free.fr" + git tag "$(git log --date=iso -1 | grep Date | sed -e 's/Date:\ *//' -e 's/\+.*//' -e 's/[- :]//g')-$(git log --format=%h --abbrev=8 -1)" || true + deploy: + provider: releases + api_key: + secure: F16J7J9a+VNEBwSiAksWxnF7c1M8Zg0+Tg8WGRRYy2p5aqR83Gj8/3M+SIPNw/Dzxj8+Sid1QkIZ42jISzoDKO3bJ0cX7ymqGy3bwqMMsue6l2TAa/heJKpaGroyLtjZRgD2pGtHigC8W1BOEz+5KhVBwWAYDF92TpluvyklGoA= + file_glob: true + file: $HOME/packages/*.deb + skip_cleanup: true + on: + repo: aymara/qhttpserver + tags: false + branches: + except: + - /^\d\d\d\d\d\d\d\d\d\d\d\d\d\d-[\da-f]+$/ + - + script: docker build --tag debian9.4 --build-arg JOB_NUMBER=$TRAVIS_JOB_NUMBER -f ./Dockerfile-debian9.4 . && docker run -t -d debian9.4 && docker cp $(docker ps -n 1 -q):/usr/share/apps/qhttpserver/packages/ $HOME && ls $HOME/packages/ + before_deploy: + git config --local user.name "Gael de Chalendar" + git config --local user.email "kleag@free.fr" + git tag "$(git log --date=iso -1 | grep Date | sed -e 's/Date:\ *//' -e 's/\+.*//' -e 's/[- :]//g')-$(git log --format=%h --abbrev=8 -1)" || true + deploy: + provider: releases + api_key: + secure: F16J7J9a+VNEBwSiAksWxnF7c1M8Zg0+Tg8WGRRYy2p5aqR83Gj8/3M+SIPNw/Dzxj8+Sid1QkIZ42jISzoDKO3bJ0cX7ymqGy3bwqMMsue6l2TAa/heJKpaGroyLtjZRgD2pGtHigC8W1BOEz+5KhVBwWAYDF92TpluvyklGoA= + file_glob: true + file: $HOME/packages/*.deb + skip_cleanup: true + on: + repo: aymara/qhttpserver + tags: false + branches: + except: + - /^\d\d\d\d\d\d\d\d\d\d\d\d\d\d-[\da-f]+$/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7753e0a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,186 @@ +project(qhttpserver) +cmake_minimum_required(VERSION 2.8) + +if (NOT (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")) + if ("x${CMAKE_BUILD_TYPE}" STREQUAL "xRelease") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -mtune=generic -march=x86-64") + endif() +endif() + +enable_testing() + +include(SystemSpecificInformations.cmake) + +message("System name is ${CMAKE_SYSTEM_NAME}") +if (NOT (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")) + message("Linux flags") + + include(CheckCXXCompilerFlag) + + CHECK_CXX_COMPILER_FLAG(-std=c++0x HAVE_STDCPP0X) + CHECK_CXX_COMPILER_FLAG(-std=c++11 HAVE_STDCPP11) + CHECK_CXX_COMPILER_FLAG(-std=c++14 HAVE_STDCPP14) + CHECK_CXX_COMPILER_FLAG(-std=c++17 HAVE_STDCPP17) + CHECK_CXX_COMPILER_FLAG(-fsanitize=undefined HAVE_SANITIZE_UNDEFINED) + CHECK_CXX_COMPILER_FLAG(-fno-omit-frame-pointer HAVE_NO_OMIT_FRAME_POINTER) + if (HAVE_NO_OMIT_FRAME_POINTER) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer") + endif() + + + if (HAVE_STDCPP17) + message("C++17 supported") + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) + elseif (HAVE_STDCPP14) + message("C++14 supported") + set(CMAKE_CXX_STANDARD 14) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) + elseif (HAVE_STDCPP11) + message("C++11 supported") + set(CMAKE_CXX_FLAGS "-std=c++11 -DBOOST_NO_HASH ${CMAKE_CXX_FLAGS}") + CHECK_CXX_COMPILER_FLAG(-Wsuggest-override HAVE_SUGGEST_OVERRIDE) + if (HAVE_SUGGEST_OVERRIDE) +# set(CMAKE_CXX_FLAGS "-Wsuggest-override -Werror=suggest-override ${CMAKE_CXX_FLAGS}") + set(CMAKE_CXX_FLAGS "-Wsuggest-override ${CMAKE_CXX_FLAGS}") + endif() + elseif (HAVE_STDCPP0X) + message("C++0x supported") + set(CMAKE_CXX_FLAGS "-std=c++0x -DBOOST_NO_HASH ${CMAKE_CXX_FLAGS}") + else () + message("C++0x NOT supported") + set(CMAKE_CXX_FLAGS "-DNO_STDCPP0X ${CMAKE_CXX_FLAGS}") + endif () + set(CMAKE_CXX_FLAGS "-W -Wall ${CMAKE_CXX_FLAGS}") + + set(LIB_INSTALL_DIR "lib") + +else (NOT (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")) + message("Windows flags") + add_definitions(-D WIN32) + + # By default, do not warn when built on machines using only VS Express: + IF(NOT DEFINED CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_NO_WARNINGS) + SET(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_NO_WARNINGS ON) + ENDIF(NOT DEFINED CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_NO_WARNINGS) + +# set(CMAKE_C_FLAGS "/Zc:wchar_t- /EHsc /GR ${CMAKE_C_FLAGS}") +# set(CMAKE_CXX_FLAGS "/Zc:wchar_t- /EHsc /GR /W4 /MP /FAu ${CMAKE_CXX_FLAGS}") + + set(LIB_INSTALL_DIR "bin") + + install(FILES ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} + DESTINATION bin) + +endif (NOT (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")) + + +# Find*.cmake will be searched in the following dirs if not found in system dirs +set(CMAKE_MODULE_PATH + "${CMAKE_SOURCE_DIR}/cmake/Modules" # for those available in this project + "/usr/share/cmake/Modules" # for those available on the system +) + +# find_path and find_library will search in these dirs too +set(CMAKE_PREFIX_PATH + "${CMAKE_PREFIX_PATH}" + "${CMAKE_INSTALL_PREFIX}" +) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -lpthread") + +# It is necessary to define Qt6_INSTALL_DIR in your environment. +set(CMAKE_PREFIX_PATH + "$ENV{Qt6_INSTALL_DIR}" + "${CMAKE_PREFIX_PATH}" +) + + +add_definitions(-DQT_NO_KEYWORDS) +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0) +if (NOT (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -DQT_DEPRECATED_WARNINGS") +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_COMPILING_QSTRING_COMPAT_CPP -D_SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS") +endif() + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +find_package(Qt6 REQUIRED COMPONENTS Core Network) +set(Qt6_LIBRARIES Qt6::Core Qt6::Network ) +message("Found Qt6 ${Qt6Core_VERSION}") + +message("\n\n\n{Qt6_LIBRARIES}=${Qt6_LIBRARIES} ") + +if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + get_target_property(QtCore_location Qt6::Core LOCATION) + get_target_property(QtNetwork_location Qt6::Network LOCATION) + install(FILES ${QtCore_location} + ${QtNetwork_location} + DESTINATION ${LIB_INSTALL_DIR}) +endif () + + +if(NOT DEFINED VERSION_RELEASE) + set(VERSION_RELEASE "1") +endif() +configure_file(QHttpServerConfig-src.cmake QHttpServerConfig.cmake @ONLY) +INCLUDE(${CMAKE_CURRENT_BINARY_DIR}/QHttpServerConfig.cmake) + + +add_subdirectory(src) +add_subdirectory(examples) + +########### documentation ############### + +# add a target to generate API documentation with Doxygen +#find_package(Doxygen) +#if(DOXYGEN_FOUND) +#configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY) +#add_custom_target(doc +#${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile +#WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +#COMMENT "Generating API documentation with Doxygen" VERBATIM +#) +#endif(DOXYGEN_FOUND) + +########### packaging ############### +# Specify additional runtime libraries that may not be detected. After +# inclusion any detected libraries will be appended to this. +set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS + ${QtCore_location} + ${QtNetwork_location} +) + +set(CMAKE_INSTALL_DEBUG_LIBRARIES TRUE) +set(MAKE_INSTALL_UCRT_LIBRARIES TRUE) +INCLUDE(InstallRequiredSystemLibraries) + + +set(CPACK_MONOLITHIC_INSTALL 1) +SET(CPACK_PACKAGE_NAME "qhttpserver") +SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A Qt HTTP server library") +SET(CPACK_PACKAGE_VENDOR "Nikhil Marathe ") +SET(CPACK_PACKAGE_CONTACT "Gaël de Chalendar ") +SET(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README.md") +SET(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") +SET(CPACK_PACKAGE_VERSION_MAJOR "${QHTTPSERVER_VERSION_MAJOR}") +SET(CPACK_PACKAGE_VERSION_MINOR "${QHTTPSERVER_VERSION_MINOR}") +SET(CPACK_PACKAGE_VERSION_PATCH "${QHTTPSERVER_VERSION_RELEASE}") +SET(CPACK_PACKAGE_VERSION "${QHTTPSERVER_VERSION}") +SET(CPACK_PACKAGE_INSTALL_DIRECTORY "qhttpserver-${QHTTPSERVER_VERSION}") +#SET(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64") +#SET(CPACK_NSIS_MODIFY_PATH "ON") +#SET(CPACK_STRIP_FILES "") +#SET(CPACK_SOURCE_STRIP_FILES "") +#SET(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) +#SET(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS OFF) +SET(CPACK_GENERATOR ${SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR}) +# set(CPACK_RPM_PACKAGE_REQUIRES "qt5 >= 5.2") +# SET(CPACK_DEBIAN_PACKAGE_DEPENDS "") + +SET(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}") + +INCLUDE(CPack) diff --git a/Dockerfile-debian11 b/Dockerfile-debian11 new file mode 100644 index 0000000..9269717 --- /dev/null +++ b/Dockerfile-debian11 @@ -0,0 +1,25 @@ +FROM debian:11 + +ARG TRAVIS_JOB_NUMBER +ENV DEBIAN_FRONTEND=noninteractive + +# Setup +RUN apt-get update -y -qq +RUN apt-get install -y apt-utils wget bzip2 git gcc g++ cmake cmake-data ninja-build qtbase5-dev-tools qtdeclarative5-dev packaging-dev -qq +RUN mkdir -p /src/ +RUN git clone https://github.com/aymara/qhttpserver /src/qhttpserver + +RUN mkdir -p /src/qhttpserver/build + +# Build +WORKDIR /src/qhttpserver/build +RUN cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release .. +RUN ninja all && ninja package +RUN echo "export packagefile=`find /src/qhttpserver/build/ -maxdepth 1 -name *.deb`" > /envfile +RUN echo "export fullname=\$(basename -- \$packagefile)" >> /envfile +RUN echo "export extension=\${fullname##*.}" >> /envfile +RUN echo "export filename=\${fullname%.*}" >> /envfile +RUN . /envfile && install -D ${packagefile} "/usr/share/apps/qhttpserver/packages/${filename}-debian11.deb" + +## install github-release to be able to deploy packages +#RUN wget https://github.com/aktau/github-release/releases/download/v0.7.2/linux-amd64-github-release.tar.bz2 && tar xjf linux-amd64-github-release.tar.bz2 && cp bin/linux/amd64/github-release /usr/bin diff --git a/Dockerfile-debian12 b/Dockerfile-debian12 new file mode 100644 index 0000000..9a50b6e --- /dev/null +++ b/Dockerfile-debian12 @@ -0,0 +1,24 @@ +FROM debian:12 + +ARG TRAVIS_JOB_NUMBER +ENV DEBIAN_FRONTEND=noninteractive + +# Setup +RUN apt-get update -y -qq && apt-get install -y apt-utils wget bzip2 git gcc g++ cmake cmake-data ninja-build qt6-base-dev-tools qt6-declarative-dev packaging-dev -qq +RUN mkdir -p /src/ +COPY . /src/qhttpserver + +RUN mkdir -p /src/qhttpserver/build + +# Build +WORKDIR /src/qhttpserver/build +RUN cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release .. +RUN ninja all && ninja package +RUN echo "export packagefile=`find /src/qhttpserver/build/ -maxdepth 1 -name *.deb`" > /envfile +RUN echo "export fullname=\$(basename -- \$packagefile)" >> /envfile +RUN echo "export extension=\${fullname##*.}" >> /envfile +RUN echo "export filename=\${fullname%.*}" >> /envfile +RUN . /envfile && install -D ${packagefile} "/usr/share/apps/qhttpserver/packages/${filename}-debian12r.deb" + +## install github-release to be able to deploy packages +#RUN wget https://github.com/aktau/github-release/releases/download/v0.7.2/linux-amd64-github-release.tar.bz2 && tar xjf linux-amd64-github-release.tar.bz2 && cp bin/linux/amd64/github-release /usr/bin diff --git a/Dockerfile-debian9.4 b/Dockerfile-debian9.4 new file mode 100644 index 0000000..3af07b3 --- /dev/null +++ b/Dockerfile-debian9.4 @@ -0,0 +1,25 @@ +FROM debian:9.4 + +ARG TRAVIS_JOB_NUMBER +ENV DEBIAN_FRONTEND=noninteractive + +# Setup +RUN apt-get update -y -qq +RUN apt-get install -y apt-utils wget bzip2 git gcc g++ cmake cmake-data ninja-build qt5-default qtbase5-dev-tools qtdeclarative5-dev packaging-dev -qq +RUN mkdir -p /src/ +RUN git clone https://github.com/aymara/qhttpserver /src/qhttpserver + +RUN mkdir -p /src/qhttpserver/build + +# Build +WORKDIR /src/qhttpserver/build +RUN cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release .. +RUN ninja all && ninja test && ninja package +RUN packagefile=$(find /src/qhttpserver/build/*.deb) \ + fullname=$(basename -- "$packagefile") \ + extension="${fullname##*.}" \ + filename="${fullname%.*}" \ + install -D $packagefile /usr/share/apps/qhttpserver/packages/$filename-debian9.4.deb + +## install github-release to be able to deploy packages +#RUN wget https://github.com/aktau/github-release/releases/download/v0.7.2/linux-amd64-github-release.tar.bz2 && tar xjf linux-amd64-github-release.tar.bz2 && cp bin/linux/amd64/github-release /usr/bin diff --git a/Dockerfile-manylinux2.28 b/Dockerfile-manylinux2.28 new file mode 100644 index 0000000..7b7c039 --- /dev/null +++ b/Dockerfile-manylinux2.28 @@ -0,0 +1,36 @@ +FROM aymara/manylinux_2_28_with_qt6.5:latest as aymara_manylinux_2_28_with_qt + +ARG MANYLINUX_TAG +FROM quay.io/pypa/manylinux_2_28_x86_64:2022-10-25-fbea779 + +COPY --from=aymara_manylinux_2_28_with_qt /opt /opt +COPY --from=aymara_manylinux_2_28_with_qt /usr/local /usr/local + +RUN yum install -y wget gcc-toolset-10.x86_64 ninja-build --nogpgcheck + +RUN install -d /src/qhttpserver +COPY . /src/qhttpserver +#COPY docs /src/qhttpserver +#COPY LICENSE /src/qhttpserver +#COPY examples /src/qhttpserver +#COPY QHttpServerConfig-src.cmake /src/qhttpserver +#COPY README.md /src/qhttpserver +#COPY src /src/qhttpserver +#COPY CMakeLists.txt /src/qhttpserver +#COPY http-parser /src/qhttpserver +#COPY SystemSpecificInformations.cmake /src/qhttpserver + +RUN install -d /src/qhttpserver/build + +# Build +WORKDIR /src/qhttpserver/build +RUN cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/opt/qhttpserver -DCMAKE_BUILD_TYPE=Release .. +RUN ninja all && ninja install +#RUN echo "export packagefile=`find /src/qhttpserver/build/ -maxdepth 1 -name *.tar.gz`" > /envfile +#RUN echo "export fullname=\$(basename -- \$packagefile)" >> /envfile +#RUN echo "export extension=\${fullname##*.}" >> /envfile +#RUN echo "export filename=\${fullname%.*}" >> /envfile +#RUN cat /envfile +#RUN source /envfile && install -D ${packagefile} "/usr/share/apps/qhttpserver/packages/${filename}-manylinux_2_28.deb" + + diff --git a/Dockerfile-ubuntu14.04 b/Dockerfile-ubuntu14.04 new file mode 100644 index 0000000..9a94b8a --- /dev/null +++ b/Dockerfile-ubuntu14.04 @@ -0,0 +1,23 @@ +FROM ubuntu:14.04 + +ARG BINTRAYKEY +ARG TRAVIS_JOB_NUMBER +ENV DEBIAN_FRONTEND=noninteractive + +# Setup +RUN apt-get update -y -qq +RUN apt-get install -y apt-utils git gcc g++ cmake cmake-data ninja-build qtbase5-dev-tools qtdeclarative5-dev packaging-dev -qq +RUN mkdir -p /src/ +RUN git clone https://github.com/aymara/qhttpserver /src/qhttpserver + +RUN mkdir -p /src/qhttpserver/build + +# Build +WORKDIR /src/qhttpserver/build +RUN cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release .. +RUN ninja all && ninja test && ninja package +RUN echo "export packagefile=`find /src/qhttpserver/build/ -maxdepth 1 -name *.deb`" > /envfile +RUN echo "export fullname=\$(basename -- \$packagefile)" >> /envfile +RUN echo "export extension=\${fullname##*.}" >> /envfile +RUN echo "export filename=\${fullname%.*}" >> /envfile +RUN . /envfile && install -D ${packagefile} "/usr/share/apps/qhttpserver/packages/${filename}-ubuntu14.04.deb" \ No newline at end of file diff --git a/Dockerfile-ubuntu16.04 b/Dockerfile-ubuntu16.04 new file mode 100644 index 0000000..7dded55 --- /dev/null +++ b/Dockerfile-ubuntu16.04 @@ -0,0 +1,26 @@ +FROM ubuntu:16.04 + +ARG TRAVIS_JOB_NUMBER +ENV DEBIAN_FRONTEND=noninteractive + +# Setup +RUN apt-get update -y -qq +RUN apt-get install -y apt-utils wget bzip2 git gcc g++ cmake cmake-data ninja-build qtbase5-dev-tools qtdeclarative5-dev packaging-dev -qq +RUN mkdir -p /src/ +RUN git clone https://github.com/aymara/qhttpserver /src/qhttpserver + +RUN mkdir -p /src/qhttpserver/build + +# Build +WORKDIR /src/qhttpserver/build +RUN cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release .. +RUN ninja all && ninja test && ninja package +RUN echo "export packagefile=`find /src/qhttpserver/build/ -maxdepth 1 -name *.deb`" > /envfile +RUN echo "export fullname=\$(basename -- \$packagefile)" >> /envfile +RUN echo "export extension=\${fullname##*.}" >> /envfile +RUN echo "export filename=\${fullname%.*}" >> /envfile +RUN . /envfile && install -D ${packagefile} "/usr/share/apps/qhttpserver/packages/${filename}-ubuntu16.04.deb" + +## install github-release to be able to deploy packages +#RUN wget https://github.com/aktau/github-release/releases/download/v0.7.2/linux-amd64-github-release.tar.bz2 && tar xjf linux-amd64-github-release.tar.bz2 && cp bin/linux/amd64/github-release /usr/bin + diff --git a/Dockerfile-ubuntu18.04 b/Dockerfile-ubuntu18.04 new file mode 100644 index 0000000..a2b4d46 --- /dev/null +++ b/Dockerfile-ubuntu18.04 @@ -0,0 +1,25 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Setup +RUN apt-get update -y -qq +RUN apt-get install -y apt-utils wget bzip2 git gcc g++ cmake cmake-data ninja-build qtbase5-dev-tools qtdeclarative5-dev- packaging-dev -qq +RUN mkdir -p /src/ +RUN git clone https://github.com/aymara/qhttpserver /src/qhttpserver + +RUN mkdir -p /src/qhttpserver/build + +# Build +WORKDIR /src/qhttpserver/build +RUN cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release .. +RUN ninja all && ninja test && ninja package +RUN echo "export packagefile=`find /src/qhttpserver/build/ -maxdepth 1 -name *.deb`" > /envfile +RUN echo "export fullname=\$(basename -- \$packagefile)" >> /envfile +RUN echo "export extension=\${fullname##*.}" >> /envfile +RUN echo "export filename=\${fullname%.*}" >> /envfile +RUN . /envfile && install -D ${packagefile} "/usr/share/apps/qhttpserver/packages/${filename}-ubuntu18.04.deb" + +## install github-release to be able to deploy packages +#RUN wget https://github.com/aktau/github-release/releases/download/v0.7.2/linux-amd64-github-release.tar.bz2 && tar xjf linux-amd64-github-release.tar.bz2 && cp bin/linux/amd64/github-release /usr/bin + diff --git a/Dockerfile-ubuntu22.04 b/Dockerfile-ubuntu22.04 new file mode 100644 index 0000000..5f22f15 --- /dev/null +++ b/Dockerfile-ubuntu22.04 @@ -0,0 +1,25 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Setup +RUN apt-get update -y -qq && apt-get install -y apt-utils wget bzip2 git gcc g++ cmake cmake-data ninja-build qt6-base-dev-tools qt6-declarative-dev packaging-dev -qq +RUN mkdir -p /src/ +COPY . /src/qhttpserver +#RUN git clone https://github.com/aymara/qhttpserver /src/qhttpserver + +RUN mkdir -p /src/qhttpserver/build + +# Build +WORKDIR /src/qhttpserver/build +RUN cmake -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release .. +RUN ninja all && ninja package +RUN echo "export packagefile=`find /src/qhttpserver/build/ -maxdepth 1 -name *.deb`" > /envfile +RUN echo "export fullname=\$(basename -- \$packagefile)" >> /envfile +RUN echo "export extension=\${fullname##*.}" >> /envfile +RUN echo "export filename=\${fullname%.*}" >> /envfile +RUN . /envfile && install -D ${packagefile} "/usr/share/apps/qhttpserver/packages/${filename}-ubuntu22.04.deb" + +## install github-release to be able to deploy packages +#RUN wget https://github.com/aktau/github-release/releases/download/v0.7.2/linux-amd64-github-release.tar.bz2 && tar xjf linux-amd64-github-release.tar.bz2 && cp bin/linux/amd64/github-release /usr/bin + diff --git a/QHttpServerConfig-src.cmake b/QHttpServerConfig-src.cmake new file mode 100644 index 0000000..bc2cf55 --- /dev/null +++ b/QHttpServerConfig-src.cmake @@ -0,0 +1,14 @@ + +SET(QHTTPSERVER_INCLUDE_DIR @CMAKE_INSTALL_PREFIX@/include) +SET(QHTTPSERVER_LIBRARY_DIRS @CMAKE_INSTALL_PREFIX@/${LIB_INSTALL_DIR}) + +set(QHTTPSERVER_VERSION_MAJOR "1") +set(QHTTPSERVER_VERSION_MINOR "1") +set(QHTTPSERVER_VERSION_RELEASE "@VERSION_RELEASE@") + +set(QHTTPSERVER_VERSION "${QHTTPSERVER_VERSION_MAJOR}.${QHTTPSERVER_VERSION_MINOR}.${QHTTPSERVER_VERSION_RELEASE}") + +set(QHTTPSERVER_GENERIC_LIB_VERSION ${QHTTPSERVER_VERSION}) +set(QHTTPSERVER_GENERIC_LIB_SOVERSION ${QHTTPSERVER_VERSION_MINOR}) + +message(STATUS "QHttpServer version ${QHTTPSERVER_VERSION} - QHTTPSERVER_GENERIC_LIB_VERSION ${QHTTPSERVER_GENERIC_LIB_VERSION} ") diff --git a/SystemSpecificInformations.cmake b/SystemSpecificInformations.cmake new file mode 100644 index 0000000..5e0be09 --- /dev/null +++ b/SystemSpecificInformations.cmake @@ -0,0 +1,140 @@ + +# define a set of string with may-be useful readable name +# this file is meant to be included in a CMakeLists.txt +# not as a standalone CMake script +set(SPECIFIC_COMPILER_NAME "") +set(SPECIFIC_SYSTEM_VERSION_NAME "") +set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "") + +if(WIN32) + set(SPECIFIC_SYSTEM_PREFERED_PACKAGE "NSIS") +# information taken from +# http://www.codeguru.com/cpp/w-p/system/systeminformation/article.php/c8973/ + # Win9x series + if(CMAKE_SYSTEM_VERSION MATCHES "4.0") + set(SPECIFIC_SYSTEM_VERSION_NAME "Win95") + endif(CMAKE_SYSTEM_VERSION MATCHES "4.0") + if(CMAKE_SYSTEM_VERSION MATCHES "4.10") + set(SPECIFIC_SYSTEM_VERSION_NAME "Win98") + endif(CMAKE_SYSTEM_VERSION MATCHES "4.10") + if(CMAKE_SYSTEM_VERSION MATCHES "4.90") + set(SPECIFIC_SYSTEM_VERSION_NAME "WinME") + endif(CMAKE_SYSTEM_VERSION MATCHES "4.90") + + # WinNTyyy series + if(CMAKE_SYSTEM_VERSION MATCHES "3.0") + set(SPECIFIC_SYSTEM_VERSION_NAME "WinNT351") + endif(CMAKE_SYSTEM_VERSION MATCHES "3.0") + if(CMAKE_SYSTEM_VERSION MATCHES "4.1") + set(SPECIFIC_SYSTEM_VERSION_NAME "WinNT4") + endif(CMAKE_SYSTEM_VERSION MATCHES "4.1") + + # Win2000/XP series + if(CMAKE_SYSTEM_VERSION MATCHES "5.0") + set(SPECIFIC_SYSTEM_VERSION_NAME "Win2000") + endif(CMAKE_SYSTEM_VERSION MATCHES "5.0") + if(CMAKE_SYSTEM_VERSION MATCHES "5.1") + set(SPECIFIC_SYSTEM_VERSION_NAME "WinXP") + endif(CMAKE_SYSTEM_VERSION MATCHES "5.1") + if(CMAKE_SYSTEM_VERSION MATCHES "5.2") + set(SPECIFIC_SYSTEM_VERSION_NAME "Win2003") + endif(CMAKE_SYSTEM_VERSION MATCHES "5.2") + + # WinVista/7 series + if(CMAKE_SYSTEM_VERSION MATCHES "6.0") + set(SPECIFIC_SYSTEM_VERSION_NAME "WinVISTA") + endif(CMAKE_SYSTEM_VERSION MATCHES "6.0") + if(CMAKE_SYSTEM_VERSION MATCHES "6.1") + set(SPECIFIC_SYSTEM_VERSION_NAME "Win7") + endif(CMAKE_SYSTEM_VERSION MATCHES "6.1") + + # Compilers + # taken from http://predef.sourceforge.net/precomp.html#sec34 + IF (MSVC) + if(MSVC_VERSION EQUAL 1200) + set(SPECIFIC_COMPILER_NAME "MSVC-6.0") + endif(MSVC_VERSION EQUAL 1200) + if(MSVC_VERSION EQUAL 1300) + set(SPECIFIC_COMPILER_NAME "MSVC-7.0") + endif(MSVC_VERSION EQUAL 1300) + if(MSVC_VERSION EQUAL 1310) + set(SPECIFIC_COMPILER_NAME "MSVC-7.1-2003") #Visual Studio 2003 + endif(MSVC_VERSION EQUAL 1310) + if(MSVC_VERSION EQUAL 1400) + set(SPECIFIC_COMPILER_NAME "MSVC-8.0-2005") #Visual Studio 2005 + endif(MSVC_VERSION EQUAL 1400) + if(MSVC_VERSION EQUAL 1500) + set(SPECIFIC_COMPILER_NAME "MSVC-9.0-2008") #Visual Studio 2008 + endif(MSVC_VERSION EQUAL 1500) + endif(MSVC) + IF (MINGW) + set(SPECIFIC_COMPILER_NAME "MinGW") + endif(MINGW) + IF (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") + set(SPECIFIC_SYSTEM_VERSION_NAME "${SPECIFIC_SYSTEM_VERSION_NAME}-x86_64") + endif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") +endif(WIN32) + +if(UNIX) + if(CMAKE_SYSTEM_NAME MATCHES "Linux") + set(SPECIFIC_SYSTEM_VERSION_NAME "${CMAKE_SYSTEM_NAME}") + set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "TGZ") + if(EXISTS "/etc/issue") + set(LINUX_NAME "") + file(READ "/etc/issue" LINUX_ISSUE) + # Fedora case + if(LINUX_ISSUE MATCHES "Fedora") + string(REGEX MATCH "release ([0-9]+)" FEDORA "${LINUX_ISSUE}") + set(LINUX_NAME "FC${CMAKE_MATCH_1}") + set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "RPM") + endif(LINUX_ISSUE MATCHES "Fedora") + # Ubuntu case + if(LINUX_ISSUE MATCHES "Ubuntu") + string(REGEX MATCH "buntu ([0-9]+\\.[0-9]+)" UBUNTU "${LINUX_ISSUE}") + set(LINUX_NAME "Ubuntu_${CMAKE_MATCH_1}") + set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "DEB") + endif(LINUX_ISSUE MATCHES "Ubuntu") + # Debian case + if(LINUX_ISSUE MATCHES "Debian") + string(REGEX MATCH "Debian .*ux ([a-zA-Z]*/?[a-zA-Z]*) .*" DEBIAN "${LINUX_ISSUE}") + set(LINUX_NAME "Debian_${CMAKE_MATCH_1}") + string(REPLACE "/" "_" LINUX_NAME ${LINUX_NAME}) + set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "DEB") + endif(LINUX_ISSUE MATCHES "Debian") + # Open SuSE case + if(LINUX_ISSUE MATCHES "SUSE") + string(REGEX MATCH "SUSE ([0-9]+\\.[0-9]+)" SUSE "${LINUX_ISSUE}") + set(LINUX_NAME "openSUSE_${CMAKE_MATCH_1}") + string(REPLACE "/" "_" LINUX_NAME ${LINUX_NAME}) + set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "RPM") + endif(LINUX_ISSUE MATCHES "SUSE") + # Mandriva Linux case + if(LINUX_ISSUE MATCHES "Mandriva") + string(REGEX MATCH "Mandriva Linux release ([0-9]+\\.[0-9]+)" MANDRIVA "${LINUX_ISSUE}") + set(LINUX_NAME "Mandriva_${CMAKE_MATCH_1}") + string(REPLACE "/" "_" LINUX_NAME ${LINUX_NAME}) + set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "RPM") + endif(LINUX_ISSUE MATCHES "Mandriva") + # Mageia case + if(LINUX_ISSUE MATCHES "Mageia") + string(REGEX MATCH "Mageia release ([0-9]+)" MAGEIA "${LINUX_ISSUE}") + set(LINUX_NAME "Mageia_${CMAKE_MATCH_1}") + string(REPLACE "/" "_" LINUX_NAME ${LINUX_NAME}) + set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "RPM") + endif(LINUX_ISSUE MATCHES "Mageia") + # KDE neon case + if(LINUX_ISSUE MATCHES "KDE neon") + string(REGEX MATCH "KDE neon ([0-9\\.]+)" NEON "${LINUX_ISSUE}") + set(LINUX_NAME "Neon_${CMAKE_MATCH_1}") + string(REPLACE "/" "_" LINUX_NAME ${LINUX_NAME}) + set(SPECIFIC_SYSTEM_PREFERED_CPACK_GENERATOR "DEB") + endif() + # TODO + if(LINUX_NAME) + set(SPECIFIC_SYSTEM_VERSION_NAME "${CMAKE_SYSTEM_NAME}-${LINUX_NAME}") + endif(LINUX_NAME) + endif(EXISTS "/etc/issue") + endif(CMAKE_SYSTEM_NAME MATCHES "Linux") + set(SPECIFIC_SYSTEM_VERSION_NAME "${SPECIFIC_SYSTEM_VERSION_NAME}-${CMAKE_SYSTEM_PROCESSOR}") + set(SPECIFIC_COMPILER_NAME "") +endif(UNIX) diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..dbac808 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,71 @@ +#init: + #- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + +branches: + except: + - /^\d\d\d\d\d\d\d\d\d\d\d\d\d\d-[\da-f]+$/ + + +environment: + QTDIR: 'C:\Qt\5.11.0\msvc2015_64' + PATH: '%QTDIR%\bin;c:\d\bin;%PATH%' + GITHUB_OAUTH_TOKEN: + secure: 01sh5OxKgkUJbTt/ldX/duLQk6iP28Iwet3reW8FK8f7Z3xiM2RLHQUwOzZye4qs + +configuration: Release + +os: Visual Studio 2015 + +install: + ############################################################################ + # All external dependencies are installed in C:\externals + ############################################################################ + - mkdir C:\externals + - cd C:\externals + + ############################################################################ + # Install Ninja + ############################################################################ + - appveyor DownloadFile https://github.com/ninja-build/ninja/releases/download/v1.8.2/ninja-win.zip -FileName ninja.zip + - 7z x ninja.zip -oC:\externals\ninja > nul + - set PATH=C:\externals\ninja;%PATH% + - ninja --version + + +build_script: + - ps: pushd c:/projects/qhttpserver + - ps: $env:VERSION_RELEASE="$(git log --date=iso -1 | grep Date | sed -e 's/Date:\ *//' -e 's/\+.*//' -e 's/[- :]//g')-$(git log --format=%h --abbrev=8 -1)" + - echo %VERSION_RELEASE% + - ps: popd + - mkdir c:\b + - cd c:\b + + - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64 + - cmake -G "Ninja" "-DCMAKE_PREFIX_PATH=%QTDIR%" "-DCMAKE_IGNORE_PATH=C:/msys64/usr/bin" "-DCMAKE_BUILD_TYPE=Release" "-DCMAKE_INSTALL_PREFIX=c:\d" "-DVERSION_RELEASE:STRING=%VERSION_RELEASE%" c:/projects/qhttpserver + - cmake --build . --config Release + - cmake --build . --target package --config Release + + +after_build: + - ps: pushd C:\b + - copy qhttpserver*.exe %APPVEYOR_BUILD_FOLDER% + - ps: popd + + +artifacts: + - path: qhttpserver*.exe + name: packages + +deploy: + description: '$(VERSION_RELEASE)' + provider: GitHub + tag: $(VERSION_RELEASE) + auth_token: + secure: 01sh5OxKgkUJbTt/ldX/duLQk6iP28Iwet3reW8FK8f7Z3xiM2RLHQUwOzZye4qs + artifact: packages + force_update: true + + +on_failure: + - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + diff --git a/cicd/extract_package.sh b/cicd/extract_package.sh new file mode 100755 index 0000000..1ee88e6 --- /dev/null +++ b/cicd/extract_package.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -u +set -e + +if [ "$#" -ne 3 ]; then + echo "Expect 3 arguments : " + exit 1; +fi + +dockerImage=$1 +packageDir=$2 +GITHUB_OUTPUT=$3 + +id=$(docker run -d "$dockerImage" sleep infinity) +echo -n "export fullname=$packageDir/" > ./envFile +docker exec $id ls $packageDir >> ./envFile +echo "export filename=\$(basename -- \$fullname)" >> ./envFile +echo "export trimmed=\$(basename \$filename .deb)" >> ./envFile +echo "tmp=\${trimmed#*-}" >> ./envFile +echo "export version=\${tmp%-*}" >> ./envFile +echo "filename=$(. envFile && echo $filename)" >> $GITHUB_OUTPUT +echo "version=$(. envFile && echo $version)" >> $GITHUB_OUTPUT +. ./envFile && echo "filename=$filename , version=$version" +echo "Extract file from docker image" +. ./envFile && docker cp $id:$fullname - > ./$filename +docker stop $id +docker rm -v $id +echo "Finaly, the package should be present in local folder : " +if [ -f "./$filename" ]; then echo "!! yes : ./$filename" ; else echo "!! no"; exit 1; fi + + + + diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..69584c6 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(helloworld) +# add_subdirectory(greeting) +#add_subdirectory(bodydata) diff --git a/examples/bodydata/CMakeLists.txt b/examples/bodydata/CMakeLists.txt new file mode 100644 index 0000000..58eb469 --- /dev/null +++ b/examples/bodydata/CMakeLists.txt @@ -0,0 +1,16 @@ +include_directories(BEFORE ${CMAKE_SOURCE_DIR}/src) + +add_definitions( -DQHTTPSERVER_EXPORT ) + +SET(œ_EXE_SRCS + bodydata.cpp + ) + +add_executable(bodydata_server ${bodydata_EXE_SRCS}) + +target_link_libraries(bodydata_server qhttpserver ${Qt6_LIBRARIES}) + +add_test( + NAME bodydata_Test + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test/bodydata.sh ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/examples/bodydata/bodydata.cpp b/examples/bodydata/bodydata.cpp index 1a2c7a1..e616b12 100644 --- a/examples/bodydata/bodydata.cpp +++ b/examples/bodydata/bodydata.cpp @@ -16,13 +16,13 @@ BodyData::BodyData() QHttpServer *server = new QHttpServer(this); connect(server, SIGNAL(newRequest(QHttpRequest*, QHttpResponse*)), this, SLOT(handleRequest(QHttpRequest*, QHttpResponse*))); - + server->listen(QHostAddress::Any, 8080); } void BodyData::handleRequest(QHttpRequest *req, QHttpResponse *resp) { - new Responder(req, resp); + Responder* resp = new Responder(req, resp); } /// Responder @@ -42,7 +42,7 @@ Responder::Responder(QHttpRequest *req, QHttpResponse *resp) resp->setHeader("Content-Type", "text/html"); resp->writeHead(200); - + QString name = exp.capturedTexts()[1]; QString bodyStart = tr("BodyData App

Hello %1!

").arg(name); resp->write(bodyStart.toUtf8()); diff --git a/examples/bodydata/bodydata.h b/examples/bodydata/bodydata.h index 8fed37d..8f84b4c 100644 --- a/examples/bodydata/bodydata.h +++ b/examples/bodydata/bodydata.h @@ -12,7 +12,7 @@ class BodyData : public QObject public: BodyData(); -private slots: +private Q_SLOTS: void handleRequest(QHttpRequest *req, QHttpResponse *resp); }; @@ -26,10 +26,10 @@ class Responder : public QObject Responder(QHttpRequest *req, QHttpResponse *resp); ~Responder(); -signals: +Q_SIGNALS: void done(); -private slots: +private Q_SLOTS: void accumulate(const QByteArray &data); void reply(); diff --git a/examples/bodydata/test/bodydata.sh b/examples/bodydata/test/bodydata.sh new file mode 100755 index 0000000..22bcfac --- /dev/null +++ b/examples/bodydata/test/bodydata.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +#------------------------------------------------------------------------------- +# to ensure the script actually runs inside its own folder +MY_PATH="`dirname \"$0\"`" # relative +MY_PATH="`( cd \"$MY_PATH\" && pwd )`" # absolutized and normalized +if [ -z "$MY_PATH" ] ; then + # Error. for some reason, the path is not accessible + # to the script (e.g. permissions re-evaled after suid) + exit 1 # Fail +fi + +BINARY_DIR=$1 + +coproc $BINARY_DIR/bodydata_server +SERVER_PID=$! + +expected="$MY_PATH/expected.html" +observed="output.html" +\rm -f $observed + +curl --request GET \ + --url http://localhost:8080/user/abc \ + -o $observed +if cmp -s "$expected" "$observed"; then + echo "All good" +else + echo "Unexpected response from server" + exit 1 +fi +\rm -f $observed + +kill $SERVER_PID + +exit 0 \ No newline at end of file diff --git a/examples/bodydata/test/expected.html b/examples/bodydata/test/expected.html new file mode 100644 index 0000000..4561256 --- /dev/null +++ b/examples/bodydata/test/expected.html @@ -0,0 +1 @@ +BodyData App

Hello abc!

\ No newline at end of file diff --git a/examples/greeting/CMakeLists.txt b/examples/greeting/CMakeLists.txt new file mode 100644 index 0000000..431fd8b --- /dev/null +++ b/examples/greeting/CMakeLists.txt @@ -0,0 +1,16 @@ +include_directories(BEFORE ${CMAKE_SOURCE_DIR}/src) + +add_definitions( -DQHTTPSERVER_EXPORT ) + +SET(greeting_EXE_SRCS + greeting.cpp + ) + +add_executable(greeting_server ${greeting_EXE_SRCS}) + +target_link_libraries(greeting_server qhttpserver ${Qt6_LIBRARIES}) + +add_test( + NAME greeting_Test + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test/greeting.sh ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/examples/greeting/greeting.h b/examples/greeting/greeting.h index ed26fbc..a2a000a 100644 --- a/examples/greeting/greeting.h +++ b/examples/greeting/greeting.h @@ -11,6 +11,6 @@ class Greeting : public QObject public: Greeting(); -private slots: +private Q_SLOTS: void handleRequest(QHttpRequest *req, QHttpResponse *resp); }; diff --git a/examples/greeting/test/bad_user_expected.txt b/examples/greeting/test/bad_user_expected.txt new file mode 100644 index 0000000..5068264 --- /dev/null +++ b/examples/greeting/test/bad_user_expected.txt @@ -0,0 +1 @@ +You aren't allowed here! \ No newline at end of file diff --git a/examples/greeting/test/expected.html b/examples/greeting/test/expected.html new file mode 100644 index 0000000..24b850a --- /dev/null +++ b/examples/greeting/test/expected.html @@ -0,0 +1 @@ +Greeting App

Hello abc!

\ No newline at end of file diff --git a/examples/greeting/test/greeting.sh b/examples/greeting/test/greeting.sh new file mode 100755 index 0000000..3119ba8 --- /dev/null +++ b/examples/greeting/test/greeting.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +#------------------------------------------------------------------------------- +# to ensure the script actually runs inside its own folder +MY_PATH="`dirname \"$0\"`" # relative +MY_PATH="`( cd \"$MY_PATH\" && pwd )`" # absolutized and normalized +if [ -z "$MY_PATH" ] ; then + # Error. for some reason, the path is not accessible + # to the script (e.g. permissions re-evaled after suid) + exit 1 # Fail +fi + +BINARY_DIR=$1 +coproc $BINARY_DIR/greeting_server +SERVER_PID=$! + +expected="$MY_PATH/bad_user_expected.txt" +observed="output.txt" +\rm -f $observed + +curl --request GET \ + --url http://localhost:8080 \ + -o $observed +if cmp -s "$expected" "$observed"; then + echo "All good" +else + echo "Unexpected response from server" + exit 1 +fi +\rm -f $observed + + +expected="$MY_PATH/expected.html" +curl --request GET \ + --url http://localhost:8080/user/abc \ + -o $observed +if cmp -s "$expected" "$observed"; then + echo "All good" +else + echo "Unexpected response from server" + exit 1 +fi +\rm -f $observed + +kill $SERVER_PID + +exit 0 \ No newline at end of file diff --git a/examples/helloworld/CMakeLists.txt b/examples/helloworld/CMakeLists.txt new file mode 100644 index 0000000..e96eabf --- /dev/null +++ b/examples/helloworld/CMakeLists.txt @@ -0,0 +1,16 @@ +include_directories(BEFORE ${CMAKE_SOURCE_DIR}/src) + +add_definitions( -DQHTTPSERVER_EXPORT ) + +SET(helloworld_EXE_SRCS + helloworld.cpp + ) + +add_executable(helloworld_server ${helloworld_EXE_SRCS}) + +target_link_libraries(helloworld_server qhttpserver ${Qt6_LIBRARIES}) + +add_test( + NAME helloworld_Test + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test/helloworld.sh ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/examples/helloworld/helloworld.cpp b/examples/helloworld/helloworld.cpp index 428eacd..3a00ddd 100644 --- a/examples/helloworld/helloworld.cpp +++ b/examples/helloworld/helloworld.cpp @@ -20,7 +20,6 @@ HelloWorld::HelloWorld() void HelloWorld::handleRequest(QHttpRequest *req, QHttpResponse *resp) { Q_UNUSED(req); - QByteArray body = "Hello World"; resp->setHeader("Content-Length", QString::number(body.size())); resp->writeHead(200); diff --git a/examples/helloworld/helloworld.h b/examples/helloworld/helloworld.h index 7e6239b..68eaee3 100644 --- a/examples/helloworld/helloworld.h +++ b/examples/helloworld/helloworld.h @@ -11,6 +11,6 @@ class HelloWorld : public QObject public: HelloWorld(); -private slots: +private Q_SLOTS: void handleRequest(QHttpRequest *req, QHttpResponse *resp); }; diff --git a/examples/helloworld/test/expected.txt b/examples/helloworld/test/expected.txt new file mode 100644 index 0000000..5e1c309 --- /dev/null +++ b/examples/helloworld/test/expected.txt @@ -0,0 +1 @@ +Hello World \ No newline at end of file diff --git a/examples/helloworld/test/helloworld.sh b/examples/helloworld/test/helloworld.sh new file mode 100755 index 0000000..05ea70d --- /dev/null +++ b/examples/helloworld/test/helloworld.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +#------------------------------------------------------------------------------- +# to ensure the script actually runs inside its own folder +MY_PATH="`dirname \"$0\"`" # relative +MY_PATH="`( cd \"$MY_PATH\" && pwd )`" # absolutized and normalized +if [ -z "$MY_PATH" ] ; then + # Error. for some reason, the path is not accessible + # to the script (e.g. permissions re-evaled after suid) + exit 1 # Fail +fi + +BINARY_DIR=$1 +coproc $BINARY_DIR/helloworld_server +SERVER_PID=$! + +expected="$MY_PATH/expected.txt" +observed="output.txt" +\rm -f $observed + +curl --request GET \ + --url http://localhost:8080 \ + -o $observed +if cmp -s "$expected" "$observed"; then + echo "All good" +else + echo "Unexpected response from server" + exit 1 +fi +\rm -f $observed + +kill $SERVER_PID + +exit 0 \ No newline at end of file diff --git a/gbuild.sh b/gbuild.sh new file mode 100755 index 0000000..d18ae53 --- /dev/null +++ b/gbuild.sh @@ -0,0 +1,256 @@ +#!/bin/bash + +#Fail if anything goes wrong +# set -o errexit +set -o pipefail +set -o nounset +# set -o xtrace + +echoerr() { printf "\e[31;1m%s\e[0m\n" "$*" >&2; } +usage() +{ +cat << EOF 1>&2; exit 1; +Synopsis: $0 [OPTIONS] + +Options default values are in parentheses. + + -a asan <(OFF)|ON> compile with adress sanitizer + -j n set the compilation to a number of parallel processes. + Default 0 => the value is derived from CPUs available. + -m mode <(Debug)|Release|RelWithDebInfo> compile mode + -n arch <(generic)|native> target architecture mode + -p package <(OFF)|ON> package building selection + -t tests <(OFF)|ON> run unit tests after install + -v version <(val)|rev> version number is set either to the value set by + config files or to the short git sha1 + -G Generator <(Ninja)|Unix|MSYS|NMake|VS> which cmake generator to use. +EOF +exit 1 +} + +[ -z "$QHTTPSERVER_DIST" ] && echo "Need to set QHTTPSERVER_DIST" && exit 1; + +arch="generic" +j="0" +WITH_DEBUG_MESSAGES="OFF" +mode="Debug" +version="val" +resources="build" +CMAKE_GENERATOR="Ninja" +WITH_ASAN="OFF" +WITH_ARCH="OFF" +WITH_PACK="OFF" +RUN_UNIT_TESTS="OFF" + +while getopts ":a:j:m:n:p:t:v:G:" o; do + case "${o}" in + a) + WITH_ASAN=${OPTARG} + [[ "$WITH_ASAN" == "ON" || "$WITH_ASAN" == "OFF" ]] || usage + ;; + j) + j=${OPTARG} + [[ -n "${j##*[!0-9]*}" ]] || usage + ;; + m) + mode=${OPTARG} + [[ "$mode" == "Debug" || "$mode" == "Release" || "$mode" == "RelWithDebInfo" ]] || usage + ;; + n) + arch=${OPTARG} + [[ "x$arch" == "xnative" || "x$arch" == "xgeneric" ]] || usage + ;; + p) + WITH_PACK=${OPTARG} + [[ "$WITH_PACK" == "ON" || "$WITH_PACK" == "OFF" ]] || usage + ;; + t) + RUN_UNIT_TESTS=${OPTARG} + [[ "x$RUN_UNIT_TESTS" == "xON" || "x$RUN_UNIT_TESTS" == "xOFF" ]] || usage + ;; + v) + version=$OPTARG + [[ "$version" == "val" || "$version" == "rev" ]] || usage + ;; + G) + CMAKE_GENERATOR=${OPTARG} + echo "CMAKE_GENERATOR=$CMAKE_GENERATOR" + [[ "$CMAKE_GENERATOR" == "Unix" || + "$CMAKE_GENERATOR" == "Ninja" || + "$CMAKE_GENERATOR" == "MSYS" || + "$CMAKE_GENERATOR" == "NMake" || + "$CMAKE_GENERATOR" == "VS" + ]] || usage + ;; + *) + usage + ;; + esac +done +shift $((OPTIND-1)) + +if type git && git rev-parse --git-dir; then + current_branch=`git rev-parse --abbrev-ref HEAD` + current_revision=`git rev-parse --short HEAD` + current_timestamp=`git show -s --format=%cI HEAD | sed -e 's/[^0-9]//g'` +else + # use default values + current_branch="default" + current_revision="default" + current_timestamp=1 +fi +current_project=`basename $PWD` +current_project_name="`head -n1 CMakeLists.txt`" +build_prefix=build/$current_branch +source_dir=$PWD + +if [[ $version = "rev" ]]; then + release="$current_timestamp-$current_revision" +else + release="0" +fi + +if [[ "$j" == "0" ]]; then + if [[ $CMAKE_GENERATOR == "VS" ]]; then + j=`WMIC CPU Get NumberOfCores | head -n 2 | tail -n 1 | sed -n "s/\s//gp"` + elif [[ $CMAKE_GENERATOR == "Unix" || $CMAKE_GENERATOR == "Ninja" ]]; then + j=`grep -c ^processor /proc/cpuinfo` + fi +fi +if [[ "$j" == "1" ]]; then + echoerr "Linear build" +else + echoerr "Parallel build on $j processors" +fi + +# export VERBOSE=1 +if [[ $mode == "Release" ]]; then + cmake_mode="Release" +elif [[ $mode == "RelWithDebInfo" ]]; then + cmake_mode="RelWithDebInfo" +else + cmake_mode="Debug" +fi + +if [[ $arch == "native" ]]; then + WITH_ARCH="ON" +else + WITH_ARCH="OFF" +fi + +if [[ $CMAKE_GENERATOR == "Unix" ]]; then + make_cmd="make -j$j" + make_test="make test" + make_install="make install" + make_package="make package" + generator="Unix Makefiles" +elif [[ $CMAKE_GENERATOR == "Ninja" ]]; then + make_cmd="ninja -j $j" + make_test="ctest" + make_install="ninja install" + make_package="ninja package" + generator="Ninja" +elif [[ $CMAKE_GENERATOR == "MSYS" ]]; then + make_cmd="make -j$j" + make_test="make test" + make_install="make install" + make_package="make package" + generator="MSYS Makefiles" +elif [[ $CMAKE_GENERATOR == "NMake" ]]; then + make_cmd="nmake && exit 0" + make_test="nmake test" + make_install="nmake install" + make_package="" + generator="NMake Makefiles" +elif [[ $CMAKE_GENERATOR == "VS" ]]; then + make_cmd=""" + pwd && + cmake --build . --config $cmake_mode + """ + make_test="" + make_install="" + generator="Visual Studio 14 2015 Win64" +else + make_cmd="make -j$j" +fi + + +echo "version='$version' release='$release'" + +build_dir=$build_prefix/$mode-$WITH_ASAN/$current_project +mkdir -p $build_dir +pushd $build_dir + +if [ $CMAKE_GENERATOR == "Unix" ] && [ "x$cmake_mode" == "xRelease" ] ; +then + if compgen -G "*/src/*-build/*.rpm" > /dev/null; then + rm -f */src/*-build/*.rpm + fi + if compgen -G "*/src/*-build/*.deb" > /dev/null; then + rm -f */src/*-build/*.deb + fi +fi + +# export LSAN_OPTIONS=suppressions=${LIMA_SOURCES}/suppr.txt +export ASAN_OPTIONS=halt_on_error=0,fast_unwind_on_malloc=0 + +echoerr "Launching cmake from $PWD $source_dir WITH_ASAN=$WITH_ASAN" +cmake -G "$generator" \ + -DCMAKE_BUILD_TYPE:STRING=$cmake_mode \ + -DCMAKE_INSTALL_PREFIX:PATH=$QHTTPSERVER_DIST \ + -DWITH_ARCH=$WITH_ARCH \ + -DWITH_ASAN=$WITH_ASAN \ + $source_dir +result=$? +if [ "$result" != "0" ]; then echoerr "Failed to configure QHTTPSERVER."; popd; exit $result; fi + +echoerr "Running make command:" +echo "$make_cmd" +eval $make_cmd +result=$? +if [ "$result" != "0" ]; then echoerr "Failed to build QHTTPSERVER."; popd; exit $result; fi + + +if [ $RUN_UNIT_TESTS == "ON" ]; +then +echoerr "Running make test:" +eval $make_test +result=$? +echoerr "Done make test:" +fi + +echoerr "Running make install:" +echo "$make_install" +eval $make_install +result=$? +if [ "$result" != "0" ]; then echoerr "Failed to install QHTTPSERVER."; popd; exit $result; fi + +if [ $WITH_PACK == "ON" ]; +then + echoerr "Running make package:" + echo "$make_package" + eval $make_package + result=$? + if [ "$result" != "0" ]; then echoerr "Failed to package QHTTPSERVER."; popd; exit $result; fi +fi + +if [[ ( $CMAKE_GENERATOR == "Ninja" || $CMAKE_GENERATOR == "Unix" ) && "x$cmake_mode" == "xRelease" ]] ; +then + echoerr "Install package:" + install -d $QHTTPSERVER_DIST/share/apps/qhttpserver/packages + if compgen -G ./*.rpm > /dev/null; then + echo "Install RPM package into $QHTTPSERVER_DIST/share/apps/qhttpserver/packages" + install ./*.rpm $QHTTPSERVER_DIST/share/apps/qhttpserver/packages + fi + if compgen -G ./*.deb > /dev/null; then + echo "Install DEB package into $QHTTPSERVER_DIST/share/apps/qhttpserver/packages" + install ./*.deb $QHTTPSERVER_DIST/share/apps/qhttpserver/packages + fi +fi + +echoerr "Built QHTTPSERVER successfully."; +popd + +exit $result + + diff --git a/http-parser/.gitignore b/http-parser/.gitignore index 32cb51b..c122e76 100644 --- a/http-parser/.gitignore +++ b/http-parser/.gitignore @@ -12,6 +12,8 @@ parsertrace_g *.mk *.Makefile *.so.* +*.exe.* +*.exe *.a diff --git a/http-parser/AUTHORS b/http-parser/AUTHORS index 8e2df1d..5323b68 100644 --- a/http-parser/AUTHORS +++ b/http-parser/AUTHORS @@ -65,3 +65,4 @@ Romain Giraud Jay Satiro Arne Steen Kjell Schubert +Olivier Mengué diff --git a/http-parser/LICENSE-MIT b/http-parser/LICENSE-MIT index 58010b3..1ec0ab4 100644 --- a/http-parser/LICENSE-MIT +++ b/http-parser/LICENSE-MIT @@ -1,8 +1,4 @@ -http_parser.c is based on src/http/ngx_http_parse.c from NGINX copyright -Igor Sysoev. - -Additional changes are licensed under the same terms as NGINX and -copyright Joyent, Inc. and other Node contributors. All rights reserved. +Copyright Joyent, Inc. and other Node contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to diff --git a/http-parser/README.md b/http-parser/README.md index 7c54dd4..b265d71 100644 --- a/http-parser/README.md +++ b/http-parser/README.md @@ -1,7 +1,7 @@ HTTP Parser =========== -[![Build Status](https://travis-ci.org/joyent/http-parser.png?branch=master)](https://travis-ci.org/joyent/http-parser) +[![Build Status](https://api.travis-ci.org/nodejs/http-parser.svg?branch=master)](https://travis-ci.org/nodejs/http-parser) This is a parser for HTTP messages written in C. It parses both requests and responses. The parser is designed to be used in performance HTTP @@ -72,9 +72,9 @@ if (parser->upgrade) { } ``` -HTTP needs to know where the end of the stream is. For example, sometimes +`http_parser` needs to know where the end of the stream is. For example, sometimes servers send responses without Content-Length and expect the client to -consume input (for the body) until EOF. To tell http_parser about EOF, give +consume input (for the body) until EOF. To tell `http_parser` about EOF, give `0` as the fourth parameter to `http_parser_execute()`. Callbacks and errors can still be encountered during an EOF, so one must still be prepared to receive them. @@ -93,8 +93,8 @@ the on_body callback. The Special Problem of Upgrade ------------------------------ -HTTP supports upgrading the connection to a different protocol. An -increasingly common example of this is the Web Socket protocol which sends +`http_parser` supports upgrading the connection to a different protocol. An +increasingly common example of this is the WebSocket protocol which sends a request like GET /demo HTTP/1.1 @@ -106,8 +106,8 @@ a request like followed by non-HTTP data. -(See http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 for more -information the Web Socket protocol.) +(See [RFC6455](https://tools.ietf.org/html/rfc6455) for more information the +WebSocket protocol.) To support this, the parser will treat this as a normal HTTP message without a body, issuing both on_headers_complete and on_message_complete callbacks. However @@ -137,9 +137,72 @@ There are two types of callbacks: Callbacks must return 0 on success. Returning a non-zero value indicates error to the parser, making it exit immediately. +For cases where it is necessary to pass local information to/from a callback, +the `http_parser` object's `data` field can be used. +An example of such a case is when using threads to handle a socket connection, +parse a request, and then give a response over that socket. By instantiation +of a thread-local struct containing relevant data (e.g. accepted socket, +allocated memory for callbacks to write into, etc), a parser's callbacks are +able to communicate data between the scope of the thread and the scope of the +callback in a threadsafe manner. This allows `http_parser` to be used in +multi-threaded contexts. + +Example: +```c + typedef struct { + socket_t sock; + void* buffer; + int buf_len; + } custom_data_t; + + +int my_url_callback(http_parser* parser, const char *at, size_t length) { + /* access to thread local custom_data_t struct. + Use this access save parsed data for later use into thread local + buffer, or communicate over socket + */ + parser->data; + ... + return 0; +} + +... + +void http_parser_thread(socket_t sock) { + int nparsed = 0; + /* allocate memory for user data */ + custom_data_t *my_data = malloc(sizeof(custom_data_t)); + + /* some information for use by callbacks. + * achieves thread -> callback information flow */ + my_data->sock = sock; + + /* instantiate a thread-local parser */ + http_parser *parser = malloc(sizeof(http_parser)); + http_parser_init(parser, HTTP_REQUEST); /* initialise parser */ + /* this custom data reference is accessible through the reference to the + parser supplied to callback functions */ + parser->data = my_data; + + http_parser_settings settings; /* set up callbacks */ + settings.on_url = my_url_callback; + + /* execute parser */ + nparsed = http_parser_execute(parser, &settings, buf, recved); + + ... + /* parsed information copied from callback. + can now perform action on data copied into thread-local memory from callbacks. + achieves callback -> thread information flow */ + my_data->buffer; + ... +} + +``` + In case you parse HTTP message in chunks (i.e. `read()` request line from socket, parse, read half headers, parse, etc) your data callbacks -may be called more than once. Http-parser guarantees that data pointer is only +may be called more than once. `http_parser` guarantees that data pointer is only valid for the lifetime of callback. You can also `read()` into a heap allocated buffer to avoid copying memory around if this fits your application. diff --git a/http-parser/bench.c b/http-parser/bench.c index 5b452fa..678f555 100644 --- a/http-parser/bench.c +++ b/http-parser/bench.c @@ -20,10 +20,14 @@ */ #include "http_parser.h" #include +#include #include #include #include +/* 8 gb */ +static const int64_t kBytes = 8LL << 30; + static const char data[] = "POST /joyent/http-parser HTTP/1.1\r\n" "Host: github.com\r\n" @@ -38,7 +42,7 @@ static const char data[] = "Referer: https://github.com/joyent/http-parser\r\n" "Connection: keep-alive\r\n" "Transfer-Encoding: chunked\r\n" - "Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n\r\n"; + "Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n"; static const size_t data_len = sizeof(data) - 1; static int on_info(http_parser* p) { @@ -67,13 +71,13 @@ int bench(int iter_count, int silent) { int err; struct timeval start; struct timeval end; - float rps; if (!silent) { err = gettimeofday(&start, NULL); assert(err == 0); } + fprintf(stderr, "req_len=%d\n", (int) data_len); for (i = 0; i < iter_count; i++) { size_t parsed; http_parser_init(&parser, HTTP_REQUEST); @@ -83,17 +87,27 @@ int bench(int iter_count, int silent) { } if (!silent) { + double elapsed; + double bw; + double total; + err = gettimeofday(&end, NULL); assert(err == 0); fprintf(stdout, "Benchmark result:\n"); - rps = (float) (end.tv_sec - start.tv_sec) + - (end.tv_usec - start.tv_usec) * 1e-6f; - fprintf(stdout, "Took %f seconds to run\n", rps); + elapsed = (double) (end.tv_sec - start.tv_sec) + + (end.tv_usec - start.tv_usec) * 1e-6f; + + total = (double) iter_count * data_len; + bw = (double) total / elapsed; + + fprintf(stdout, "%.2f mb | %.2f mb/s | %.2f req/sec | %.2f s\n", + (double) total / (1024 * 1024), + bw / (1024 * 1024), + (double) iter_count / elapsed, + elapsed); - rps = (float) iter_count / rps; - fprintf(stdout, "%f req/sec\n", rps); fflush(stdout); } @@ -101,11 +115,14 @@ int bench(int iter_count, int silent) { } int main(int argc, char** argv) { + int64_t iterations; + + iterations = kBytes / (int64_t) data_len; if (argc == 2 && strcmp(argv[1], "infinite") == 0) { for (;;) - bench(5000000, 1); + bench(iterations, 1); return 0; } else { - return bench(5000000, 0); + return bench(iterations, 0); } } diff --git a/http-parser/contrib/parsertrace.c b/http-parser/contrib/parsertrace.c index e715368..3daa7f4 100644 --- a/http-parser/contrib/parsertrace.c +++ b/http-parser/contrib/parsertrace.c @@ -1,7 +1,4 @@ -/* Based on src/http/ngx_http_parse.c from NGINX copyright Igor Sysoev - * - * Additional changes are licensed under the same terms as NGINX and - * copyright Joyent, Inc. and other Node contributors. All rights reserved. +/* Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to diff --git a/http-parser/contrib/url_parser.c b/http-parser/contrib/url_parser.c index 6650b41..f235bed 100644 --- a/http-parser/contrib/url_parser.c +++ b/http-parser/contrib/url_parser.c @@ -35,6 +35,7 @@ int main(int argc, char ** argv) { connect = strcmp("connect", argv[1]) == 0 ? 1 : 0; printf("Parsing %s, connect %d\n", argv[2], connect); + http_parser_url_init(&u); result = http_parser_parse_url(argv[2], len, connect, &u); if (result != 0) { printf("Parse error : %d\n", result); @@ -43,4 +44,4 @@ int main(int argc, char ** argv) { printf("Parse ok, result : \n"); dump_url(argv[2], &u); return 0; -} \ No newline at end of file +} diff --git a/http-parser/http_parser.c b/http-parser/http_parser.c index 0fa1c36..4896385 100644 --- a/http-parser/http_parser.c +++ b/http-parser/http_parser.c @@ -1,7 +1,4 @@ -/* Based on src/http/ngx_http_parse.c from NGINX copyright Igor Sysoev - * - * Additional changes are licensed under the same terms as NGINX and - * copyright Joyent, Inc. and other Node contributors. All rights reserved. +/* Copyright Joyent, Inc. and other Node contributors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -25,10 +22,11 @@ #include #include #include -#include #include #include +static uint32_t max_header_size = HTTP_MAX_HEADER_SIZE; + #ifndef ULLONG_MAX # define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */ #endif @@ -53,6 +51,7 @@ #define SET_ERRNO(e) \ do { \ + parser->nread = nread; \ parser->http_errno = (e); \ } while(0) @@ -60,6 +59,7 @@ do { \ #define UPDATE_STATE(V) p_state = (enum state) (V); #define RETURN(V) \ do { \ + parser->nread = nread; \ parser->state = CURRENT_STATE(); \ return (V); \ } while (0); @@ -123,7 +123,7 @@ do { \ FOR##_mark = NULL; \ } \ } while (0) - + /* Run the data callback FOR and consume the current byte */ #define CALLBACK_DATA(FOR) \ CALLBACK_DATA_(FOR, p - FOR##_mark, p - data + 1) @@ -141,20 +141,20 @@ do { \ } while (0) /* Don't allow the total size of the HTTP headers (including the status - * line) to exceed HTTP_MAX_HEADER_SIZE. This check is here to protect + * line) to exceed max_header_size. This check is here to protect * embedders against denial-of-service attacks where the attacker feeds * us a never-ending header that the embedder keeps buffering. * * This check is arguably the responsibility of embedders but we're doing * it on the embedder's behalf because most won't bother and this way we - * make the web a little safer. HTTP_MAX_HEADER_SIZE is still far bigger + * make the web a little safer. max_header_size is still far bigger * than any reasonable request or response so this should never affect * day-to-day operation. */ #define COUNT_HEADER_SIZE(V) \ do { \ - parser->nread += (V); \ - if (UNLIKELY(parser->nread > (HTTP_MAX_HEADER_SIZE))) { \ + nread += (uint32_t)(V); \ + if (UNLIKELY(nread > max_header_size)) { \ SET_ERRNO(HPE_HEADER_OVERFLOW); \ goto error; \ } \ @@ -196,7 +196,7 @@ static const char tokens[256] = { /* 24 can 25 em 26 sub 27 esc 28 fs 29 gs 30 rs 31 us */ 0, 0, 0, 0, 0, 0, 0, 0, /* 32 sp 33 ! 34 " 35 # 36 $ 37 % 38 & 39 ' */ - 0, '!', 0, '#', '$', '%', '&', '\'', + ' ', '!', 0, '#', '$', '%', '&', '\'', /* 40 ( 41 ) 42 * 43 + 44 , 45 - 46 . 47 / */ 0, 0, '*', '+', 0, '-', '.', 0, /* 48 0 49 1 50 2 51 3 52 4 53 5 54 6 55 7 */ @@ -286,10 +286,10 @@ enum state , s_res_HT , s_res_HTT , s_res_HTTP - , s_res_first_http_major , s_res_http_major - , s_res_first_http_minor + , s_res_http_dot , s_res_http_minor + , s_res_http_end , s_res_first_status_code , s_res_status_code , s_res_status_start @@ -316,10 +316,12 @@ enum state , s_req_http_HT , s_req_http_HTT , s_req_http_HTTP - , s_req_first_http_major + , s_req_http_I + , s_req_http_IC , s_req_http_major - , s_req_first_http_minor + , s_req_http_dot , s_req_http_minor + , s_req_http_end , s_req_line_almost_done , s_header_field_start @@ -374,6 +376,8 @@ enum header_states , h_connection , h_content_length + , h_content_length_num + , h_content_length_ws , h_transfer_encoding , h_upgrade @@ -400,6 +404,8 @@ enum http_host_state , s_http_host , s_http_host_v6 , s_http_host_v6_end + , s_http_host_v6_zone_start + , s_http_host_v6_zone , s_http_host_port_start , s_http_host_port }; @@ -419,20 +425,26 @@ enum http_host_state (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ (c) == '$' || (c) == ',') -#define STRICT_TOKEN(c) (tokens[(unsigned char)c]) +#define STRICT_TOKEN(c) ((c == ' ') ? 0 : tokens[(unsigned char)c]) #if HTTP_PARSER_STRICT -#define TOKEN(c) (tokens[(unsigned char)c]) +#define TOKEN(c) STRICT_TOKEN(c) #define IS_URL_CHAR(c) (BIT_AT(normal_url_char, (unsigned char)c)) #define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') #else -#define TOKEN(c) ((c == ' ') ? ' ' : tokens[(unsigned char)c]) +#define TOKEN(c) tokens[(unsigned char)c] #define IS_URL_CHAR(c) \ (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) #define IS_HOST_CHAR(c) \ (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') #endif +/** + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + **/ +#define IS_HEADER_CHAR(ch) \ + (ch == CR || ch == LF || ch == 9 || ((unsigned char)ch > 31 && ch != 127)) #define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res) @@ -534,7 +546,7 @@ parse_url_char(enum state s, const char ch) return s_dead; } - /* FALLTHROUGH */ + /* fall through */ case s_req_server_start: case s_req_server: if (ch == '/') { @@ -637,6 +649,8 @@ size_t http_parser_execute (http_parser *parser, const char *body_mark = 0; const char *status_mark = 0; enum state p_state = (enum state) parser->state; + const unsigned int lenient = parser->lenient_http_headers; + uint32_t nread = parser->nread; /* We're in an error state. Don't bother doing anything. */ if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { @@ -748,21 +762,16 @@ size_t http_parser_execute (http_parser *parser, case s_start_res: { + if (ch == CR || ch == LF) + break; parser->flags = 0; parser->content_length = ULLONG_MAX; - switch (ch) { - case 'H': - UPDATE_STATE(s_res_H); - break; - - case CR: - case LF: - break; - - default: - SET_ERRNO(HPE_INVALID_CONSTANT); - goto error; + if (ch == 'H') { + UPDATE_STATE(s_res_H); + } else { + SET_ERRNO(HPE_INVALID_CONSTANT); + goto error; } CALLBACK_NOTIFY(message_begin); @@ -786,75 +795,48 @@ size_t http_parser_execute (http_parser *parser, case s_res_HTTP: STRICT_CHECK(ch != '/'); - UPDATE_STATE(s_res_first_http_major); + UPDATE_STATE(s_res_http_major); break; - case s_res_first_http_major: - if (UNLIKELY(ch < '0' || ch > '9')) { + case s_res_http_major: + if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_major = ch - '0'; - UPDATE_STATE(s_res_http_major); + UPDATE_STATE(s_res_http_dot); break; - /* major HTTP version or dot */ - case s_res_http_major: + case s_res_http_dot: { - if (ch == '.') { - UPDATE_STATE(s_res_first_http_minor); - break; - } - - if (!IS_NUM(ch)) { - SET_ERRNO(HPE_INVALID_VERSION); - goto error; - } - - parser->http_major *= 10; - parser->http_major += ch - '0'; - - if (UNLIKELY(parser->http_major > 999)) { + if (UNLIKELY(ch != '.')) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } + UPDATE_STATE(s_res_http_minor); break; } - /* first digit of minor HTTP version */ - case s_res_first_http_minor: + case s_res_http_minor: if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_minor = ch - '0'; - UPDATE_STATE(s_res_http_minor); + UPDATE_STATE(s_res_http_end); break; - /* minor HTTP version or end of request line */ - case s_res_http_minor: + case s_res_http_end: { - if (ch == ' ') { - UPDATE_STATE(s_res_first_status_code); - break; - } - - if (UNLIKELY(!IS_NUM(ch))) { - SET_ERRNO(HPE_INVALID_VERSION); - goto error; - } - - parser->http_minor *= 10; - parser->http_minor += ch - '0'; - - if (UNLIKELY(parser->http_minor > 999)) { + if (UNLIKELY(ch != ' ')) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } + UPDATE_STATE(s_res_first_status_code); break; } @@ -881,10 +863,9 @@ size_t http_parser_execute (http_parser *parser, UPDATE_STATE(s_res_status_start); break; case CR: - UPDATE_STATE(s_res_line_almost_done); - break; case LF: - UPDATE_STATE(s_header_field_start); + UPDATE_STATE(s_res_status_start); + REEXECUTE(); break; default: SET_ERRNO(HPE_INVALID_STATUS); @@ -906,19 +887,13 @@ size_t http_parser_execute (http_parser *parser, case s_res_status_start: { - if (ch == CR) { - UPDATE_STATE(s_res_line_almost_done); - break; - } - - if (ch == LF) { - UPDATE_STATE(s_header_field_start); - break; - } - MARK(status); UPDATE_STATE(s_res_status); parser->index = 0; + + if (ch == CR || ch == LF) + REEXECUTE(); + break; } @@ -957,21 +932,23 @@ size_t http_parser_execute (http_parser *parser, parser->method = (enum http_method) 0; parser->index = 1; switch (ch) { + case 'A': parser->method = HTTP_ACL; break; + case 'B': parser->method = HTTP_BIND; break; case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; case 'D': parser->method = HTTP_DELETE; break; case 'G': parser->method = HTTP_GET; break; case 'H': parser->method = HTTP_HEAD; break; - case 'L': parser->method = HTTP_LOCK; break; + case 'L': parser->method = HTTP_LOCK; /* or LINK */ break; case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break; case 'N': parser->method = HTTP_NOTIFY; break; case 'O': parser->method = HTTP_OPTIONS; break; case 'P': parser->method = HTTP_POST; /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ break; - case 'R': parser->method = HTTP_REPORT; break; - case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH */ break; + case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break; + case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH, SOURCE */ break; case 'T': parser->method = HTTP_TRACE; break; - case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE */ break; + case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break; default: SET_ERRNO(HPE_INVALID_METHOD); goto error; @@ -996,69 +973,37 @@ size_t http_parser_execute (http_parser *parser, UPDATE_STATE(s_req_spaces_before_url); } else if (ch == matcher[parser->index]) { ; /* nada */ - } else if (parser->method == HTTP_CONNECT) { - if (parser->index == 1 && ch == 'H') { - parser->method = HTTP_CHECKOUT; - } else if (parser->index == 2 && ch == 'P') { - parser->method = HTTP_COPY; - } else { - SET_ERRNO(HPE_INVALID_METHOD); - goto error; - } - } else if (parser->method == HTTP_MKCOL) { - if (parser->index == 1 && ch == 'O') { - parser->method = HTTP_MOVE; - } else if (parser->index == 1 && ch == 'E') { - parser->method = HTTP_MERGE; - } else if (parser->index == 1 && ch == '-') { - parser->method = HTTP_MSEARCH; - } else if (parser->index == 2 && ch == 'A') { - parser->method = HTTP_MKACTIVITY; - } else if (parser->index == 3 && ch == 'A') { - parser->method = HTTP_MKCALENDAR; - } else { - SET_ERRNO(HPE_INVALID_METHOD); - goto error; - } - } else if (parser->method == HTTP_SUBSCRIBE) { - if (parser->index == 1 && ch == 'E') { - parser->method = HTTP_SEARCH; - } else { - SET_ERRNO(HPE_INVALID_METHOD); - goto error; - } - } else if (parser->index == 1 && parser->method == HTTP_POST) { - if (ch == 'R') { - parser->method = HTTP_PROPFIND; /* or HTTP_PROPPATCH */ - } else if (ch == 'U') { - parser->method = HTTP_PUT; /* or HTTP_PURGE */ - } else if (ch == 'A') { - parser->method = HTTP_PATCH; - } else { - SET_ERRNO(HPE_INVALID_METHOD); - goto error; - } - } else if (parser->index == 2) { - if (parser->method == HTTP_PUT) { - if (ch == 'R') { - parser->method = HTTP_PURGE; - } else { - SET_ERRNO(HPE_INVALID_METHOD); - goto error; - } - } else if (parser->method == HTTP_UNLOCK) { - if (ch == 'S') { - parser->method = HTTP_UNSUBSCRIBE; - } else { + } else if ((ch >= 'A' && ch <= 'Z') || ch == '-') { + + switch (parser->method << 16 | parser->index << 8 | ch) { +#define XX(meth, pos, ch, new_meth) \ + case (HTTP_##meth << 16 | pos << 8 | ch): \ + parser->method = HTTP_##new_meth; break; + + XX(POST, 1, 'U', PUT) + XX(POST, 1, 'A', PATCH) + XX(POST, 1, 'R', PROPFIND) + XX(PUT, 2, 'R', PURGE) + XX(CONNECT, 1, 'H', CHECKOUT) + XX(CONNECT, 2, 'P', COPY) + XX(MKCOL, 1, 'O', MOVE) + XX(MKCOL, 1, 'E', MERGE) + XX(MKCOL, 1, '-', MSEARCH) + XX(MKCOL, 2, 'A', MKACTIVITY) + XX(MKCOL, 3, 'A', MKCALENDAR) + XX(SUBSCRIBE, 1, 'E', SEARCH) + XX(SUBSCRIBE, 1, 'O', SOURCE) + XX(REPORT, 2, 'B', REBIND) + XX(PROPFIND, 4, 'P', PROPPATCH) + XX(LOCK, 1, 'I', LINK) + XX(UNLOCK, 2, 'S', UNSUBSCRIBE) + XX(UNLOCK, 2, 'B', UNBIND) + XX(UNLOCK, 3, 'I', UNLINK) +#undef XX + default: SET_ERRNO(HPE_INVALID_METHOD); goto error; - } - } else { - SET_ERRNO(HPE_INVALID_METHOD); - goto error; } - } else if (parser->index == 4 && parser->method == HTTP_PROPFIND && ch == 'P') { - parser->method = HTTP_PROPPATCH; } else { SET_ERRNO(HPE_INVALID_METHOD); goto error; @@ -1143,11 +1088,17 @@ size_t http_parser_execute (http_parser *parser, case s_req_http_start: switch (ch) { + case ' ': + break; case 'H': UPDATE_STATE(s_req_http_H); break; - case ' ': - break; + case 'I': + if (parser->method == HTTP_SOURCE) { + UPDATE_STATE(s_req_http_I); + break; + } + /* fall through */ default: SET_ERRNO(HPE_INVALID_CONSTANT); goto error; @@ -1169,59 +1120,53 @@ size_t http_parser_execute (http_parser *parser, UPDATE_STATE(s_req_http_HTTP); break; - case s_req_http_HTTP: - STRICT_CHECK(ch != '/'); - UPDATE_STATE(s_req_first_http_major); + case s_req_http_I: + STRICT_CHECK(ch != 'C'); + UPDATE_STATE(s_req_http_IC); break; - /* first digit of major HTTP version */ - case s_req_first_http_major: - if (UNLIKELY(ch < '1' || ch > '9')) { - SET_ERRNO(HPE_INVALID_VERSION); - goto error; - } + case s_req_http_IC: + STRICT_CHECK(ch != 'E'); + UPDATE_STATE(s_req_http_HTTP); /* Treat "ICE" as "HTTP". */ + break; - parser->http_major = ch - '0'; + case s_req_http_HTTP: + STRICT_CHECK(ch != '/'); UPDATE_STATE(s_req_http_major); break; - /* major HTTP version or dot */ case s_req_http_major: - { - if (ch == '.') { - UPDATE_STATE(s_req_first_http_minor); - break; - } - if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } - parser->http_major *= 10; - parser->http_major += ch - '0'; + parser->http_major = ch - '0'; + UPDATE_STATE(s_req_http_dot); + break; - if (UNLIKELY(parser->http_major > 999)) { + case s_req_http_dot: + { + if (UNLIKELY(ch != '.')) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } + UPDATE_STATE(s_req_http_minor); break; } - /* first digit of minor HTTP version */ - case s_req_first_http_minor: + case s_req_http_minor: if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_VERSION); goto error; } parser->http_minor = ch - '0'; - UPDATE_STATE(s_req_http_minor); + UPDATE_STATE(s_req_http_end); break; - /* minor HTTP version or end of request line */ - case s_req_http_minor: + case s_req_http_end: { if (ch == CR) { UPDATE_STATE(s_req_line_almost_done); @@ -1233,21 +1178,8 @@ size_t http_parser_execute (http_parser *parser, break; } - /* XXX allow spaces after digit? */ - - if (UNLIKELY(!IS_NUM(ch))) { - SET_ERRNO(HPE_INVALID_VERSION); - goto error; - } - - parser->http_minor *= 10; - parser->http_minor += ch - '0'; - - if (UNLIKELY(parser->http_minor > 999)) { - SET_ERRNO(HPE_INVALID_VERSION); - goto error; - } - + SET_ERRNO(HPE_INVALID_VERSION); + goto error; break; } @@ -1324,8 +1256,14 @@ size_t http_parser_execute (http_parser *parser, break; switch (parser->header_state) { - case h_general: + case h_general: { + size_t left = data + len - p; + const char* pe = p + MIN(left, max_header_size); + while (p+1 < pe && TOKEN(p[1])) { + p++; + } break; + } case h_C: parser->index++; @@ -1425,13 +1363,14 @@ size_t http_parser_execute (http_parser *parser, } } - COUNT_HEADER_SIZE(p - start); - if (p == data + len) { --p; + COUNT_HEADER_SIZE(p - start); break; } + COUNT_HEADER_SIZE(p - start); + if (ch == ':') { UPDATE_STATE(s_header_value_discard_ws); CALLBACK_DATA(header_field); @@ -1455,7 +1394,7 @@ size_t http_parser_execute (http_parser *parser, break; } - /* FALLTHROUGH */ + /* fall through */ case s_header_value_start: { @@ -1487,7 +1426,19 @@ size_t http_parser_execute (http_parser *parser, goto error; } + if (parser->flags & F_CONTENTLENGTH) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + + parser->flags |= F_CONTENTLENGTH; parser->content_length = ch - '0'; + parser->header_state = h_content_length_num; + break; + + /* when obsolete line folding is encountered for content length + * continue to the s_header_value state */ + case h_content_length_ws: break; case h_connection: @@ -1536,33 +1487,34 @@ size_t http_parser_execute (http_parser *parser, REEXECUTE(); } + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + c = LOWER(ch); switch (h_state) { case h_general: - { - const char* p_cr; - const char* p_lf; - size_t limit = data + len - p; - - limit = MIN(limit, HTTP_MAX_HEADER_SIZE); - - p_cr = (const char*) memchr(p, CR, limit); - p_lf = (const char*) memchr(p, LF, limit); - if (p_cr != NULL) { - if (p_lf != NULL && p_cr >= p_lf) - p = p_lf; - else - p = p_cr; - } else if (UNLIKELY(p_lf != NULL)) { - p = p_lf; - } else { - p = data + len; + { + size_t left = data + len - p; + const char* pe = p + MIN(left, max_header_size); + + for (; p != pe; p++) { + ch = *p; + if (ch == CR || ch == LF) { + --p; + break; + } + if (!lenient && !IS_HEADER_CHAR(ch)) { + SET_ERRNO(HPE_INVALID_HEADER_TOKEN); + goto error; + } + } + if (p == data + len) + --p; + break; } - --p; - - break; - } case h_connection: case h_transfer_encoding: @@ -1570,10 +1522,18 @@ size_t http_parser_execute (http_parser *parser, break; case h_content_length: + if (ch == ' ') break; + h_state = h_content_length_num; + /* fall through */ + + case h_content_length_num: { uint64_t t; - if (ch == ' ') break; + if (ch == ' ') { + h_state = h_content_length_ws; + break; + } if (UNLIKELY(!IS_NUM(ch))) { SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); @@ -1596,6 +1556,12 @@ size_t http_parser_execute (http_parser *parser, break; } + case h_content_length_ws: + if (ch == ' ') break; + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + parser->header_state = h_state; + goto error; + /* Transfer-Encoding: chunked */ case h_matching_transfer_encoding_chunked: parser->index++; @@ -1694,16 +1660,19 @@ size_t http_parser_execute (http_parser *parser, } parser->header_state = h_state; - COUNT_HEADER_SIZE(p - start); - if (p == data + len) --p; + + COUNT_HEADER_SIZE(p - start); break; } case s_header_almost_done: { - STRICT_CHECK(ch != LF); + if (UNLIKELY(ch != LF)) { + SET_ERRNO(HPE_LF_EXPECTED); + goto error; + } UPDATE_STATE(s_header_value_lws); break; @@ -1712,6 +1681,10 @@ size_t http_parser_execute (http_parser *parser, case s_header_value_lws: { if (ch == ' ' || ch == '\t') { + if (parser->header_state == h_content_length_num) { + /* treat obsolete line folding as space */ + parser->header_state = h_content_length_ws; + } UPDATE_STATE(s_header_value_start); REEXECUTE(); } @@ -1764,6 +1737,11 @@ size_t http_parser_execute (http_parser *parser, case h_transfer_encoding_chunked: parser->flags |= F_CHUNKED; break; + case h_content_length: + /* do not allow empty content length */ + SET_ERRNO(HPE_INVALID_CONTENT_LENGTH); + goto error; + break; default: break; } @@ -1787,13 +1765,28 @@ size_t http_parser_execute (http_parser *parser, REEXECUTE(); } + /* Cannot use chunked encoding and a content-length header together + per the HTTP specification. */ + if ((parser->flags & F_CHUNKED) && + (parser->flags & F_CONTENTLENGTH)) { + SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH); + goto error; + } + UPDATE_STATE(s_headers_done); /* Set this here so that on_headers_complete() callbacks can see it */ - parser->upgrade = - ((parser->flags & (F_UPGRADE | F_CONNECTION_UPGRADE)) == - (F_UPGRADE | F_CONNECTION_UPGRADE) || - parser->method == HTTP_CONNECT); + if ((parser->flags & F_UPGRADE) && + (parser->flags & F_CONNECTION_UPGRADE)) { + /* For responses, "Upgrade: foo" and "Connection: upgrade" are + * mandatory only when it is a 101 Switching Protocols response, + * otherwise it is purely informational, to announce support. + */ + parser->upgrade = + (parser->type == HTTP_REQUEST || parser->status_code == 101); + } else { + parser->upgrade = (parser->method == HTTP_CONNECT); + } /* Here we call the headers_complete callback. This is somewhat * different than other callbacks because if the user returns 1, we @@ -1809,6 +1802,10 @@ size_t http_parser_execute (http_parser *parser, case 0: break; + case 2: + parser->upgrade = 1; + + /* fall through */ case 1: parser->flags |= F_SKIPBODY; break; @@ -1828,11 +1825,13 @@ size_t http_parser_execute (http_parser *parser, case s_headers_done: { + int hasBody; STRICT_CHECK(ch != LF); parser->nread = 0; + nread = 0; - int hasBody = parser->flags & F_CHUNKED || + hasBody = parser->flags & F_CHUNKED || (parser->content_length > 0 && parser->content_length != ULLONG_MAX); if (parser->upgrade && (parser->method == HTTP_CONNECT || (parser->flags & F_SKIPBODY) || !hasBody)) { @@ -1857,8 +1856,7 @@ size_t http_parser_execute (http_parser *parser, /* Content-Length header given and non-zero */ UPDATE_STATE(s_body_identity); } else { - if (parser->type == HTTP_REQUEST || - !http_message_needs_eof(parser)) { + if (!http_message_needs_eof(parser)) { /* Assume content-length 0 - read the next */ UPDATE_STATE(NEW_MESSAGE()); CALLBACK_NOTIFY(message_complete); @@ -1926,7 +1924,7 @@ size_t http_parser_execute (http_parser *parser, case s_chunk_size_start: { - assert(parser->nread == 1); + assert(nread == 1); assert(parser->flags & F_CHUNKED); unhex_val = unhex[(unsigned char)ch]; @@ -1994,6 +1992,7 @@ size_t http_parser_execute (http_parser *parser, STRICT_CHECK(ch != LF); parser->nread = 0; + nread = 0; if (parser->content_length == 0) { parser->flags |= F_TRAILING; @@ -2040,6 +2039,7 @@ size_t http_parser_execute (http_parser *parser, assert(parser->flags & F_CHUNKED); STRICT_CHECK(ch != LF); parser->nread = 0; + nread = 0; UPDATE_STATE(s_chunk_size_start); CALLBACK_NOTIFY(chunk_complete); break; @@ -2051,7 +2051,7 @@ size_t http_parser_execute (http_parser *parser, } } - /* Run callbacks for any marks that we have leftover after we ran our of + /* Run callbacks for any marks that we have leftover after we ran out of * bytes. There should be at most one of these set, so it's OK to invoke * them in series (unset marks will not result in callbacks). * @@ -2133,6 +2133,16 @@ http_method_str (enum http_method m) return ELEM_AT(method_strings, m, ""); } +const char * +http_status_str (enum http_status s) +{ + switch (s) { +#define XX(num, name, string) case HTTP_STATUS_##name: return #string; + HTTP_STATUS_MAP(XX) +#undef XX + default: return ""; + } +} void http_parser_init (http_parser *parser, enum http_parser_type t) @@ -2153,15 +2163,13 @@ http_parser_settings_init(http_parser_settings *settings) const char * http_errno_name(enum http_errno err) { - assert(((size_t) err) < - (sizeof(http_strerror_tab) / sizeof(http_strerror_tab[0]))); + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); return http_strerror_tab[err].name; } const char * http_errno_description(enum http_errno err) { - assert(((size_t) err) < - (sizeof(http_strerror_tab) / sizeof(http_strerror_tab[0]))); + assert(((size_t) err) < ARRAY_SIZE(http_strerror_tab)); return http_strerror_tab[err].description; } @@ -2195,7 +2203,7 @@ http_parse_host_char(enum http_host_state s, const char ch) { return s_http_host; } - /* FALLTHROUGH */ + /* fall through */ case s_http_host_v6_end: if (ch == ':') { return s_http_host_port_start; @@ -2208,12 +2216,29 @@ http_parse_host_char(enum http_host_state s, const char ch) { return s_http_host_v6_end; } - /* FALLTHROUGH */ + /* fall through */ case s_http_host_v6_start: if (IS_HEX(ch) || ch == ':' || ch == '.') { return s_http_host_v6; } + if (s == s_http_host_v6 && ch == '%') { + return s_http_host_v6_zone_start; + } + break; + + case s_http_host_v6_zone: + if (ch == ']') { + return s_http_host_v6_end; + } + + /* fall through */ + case s_http_host_v6_zone_start: + /* RFC 6874 Zone ID consists of 1*( unreserved / pct-encoded) */ + if (IS_ALPHANUM(ch) || ch == '%' || ch == '.' || ch == '-' || ch == '_' || + ch == '~') { + return s_http_host_v6_zone; + } break; case s_http_host_port: @@ -2237,6 +2262,8 @@ http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { const char *p; size_t buflen = u->field_data[UF_HOST].off + u->field_data[UF_HOST].len; + assert(u->field_set & (1 << UF_HOST)); + u->field_data[UF_HOST].len = 0; s = found_at ? s_http_userinfo_start : s_http_host_start; @@ -2251,21 +2278,26 @@ http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { switch(new_s) { case s_http_host: if (s != s_http_host) { - u->field_data[UF_HOST].off = p - buf; + u->field_data[UF_HOST].off = (uint16_t)(p - buf); } u->field_data[UF_HOST].len++; break; case s_http_host_v6: if (s != s_http_host_v6) { - u->field_data[UF_HOST].off = p - buf; + u->field_data[UF_HOST].off = (uint16_t)(p - buf); } u->field_data[UF_HOST].len++; break; + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: + u->field_data[UF_HOST].len++; + break; + case s_http_host_port: if (s != s_http_host_port) { - u->field_data[UF_PORT].off = p - buf; + u->field_data[UF_PORT].off = (uint16_t)(p - buf); u->field_data[UF_PORT].len = 0; u->field_set |= (1 << UF_PORT); } @@ -2274,7 +2306,7 @@ http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { case s_http_userinfo: if (s != s_http_userinfo) { - u->field_data[UF_USERINFO].off = p - buf ; + u->field_data[UF_USERINFO].off = (uint16_t)(p - buf); u->field_data[UF_USERINFO].len = 0; u->field_set |= (1 << UF_USERINFO); } @@ -2292,6 +2324,8 @@ http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { case s_http_host_start: case s_http_host_v6_start: case s_http_host_v6: + case s_http_host_v6_zone_start: + case s_http_host_v6_zone: case s_http_host_port_start: case s_http_userinfo: case s_http_userinfo_start: @@ -2303,6 +2337,11 @@ http_parse_host(const char * buf, struct http_parser_url *u, int found_at) { return 0; } +void +http_parser_url_init(struct http_parser_url *u) { + memset(u, 0, sizeof(*u)); +} + int http_parser_parse_url(const char *buf, size_t buflen, int is_connect, struct http_parser_url *u) @@ -2312,6 +2351,10 @@ http_parser_parse_url(const char *buf, size_t buflen, int is_connect, enum http_parser_url_fields uf, old_uf; int found_at = 0; + if (buflen == 0) { + return 1; + } + u->port = u->field_set = 0; s = is_connect ? s_req_server_start : s_req_spaces_before_url; old_uf = UF_MAX; @@ -2339,7 +2382,7 @@ http_parser_parse_url(const char *buf, size_t buflen, int is_connect, case s_req_server_with_at: found_at = 1; - /* FALLTROUGH */ + /* fall through */ case s_req_server: uf = UF_HOST; break; @@ -2367,7 +2410,7 @@ http_parser_parse_url(const char *buf, size_t buflen, int is_connect, continue; } - u->field_data[uf].off = p - buf; + u->field_data[uf].off = (uint16_t)(p - buf); u->field_data[uf].len = 1; u->field_set |= (1 << uf); @@ -2376,7 +2419,12 @@ http_parser_parse_url(const char *buf, size_t buflen, int is_connect, /* host must be present if there is a schema */ /* parsing http:///toto will fail */ - if ((u->field_set & ((1 << UF_SCHEMA) | (1 << UF_HOST))) != 0) { + if ((u->field_set & (1 << UF_SCHEMA)) && + (u->field_set & (1 << UF_HOST)) == 0) { + return 1; + } + + if (u->field_set & (1 << UF_HOST)) { if (http_parse_host(buf, u, found_at) != 0) { return 1; } @@ -2388,12 +2436,27 @@ http_parser_parse_url(const char *buf, size_t buflen, int is_connect, } if (u->field_set & (1 << UF_PORT)) { - /* Don't bother with endp; we've already validated the string */ - unsigned long v = strtoul(buf + u->field_data[UF_PORT].off, NULL, 10); - - /* Ports have a max value of 2^16 */ - if (v > 0xffff) { - return 1; + uint16_t off; + uint16_t len; + const char* p; + const char* end; + unsigned long v; + + off = u->field_data[UF_PORT].off; + len = u->field_data[UF_PORT].len; + end = buf + off + len; + + /* NOTE: The characters are already validated and are in the [0-9] range */ + assert(off + len <= buflen && "Port number overflow"); + v = 0; + for (p = buf + off; p < end; p++) { + v *= 10; + v += *p - '0'; + + /* Ports have a max value of 2^16 */ + if (v > 0xffff) { + return 1; + } } u->port = (uint16_t) v; @@ -2410,6 +2473,7 @@ http_parser_pause(http_parser *parser, int paused) { */ if (HTTP_PARSER_ERRNO(parser) == HPE_OK || HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) { + uint32_t nread = parser->nread; /* used by the SET_ERRNO macro */ SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK); } else { assert(0 && "Attempting to pause parser in error state"); @@ -2427,3 +2491,8 @@ http_parser_version(void) { HTTP_PARSER_VERSION_MINOR * 0x00100 | HTTP_PARSER_VERSION_PATCH * 0x00001; } + +void +http_parser_set_max_header_size(uint32_t size) { + max_header_size = size; +} diff --git a/http-parser/http_parser.h b/http-parser/http_parser.h index eb71bf9..16b5281 100644 --- a/http-parser/http_parser.h +++ b/http-parser/http_parser.h @@ -26,13 +26,13 @@ extern "C" { /* Also update SONAME in the Makefile whenever you change these. */ #define HTTP_PARSER_VERSION_MAJOR 2 -#define HTTP_PARSER_VERSION_MINOR 5 -#define HTTP_PARSER_VERSION_PATCH 0 +#define HTTP_PARSER_VERSION_MINOR 9 +#define HTTP_PARSER_VERSION_PATCH 2 -#include -#if defined(_WIN32) && !defined(__MINGW32__) && (!defined(_MSC_VER) || _MSC_VER<1600) -#include #include +#if defined(_WIN32) && !defined(__MINGW32__) && \ + (!defined(_MSC_VER) || _MSC_VER<1600) && !defined(__WINE__) +#include typedef __int8 int8_t; typedef unsigned __int8 uint8_t; typedef __int16 int16_t; @@ -76,6 +76,11 @@ typedef struct http_parser_settings http_parser_settings; * HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: * chunked' headers that indicate the presence of a body. * + * Returning `2` from on_headers_complete will tell parser that it should not + * expect neither a body nor any futher responses on this connection. This is + * useful for handling responses to a CONNECT request which may not contain + * `Upgrade` or `Connection: upgrade` headers. + * * http_data_cb does not return data chunks. It will be called arbitrarily * many times for each string. E.G. you might get 10 callbacks for "on_url" * each providing just a few characters more data. @@ -84,6 +89,76 @@ typedef int (*http_data_cb) (http_parser*, const char *at, size_t length); typedef int (*http_cb) (http_parser*); +/* Status Codes */ +#define HTTP_STATUS_MAP(XX) \ + XX(100, CONTINUE, Continue) \ + XX(101, SWITCHING_PROTOCOLS, Switching Protocols) \ + XX(102, PROCESSING, Processing) \ + XX(200, OK, OK) \ + XX(201, CREATED, Created) \ + XX(202, ACCEPTED, Accepted) \ + XX(203, NON_AUTHORITATIVE_INFORMATION, Non-Authoritative Information) \ + XX(204, NO_CONTENT, No Content) \ + XX(205, RESET_CONTENT, Reset Content) \ + XX(206, PARTIAL_CONTENT, Partial Content) \ + XX(207, MULTI_STATUS, Multi-Status) \ + XX(208, ALREADY_REPORTED, Already Reported) \ + XX(226, IM_USED, IM Used) \ + XX(300, MULTIPLE_CHOICES, Multiple Choices) \ + XX(301, MOVED_PERMANENTLY, Moved Permanently) \ + XX(302, FOUND, Found) \ + XX(303, SEE_OTHER, See Other) \ + XX(304, NOT_MODIFIED, Not Modified) \ + XX(305, USE_PROXY, Use Proxy) \ + XX(307, TEMPORARY_REDIRECT, Temporary Redirect) \ + XX(308, PERMANENT_REDIRECT, Permanent Redirect) \ + XX(400, BAD_REQUEST, Bad Request) \ + XX(401, UNAUTHORIZED, Unauthorized) \ + XX(402, PAYMENT_REQUIRED, Payment Required) \ + XX(403, FORBIDDEN, Forbidden) \ + XX(404, NOT_FOUND, Not Found) \ + XX(405, METHOD_NOT_ALLOWED, Method Not Allowed) \ + XX(406, NOT_ACCEPTABLE, Not Acceptable) \ + XX(407, PROXY_AUTHENTICATION_REQUIRED, Proxy Authentication Required) \ + XX(408, REQUEST_TIMEOUT, Request Timeout) \ + XX(409, CONFLICT, Conflict) \ + XX(410, GONE, Gone) \ + XX(411, LENGTH_REQUIRED, Length Required) \ + XX(412, PRECONDITION_FAILED, Precondition Failed) \ + XX(413, PAYLOAD_TOO_LARGE, Payload Too Large) \ + XX(414, URI_TOO_LONG, URI Too Long) \ + XX(415, UNSUPPORTED_MEDIA_TYPE, Unsupported Media Type) \ + XX(416, RANGE_NOT_SATISFIABLE, Range Not Satisfiable) \ + XX(417, EXPECTATION_FAILED, Expectation Failed) \ + XX(421, MISDIRECTED_REQUEST, Misdirected Request) \ + XX(422, UNPROCESSABLE_ENTITY, Unprocessable Entity) \ + XX(423, LOCKED, Locked) \ + XX(424, FAILED_DEPENDENCY, Failed Dependency) \ + XX(426, UPGRADE_REQUIRED, Upgrade Required) \ + XX(428, PRECONDITION_REQUIRED, Precondition Required) \ + XX(429, TOO_MANY_REQUESTS, Too Many Requests) \ + XX(431, REQUEST_HEADER_FIELDS_TOO_LARGE, Request Header Fields Too Large) \ + XX(451, UNAVAILABLE_FOR_LEGAL_REASONS, Unavailable For Legal Reasons) \ + XX(500, INTERNAL_SERVER_ERROR, Internal Server Error) \ + XX(501, NOT_IMPLEMENTED, Not Implemented) \ + XX(502, BAD_GATEWAY, Bad Gateway) \ + XX(503, SERVICE_UNAVAILABLE, Service Unavailable) \ + XX(504, GATEWAY_TIMEOUT, Gateway Timeout) \ + XX(505, HTTP_VERSION_NOT_SUPPORTED, HTTP Version Not Supported) \ + XX(506, VARIANT_ALSO_NEGOTIATES, Variant Also Negotiates) \ + XX(507, INSUFFICIENT_STORAGE, Insufficient Storage) \ + XX(508, LOOP_DETECTED, Loop Detected) \ + XX(510, NOT_EXTENDED, Not Extended) \ + XX(511, NETWORK_AUTHENTICATION_REQUIRED, Network Authentication Required) \ + +enum http_status + { +#define XX(num, name, string) HTTP_STATUS_##name = num, + HTTP_STATUS_MAP(XX) +#undef XX + }; + + /* Request Methods */ #define HTTP_METHOD_MAP(XX) \ XX(0, DELETE, DELETE) \ @@ -95,7 +170,7 @@ typedef int (*http_cb) (http_parser*); XX(5, CONNECT, CONNECT) \ XX(6, OPTIONS, OPTIONS) \ XX(7, TRACE, TRACE) \ - /* webdav */ \ + /* WebDAV */ \ XX(8, COPY, COPY) \ XX(9, LOCK, LOCK) \ XX(10, MKCOL, MKCOL) \ @@ -104,21 +179,30 @@ typedef int (*http_cb) (http_parser*); XX(13, PROPPATCH, PROPPATCH) \ XX(14, SEARCH, SEARCH) \ XX(15, UNLOCK, UNLOCK) \ + XX(16, BIND, BIND) \ + XX(17, REBIND, REBIND) \ + XX(18, UNBIND, UNBIND) \ + XX(19, ACL, ACL) \ /* subversion */ \ - XX(16, REPORT, REPORT) \ - XX(17, MKACTIVITY, MKACTIVITY) \ - XX(18, CHECKOUT, CHECKOUT) \ - XX(19, MERGE, MERGE) \ + XX(20, REPORT, REPORT) \ + XX(21, MKACTIVITY, MKACTIVITY) \ + XX(22, CHECKOUT, CHECKOUT) \ + XX(23, MERGE, MERGE) \ /* upnp */ \ - XX(20, MSEARCH, M-SEARCH) \ - XX(21, NOTIFY, NOTIFY) \ - XX(22, SUBSCRIBE, SUBSCRIBE) \ - XX(23, UNSUBSCRIBE, UNSUBSCRIBE) \ + XX(24, MSEARCH, M-SEARCH) \ + XX(25, NOTIFY, NOTIFY) \ + XX(26, SUBSCRIBE, SUBSCRIBE) \ + XX(27, UNSUBSCRIBE, UNSUBSCRIBE) \ /* RFC-5789 */ \ - XX(24, PATCH, PATCH) \ - XX(25, PURGE, PURGE) \ + XX(28, PATCH, PATCH) \ + XX(29, PURGE, PURGE) \ /* CalDAV */ \ - XX(26, MKCALENDAR, MKCALENDAR) \ + XX(30, MKCALENDAR, MKCALENDAR) \ + /* RFC-2068, section 19.6.1.2 */ \ + XX(31, LINK, LINK) \ + XX(32, UNLINK, UNLINK) \ + /* icecast */ \ + XX(33, SOURCE, SOURCE) \ enum http_method { @@ -140,11 +224,12 @@ enum flags , F_TRAILING = 1 << 4 , F_UPGRADE = 1 << 5 , F_SKIPBODY = 1 << 6 + , F_CONTENTLENGTH = 1 << 7 }; /* Map for errno-related constants - * + * * The provided argument should be a macro that takes 2 arguments. */ #define HTTP_ERRNO_MAP(XX) \ @@ -182,6 +267,8 @@ enum flags XX(INVALID_HEADER_TOKEN, "invalid character in header") \ XX(INVALID_CONTENT_LENGTH, \ "invalid character in content-length header") \ + XX(UNEXPECTED_CONTENT_LENGTH, \ + "unexpected content-length header") \ XX(INVALID_CHUNK_SIZE, \ "invalid character in chunk size header") \ XX(INVALID_CONSTANT, "invalid constant string") \ @@ -206,10 +293,11 @@ enum http_errno { struct http_parser { /** PRIVATE **/ unsigned int type : 2; /* enum http_parser_type */ - unsigned int flags : 7; /* F_* values from 'flags' enum; semi-public */ + unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */ unsigned int state : 7; /* enum state from http_parser.c */ - unsigned int header_state : 8; /* enum header_state from http_parser.c */ - unsigned int index : 8; /* index into current matcher */ + unsigned int header_state : 7; /* enum header_state from http_parser.c */ + unsigned int index : 7; /* index into current matcher */ + unsigned int lenient_http_headers : 1; uint32_t nread; /* # bytes read in various scenarios */ uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */ @@ -319,12 +407,18 @@ int http_should_keep_alive(const http_parser *parser); /* Returns a string version of the HTTP method. */ const char *http_method_str(enum http_method m); +/* Returns a string version of the HTTP status code. */ +const char *http_status_str(enum http_status s); + /* Return a string name of the given error */ const char *http_errno_name(enum http_errno err); /* Return a string description of the given error */ const char *http_errno_description(enum http_errno err); +/* Initialize all http_parser_url members to 0 */ +void http_parser_url_init(struct http_parser_url *u); + /* Parse a URL; return nonzero on failure */ int http_parser_parse_url(const char *buf, size_t buflen, int is_connect, @@ -336,6 +430,9 @@ void http_parser_pause(http_parser *parser, int paused); /* Checks if this is the final chunk of the body. */ int http_body_is_final(const http_parser *parser); +/* Change the maximum header size provided at compile time. */ +void http_parser_set_max_header_size(uint32_t size); + #ifdef __cplusplus } #endif diff --git a/http-parser/test.c b/http-parser/test.c index 4c00571..0140a18 100644 --- a/http-parser/test.c +++ b/http-parser/test.c @@ -27,9 +27,7 @@ #include #if defined(__APPLE__) -# undef strlcat # undef strlncpy -# undef strlcpy #endif /* defined(__APPLE__) */ #undef TRUE @@ -43,7 +41,9 @@ #define MIN(a,b) ((a) < (b) ? (a) : (b)) -static http_parser *parser; +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(*x)) + +static http_parser parser; struct message { const char *name; // for debugging purposes @@ -78,6 +78,7 @@ struct message { int message_begin_cb_called; int headers_complete_cb_called; int message_complete_cb_called; + int status_cb_called; int message_complete_on_eof; int body_is_final; }; @@ -152,10 +153,10 @@ const struct message requests[] = ,.body= "" } -#define DUMBFUCK 2 -, {.name= "dumbfuck" +#define DUMBLUCK 2 +, {.name= "dumbluck" ,.type= HTTP_REQUEST - ,.raw= "GET /dumbfuck HTTP/1.1\r\n" + ,.raw= "GET /dumbluck HTTP/1.1\r\n" "aaaaaaaaaaaaa:++++++++++\r\n" "\r\n" ,.should_keep_alive= TRUE @@ -165,8 +166,8 @@ const struct message requests[] = ,.method= HTTP_GET ,.query_string= "" ,.fragment= "" - ,.request_path= "/dumbfuck" - ,.request_url= "/dumbfuck" + ,.request_path= "/dumbluck" + ,.request_url= "/dumbluck" ,.num_headers= 1 ,.headers= { { "aaaaaaaaaaaaa", "++++++++++" } @@ -370,13 +371,13 @@ const struct message requests[] = ,.chunk_lengths= { 5, 6 } } -#define CHUNKED_W_BULLSHIT_AFTER_LENGTH 11 -, {.name= "with bullshit after the length" +#define CHUNKED_W_NONSENSE_AFTER_LENGTH 11 +, {.name= "with nonsense after the length" ,.type= HTTP_REQUEST - ,.raw= "POST /chunked_w_bullshit_after_length HTTP/1.1\r\n" + ,.raw= "POST /chunked_w_nonsense_after_length HTTP/1.1\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" - "5; ihatew3;whatthefuck=aretheseparametersfor\r\nhello\r\n" + "5; ilovew3;whattheluck=aretheseparametersfor\r\nhello\r\n" "6; blahblah; blah\r\n world\r\n" "0\r\n" "\r\n" @@ -387,8 +388,8 @@ const struct message requests[] = ,.method= HTTP_POST ,.query_string= "" ,.fragment= "" - ,.request_path= "/chunked_w_bullshit_after_length" - ,.request_url= "/chunked_w_bullshit_after_length" + ,.request_path= "/chunked_w_nonsense_after_length" + ,.request_url= "/chunked_w_nonsense_after_length" ,.num_headers= 1 ,.headers= { { "Transfer-Encoding", "chunked" } @@ -1101,7 +1102,97 @@ const struct message requests[] = ,.body= "" } -, {.name= NULL } /* sentinel */ +/* Examples from the Internet draft for LINK/UNLINK methods: + * https://tools.ietf.org/id/draft-snell-link-method-01.html#rfc.section.5 + */ + +#define LINK_REQUEST 40 +, {.name = "link request" + ,.type= HTTP_REQUEST + ,.raw= "LINK /images/my_dog.jpg HTTP/1.1\r\n" + "Host: example.com\r\n" + "Link: ; rel=\"tag\"\r\n" + "Link: ; rel=\"tag\"\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_LINK + ,.request_path= "/images/my_dog.jpg" + ,.request_url= "/images/my_dog.jpg" + ,.query_string= "" + ,.fragment= "" + ,.num_headers= 3 + ,.headers= { { "Host", "example.com" } + , { "Link", "; rel=\"tag\"" } + , { "Link", "; rel=\"tag\"" } + } + ,.body= "" + } + +#define UNLINK_REQUEST 41 +, {.name = "unlink request" + ,.type= HTTP_REQUEST + ,.raw= "UNLINK /images/my_dog.jpg HTTP/1.1\r\n" + "Host: example.com\r\n" + "Link: ; rel=\"tag\"\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_UNLINK + ,.request_path= "/images/my_dog.jpg" + ,.request_url= "/images/my_dog.jpg" + ,.query_string= "" + ,.fragment= "" + ,.num_headers= 2 + ,.headers= { { "Host", "example.com" } + , { "Link", "; rel=\"tag\"" } + } + ,.body= "" + } + +#define SOURCE_REQUEST 42 +, {.name = "source request" + ,.type= HTTP_REQUEST + ,.raw= "SOURCE /music/sweet/music HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.method= HTTP_SOURCE + ,.request_path= "/music/sweet/music" + ,.request_url= "/music/sweet/music" + ,.query_string= "" + ,.fragment= "" + ,.num_headers= 1 + ,.headers= { { "Host", "example.com" } } + ,.body= "" + } + +#define SOURCE_ICE_REQUEST 42 +, {.name = "source request" + ,.type= HTTP_REQUEST + ,.raw= "SOURCE /music/sweet/music ICE/1.0\r\n" + "Host: example.com\r\n" + "\r\n" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 0 + ,.method= HTTP_SOURCE + ,.request_path= "/music/sweet/music" + ,.request_url= "/music/sweet/music" + ,.query_string= "" + ,.fragment= "" + ,.num_headers= 1 + ,.headers= { { "Host", "example.com" } } + ,.body= "" + } }; /* * R E S P O N S E S * */ @@ -1693,7 +1784,192 @@ const struct message responses[] = ,.body= "" } -, {.name= NULL } /* sentinel */ +#define CONTENT_LENGTH_X 21 +, {.name= "Content-Length-X" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Content-Length-X: 0\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "OK\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 2 + ,.headers= { { "Content-Length-X", "0" } + , { "Transfer-Encoding", "chunked" } + } + ,.body= "OK" + ,.num_chunks_complete= 2 + ,.chunk_lengths= { 2 } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER 22 +, {.name= "HTTP 101 response with Upgrade header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "\r\n" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.upgrade= "proto" + ,.num_headers= 2 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER_AND_CONTENT_LENGTH 23 +, {.name= "HTTP 101 response with Upgrade and Content-Length header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Content-Length: 4\r\n" + "\r\n" + "body" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.body= "body" + ,.upgrade= "proto" + ,.num_headers= 3 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Content-Length", "4" } + } + } + +#define HTTP_101_RESPONSE_WITH_UPGRADE_HEADER_AND_TRANSFER_ENCODING 24 +, {.name= "HTTP 101 response with Upgrade and Transfer-Encoding header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "bo\r\n" + "2\r\n" + "dy\r\n" + "0\r\n" + "\r\n" + "proto" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 101 + ,.response_status= "Switching Protocols" + ,.body= "body" + ,.upgrade= "proto" + ,.num_headers= 3 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Transfer-Encoding", "chunked" } + } + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 2, 2 } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER 25 +, {.name= "HTTP 200 response with Upgrade header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "\r\n" + "body" + ,.should_keep_alive= FALSE + ,.message_complete_on_eof= TRUE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.body= "body" + ,.upgrade= NULL + ,.num_headers= 2 + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER_AND_CONTENT_LENGTH 26 +, {.name= "HTTP 200 response with Upgrade and Content-Length header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Content-Length: 4\r\n" + "\r\n" + "body" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 3 + ,.body= "body" + ,.upgrade= NULL + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Content-Length", "4" } + } + } + +#define HTTP_200_RESPONSE_WITH_UPGRADE_HEADER_AND_TRANSFER_ENCODING 27 +, {.name= "HTTP 200 response with Upgrade and Transfer-Encoding header" + ,.type= HTTP_RESPONSE + ,.raw= "HTTP/1.1 200 OK\r\n" + "Connection: upgrade\r\n" + "Upgrade: h2c\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "bo\r\n" + "2\r\n" + "dy\r\n" + "0\r\n" + "\r\n" + ,.should_keep_alive= TRUE + ,.message_complete_on_eof= FALSE + ,.http_major= 1 + ,.http_minor= 1 + ,.status_code= 200 + ,.response_status= "OK" + ,.num_headers= 3 + ,.body= "body" + ,.upgrade= NULL + ,.headers= + { { "Connection", "upgrade" } + , { "Upgrade", "h2c" } + , { "Transfer-Encoding", "chunked" } + } + ,.num_chunks_complete= 3 + ,.chunk_lengths= { 2, 2 } + } }; /* strnlen() is a POSIX.2008 addition. Can't rely on it being available so @@ -1733,12 +2009,6 @@ strlncat(char *dst, size_t len, const char *src, size_t n) return slen + dlen; } -size_t -strlcat(char *dst, const char *src, size_t len) -{ - return strlncat(dst, len, src, (size_t) -1); -} - size_t strlncpy(char *dst, size_t len, const char *src, size_t n) { @@ -1757,16 +2027,10 @@ strlncpy(char *dst, size_t len, const char *src, size_t n) return slen; } -size_t -strlcpy(char *dst, const char *src, size_t len) -{ - return strlncpy(dst, len, src, (size_t) -1); -} - int request_url_cb (http_parser *p, const char *buf, size_t len) { - assert(p == parser); + assert(p == &parser); strlncat(messages[num_messages].request_url, sizeof(messages[num_messages].request_url), buf, @@ -1777,7 +2041,7 @@ request_url_cb (http_parser *p, const char *buf, size_t len) int header_field_cb (http_parser *p, const char *buf, size_t len) { - assert(p == parser); + assert(p == &parser); struct message *m = &messages[num_messages]; if (m->last_header_element != FIELD) @@ -1796,7 +2060,7 @@ header_field_cb (http_parser *p, const char *buf, size_t len) int header_value_cb (http_parser *p, const char *buf, size_t len) { - assert(p == parser); + assert(p == &parser); struct message *m = &messages[num_messages]; strlncat(m->headers[m->num_headers-1][1], @@ -1825,7 +2089,7 @@ check_body_is_final (const http_parser *p) int body_cb (http_parser *p, const char *buf, size_t len) { - assert(p == parser); + assert(p == &parser); strlncat(messages[num_messages].body, sizeof(messages[num_messages].body), buf, @@ -1839,7 +2103,7 @@ body_cb (http_parser *p, const char *buf, size_t len) int count_body_cb (http_parser *p, const char *buf, size_t len) { - assert(p == parser); + assert(p == &parser); assert(buf); messages[num_messages].body_size += len; check_body_is_final(p); @@ -1849,7 +2113,8 @@ count_body_cb (http_parser *p, const char *buf, size_t len) int message_begin_cb (http_parser *p) { - assert(p == parser); + assert(p == &parser); + assert(!messages[num_messages].message_begin_cb_called); messages[num_messages].message_begin_cb_called = TRUE; return 0; } @@ -1857,21 +2122,22 @@ message_begin_cb (http_parser *p) int headers_complete_cb (http_parser *p) { - assert(p == parser); - messages[num_messages].method = parser->method; - messages[num_messages].status_code = parser->status_code; - messages[num_messages].http_major = parser->http_major; - messages[num_messages].http_minor = parser->http_minor; + assert(p == &parser); + messages[num_messages].method = parser.method; + messages[num_messages].status_code = parser.status_code; + messages[num_messages].http_major = parser.http_major; + messages[num_messages].http_minor = parser.http_minor; messages[num_messages].headers_complete_cb_called = TRUE; - messages[num_messages].should_keep_alive = http_should_keep_alive(parser); + messages[num_messages].should_keep_alive = http_should_keep_alive(&parser); return 0; } int message_complete_cb (http_parser *p) { - assert(p == parser); - if (messages[num_messages].should_keep_alive != http_should_keep_alive(parser)) + assert(p == &parser); + if (messages[num_messages].should_keep_alive != + http_should_keep_alive(&parser)) { fprintf(stderr, "\n\n *** Error http_should_keep_alive() should have same " "value in both on_message_complete and on_headers_complete " @@ -1902,7 +2168,10 @@ message_complete_cb (http_parser *p) int response_status_cb (http_parser *p, const char *buf, size_t len) { - assert(p == parser); + assert(p == &parser); + + messages[num_messages].status_cb_called = TRUE; + strlncat(messages[num_messages].response_status, sizeof(messages[num_messages].response_status), buf, @@ -1913,7 +2182,7 @@ response_status_cb (http_parser *p, const char *buf, size_t len) int chunk_header_cb (http_parser *p) { - assert(p == parser); + assert(p == &parser); int chunk_idx = messages[num_messages].num_chunks; messages[num_messages].num_chunks++; if (chunk_idx < MAX_CHUNKS) { @@ -1926,7 +2195,7 @@ chunk_header_cb (http_parser *p) int chunk_complete_cb (http_parser *p) { - assert(p == parser); + assert(p == &parser); /* Here we want to verify that each chunk_header_cb is matched by a * chunk_complete_cb, so not only should the total number of calls to @@ -2121,6 +2390,20 @@ pause_chunk_complete_cb (http_parser *p) return chunk_complete_cb(p); } +int +connect_headers_complete_cb (http_parser *p) +{ + headers_complete_cb(p); + return 1; +} + +int +connect_message_complete_cb (http_parser *p) +{ + messages[num_messages].should_keep_alive = http_should_keep_alive(&parser); + return message_complete_cb(p); +} + static http_parser_settings settings_pause = {.on_message_begin = pause_message_begin_cb ,.on_header_field = pause_header_field_cb @@ -2160,6 +2443,19 @@ static http_parser_settings settings_count_body = ,.on_chunk_complete = chunk_complete_cb }; +static http_parser_settings settings_connect = + {.on_message_begin = message_begin_cb + ,.on_header_field = header_field_cb + ,.on_header_value = header_value_cb + ,.on_url = request_url_cb + ,.on_status = response_status_cb + ,.on_body = dontcall_body_cb + ,.on_headers_complete = connect_headers_complete_cb + ,.on_message_complete = connect_message_complete_cb + ,.on_chunk_header = chunk_header_cb + ,.on_chunk_complete = chunk_complete_cb + }; + static http_parser_settings settings_null = {.on_message_begin = 0 ,.on_header_field = 0 @@ -2177,30 +2473,15 @@ void parser_init (enum http_parser_type type) { num_messages = 0; - - assert(parser == NULL); - - parser = malloc(sizeof(http_parser)); - - http_parser_init(parser, type); - + http_parser_init(&parser, type); memset(&messages, 0, sizeof messages); - -} - -void -parser_free () -{ - assert(parser); - free(parser); - parser = NULL; } size_t parse (const char *buf, size_t len) { size_t nparsed; currently_parsing_eof = (len == 0); - nparsed = http_parser_execute(parser, &settings, buf, len); + nparsed = http_parser_execute(&parser, &settings, buf, len); return nparsed; } @@ -2208,7 +2489,7 @@ size_t parse_count_body (const char *buf, size_t len) { size_t nparsed; currently_parsing_eof = (len == 0); - nparsed = http_parser_execute(parser, &settings_count_body, buf, len); + nparsed = http_parser_execute(&parser, &settings_count_body, buf, len); return nparsed; } @@ -2219,7 +2500,15 @@ size_t parse_pause (const char *buf, size_t len) currently_parsing_eof = (len == 0); current_pause_parser = &s; - nparsed = http_parser_execute(parser, current_pause_parser, buf, len); + nparsed = http_parser_execute(&parser, current_pause_parser, buf, len); + return nparsed; +} + +size_t parse_connect (const char *buf, size_t len) +{ + size_t nparsed; + currently_parsing_eof = (len == 0); + nparsed = http_parser_execute(&parser, &settings_connect, buf, len); return nparsed; } @@ -2279,7 +2568,7 @@ do { \ } while(0) int -message_eq (int index, const struct message *expected) +message_eq (int index, int connect, const struct message *expected) { int i; struct message *m = &messages[index]; @@ -2292,10 +2581,13 @@ message_eq (int index, const struct message *expected) } else { MESSAGE_CHECK_NUM_EQ(expected, m, status_code); MESSAGE_CHECK_STR_EQ(expected, m, response_status); + assert(m->status_cb_called); } - MESSAGE_CHECK_NUM_EQ(expected, m, should_keep_alive); - MESSAGE_CHECK_NUM_EQ(expected, m, message_complete_on_eof); + if (!connect) { + MESSAGE_CHECK_NUM_EQ(expected, m, should_keep_alive); + MESSAGE_CHECK_NUM_EQ(expected, m, message_complete_on_eof); + } assert(m->message_begin_cb_called); assert(m->headers_complete_cb_called); @@ -2333,16 +2625,22 @@ message_eq (int index, const struct message *expected) MESSAGE_CHECK_NUM_EQ(expected, m, port); } - if (expected->body_size) { + if (connect) { + check_num_eq(m, "body_size", 0, m->body_size); + } else if (expected->body_size) { MESSAGE_CHECK_NUM_EQ(expected, m, body_size); } else { MESSAGE_CHECK_STR_EQ(expected, m, body); } - assert(m->num_chunks == m->num_chunks_complete); - MESSAGE_CHECK_NUM_EQ(expected, m, num_chunks_complete); - for (i = 0; i < m->num_chunks && i < MAX_CHUNKS; i++) { - MESSAGE_CHECK_NUM_EQ(expected, m, chunk_lengths[i]); + if (connect) { + check_num_eq(m, "num_chunks_complete", 0, m->num_chunks_complete); + } else { + assert(m->num_chunks == m->num_chunks_complete); + MESSAGE_CHECK_NUM_EQ(expected, m, num_chunks_complete); + for (i = 0; i < m->num_chunks && i < MAX_CHUNKS; i++) { + MESSAGE_CHECK_NUM_EQ(expected, m, chunk_lengths[i]); + } } MESSAGE_CHECK_NUM_EQ(expected, m, num_headers); @@ -2355,7 +2653,9 @@ message_eq (int index, const struct message *expected) if (!r) return 0; } - MESSAGE_CHECK_STR_EQ(expected, m, upgrade); + if (!connect) { + MESSAGE_CHECK_STR_EQ(expected, m, upgrade); + } return 1; } @@ -2392,7 +2692,7 @@ upgrade_message_fix(char *body, const size_t nread, const size_t nmsgs, ...) { va_list ap; size_t i; size_t off = 0; - + va_start(ap, nmsgs); for (i = 0; i < nmsgs; i++) { @@ -2428,7 +2728,7 @@ static void print_error (const char *raw, size_t error_location) { fprintf(stderr, "\n*** %s ***\n\n", - http_errno_description(HTTP_PARSER_ERRNO(parser))); + http_errno_description(HTTP_PARSER_ERRNO(&parser))); int this_line = 0, char_len = 0; size_t i, j, len = strlen(raw), error_location_line = 0; @@ -2918,6 +3218,77 @@ const struct url_test url_tests[] = ,.rv=1 /* s_dead */ } +, {.name="ipv6 address with Zone ID" + ,.url="http://[fe80::a%25eth0]/" + ,.is_connect=0 + ,.u= + {.field_set= (1<url, - strlen(test->url), + test->url ? strlen(test->url) : 0, test->is_connect, &u); @@ -3042,6 +3413,14 @@ test_method_str (void) assert(0 == strcmp("", http_method_str(1337))); } +void +test_status_str (void) +{ + assert(0 == strcmp("OK", http_status_str(HTTP_STATUS_OK))); + assert(0 == strcmp("Not Found", http_status_str(HTTP_STATUS_NOT_FOUND))); + assert(0 == strcmp("", http_status_str(1337))); +} + void test_message (const struct message *message) { @@ -3056,9 +3435,18 @@ test_message (const struct message *message) size_t msg2len = raw_len - msg1len; if (msg1len) { + assert(num_messages == 0); + messages[0].headers_complete_cb_called = FALSE; + read = parse(msg1, msg1len); - if (message->upgrade && parser->upgrade && num_messages > 0) { + if (!messages[0].headers_complete_cb_called && parser.nread != read) { + assert(parser.nread == read); + print_error(msg1, read); + abort(); + } + + if (message->upgrade && parser.upgrade && num_messages > 0) { messages[num_messages - 1].upgrade = msg1 + read; goto test; } @@ -3072,7 +3460,7 @@ test_message (const struct message *message) read = parse(msg2, msg2len); - if (message->upgrade && parser->upgrade) { + if (message->upgrade && parser.upgrade) { messages[num_messages - 1].upgrade = msg2 + read; goto test; } @@ -3096,9 +3484,7 @@ test_message (const struct message *message) abort(); } - if(!message_eq(0, message)) abort(); - - parser_free(); + if(!message_eq(0, 0, message)) abort(); } } @@ -3133,24 +3519,22 @@ test_message_count_body (const struct message *message) abort(); } - if(!message_eq(0, message)) abort(); - - parser_free(); + if(!message_eq(0, 0, message)) abort(); } void -test_simple (const char *buf, enum http_errno err_expected) +test_simple_type (const char *buf, + enum http_errno err_expected, + enum http_parser_type type) { - parser_init(HTTP_REQUEST); + parser_init(type); enum http_errno err; parse(buf, strlen(buf)); - err = HTTP_PARSER_ERRNO(parser); + err = HTTP_PARSER_ERRNO(&parser); parse(NULL, 0); - parser_free(); - /* In strict mode, allow us to pass with an unexpected HPE_STRICT as * long as the caller isn't expecting success. */ @@ -3165,6 +3549,185 @@ test_simple (const char *buf, enum http_errno err_expected) } } +void +test_simple (const char *buf, enum http_errno err_expected) +{ + test_simple_type(buf, err_expected, HTTP_REQUEST); +} + +void +test_invalid_header_content (int req, const char* str) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = str; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_HEADER_TOKEN); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in invalid header content test ***\n"); + abort(); +} + +void +test_invalid_header_field_content_error (int req) +{ + test_invalid_header_content(req, "Foo: F\01ailure"); + test_invalid_header_content(req, "Foo: B\02ar"); +} + +void +test_invalid_header_field (int req, const char* str) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = str; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_HEADER_TOKEN); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in invalid header token test ***\n"); + abort(); +} + +void +test_invalid_header_field_token_error (int req) +{ + test_invalid_header_field(req, "Fo@: Failure"); + test_invalid_header_field(req, "Foo\01\test: Bar"); +} + +void +test_double_content_length_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Content-Length: 0\r\nContent-Length: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_UNEXPECTED_CONTENT_LENGTH); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in double content-length test ***\n"); + abort(); +} + +void +test_chunked_content_length_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Transfer-Encoding: chunked\r\nContent-Length: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_UNEXPECTED_CONTENT_LENGTH); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in chunked content-length test ***\n"); + abort(); +} + +void +test_header_cr_no_lf_error (int req) +{ + http_parser parser; + http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE); + size_t parsed; + const char *buf; + buf = req ? + "GET / HTTP/1.1\r\n" : + "HTTP/1.1 200 OK\r\n"; + parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf)); + assert(parsed == strlen(buf)); + + buf = "Foo: 1\rBar: 1\r\n\r\n"; + size_t buflen = strlen(buf); + + parsed = http_parser_execute(&parser, &settings_null, buf, buflen); + if (parsed != buflen) { + assert(HTTP_PARSER_ERRNO(&parser) == HPE_LF_EXPECTED); + return; + } + + fprintf(stderr, + "\n*** Error expected but none in header whitespace test ***\n"); + abort(); +} + +void +test_no_overflow_parse_url (void) +{ + int rv; + struct http_parser_url u; + + http_parser_url_init(&u); + rv = http_parser_parse_url("http://example.com:8001", 22, 0, &u); + + if (rv != 0) { + fprintf(stderr, + "\n*** test_no_overflow_parse_url invalid return value=%d\n", + rv); + abort(); + } + + if (u.port != 800) { + fprintf(stderr, + "\n*** test_no_overflow_parse_url invalid port number=%d\n", + u.port); + abort(); + } +} + void test_header_overflow_error (int req) { @@ -3311,7 +3874,7 @@ test_multiple3 (const struct message *r1, const struct message *r2, const struct read = parse(total, strlen(total)); - if (parser->upgrade) { + if (parser.upgrade) { upgrade_message_fix(total, read, 3, r1, r2, r3); goto test; } @@ -3335,11 +3898,9 @@ test_multiple3 (const struct message *r1, const struct message *r2, const struct abort(); } - if (!message_eq(0, r1)) abort(); - if (message_count > 1 && !message_eq(1, r2)) abort(); - if (message_count > 2 && !message_eq(2, r3)) abort(); - - parser_free(); + if (!message_eq(0, 0, r1)) abort(); + if (message_count > 1 && !message_eq(1, 0, r2)) abort(); + if (message_count > 2 && !message_eq(2, 0, r3)) abort(); } /* SCAN through every possible breaking to make sure the @@ -3393,9 +3954,17 @@ test_scan (const struct message *r1, const struct message *r2, const struct mess strlncpy(buf3, sizeof(buf1), total+j, buf3_len); buf3[buf3_len] = 0; + assert(num_messages == 0); + messages[0].headers_complete_cb_called = FALSE; + read = parse(buf1, buf1_len); - if (parser->upgrade) goto test; + if (!messages[0].headers_complete_cb_called && parser.nread != read) { + print_error(buf1, read); + goto error; + } + + if (parser.upgrade) goto test; if (read != buf1_len) { print_error(buf1, read); @@ -3404,7 +3973,7 @@ test_scan (const struct message *r1, const struct message *r2, const struct mess read += parse(buf2, buf2_len); - if (parser->upgrade) goto test; + if (parser.upgrade) goto test; if (read != buf1_len + buf2_len) { print_error(buf2, read); @@ -3413,7 +3982,7 @@ test_scan (const struct message *r1, const struct message *r2, const struct mess read += parse(buf3, buf3_len); - if (parser->upgrade) goto test; + if (parser.upgrade) goto test; if (read != buf1_len + buf2_len + buf3_len) { print_error(buf3, read); @@ -3423,7 +3992,7 @@ test_scan (const struct message *r1, const struct message *r2, const struct mess parse(NULL, 0); test: - if (parser->upgrade) { + if (parser.upgrade) { upgrade_message_fix(total, read, 3, r1, r2, r3); } @@ -3433,22 +4002,20 @@ test_scan (const struct message *r1, const struct message *r2, const struct mess goto error; } - if (!message_eq(0, r1)) { + if (!message_eq(0, 0, r1)) { fprintf(stderr, "\n\nError matching messages[0] in test_scan.\n"); goto error; } - if (message_count > 1 && !message_eq(1, r2)) { + if (message_count > 1 && !message_eq(1, 0, r2)) { fprintf(stderr, "\n\nError matching messages[1] in test_scan.\n"); goto error; } - if (message_count > 2 && !message_eq(2, r3)) { + if (message_count > 2 && !message_eq(2, 0, r3)) { fprintf(stderr, "\n\nError matching messages[2] in test_scan.\n"); goto error; } - - parser_free(); } } } @@ -3512,7 +4079,7 @@ test_message_pause (const struct message *msg) // completion callback. if (messages[0].message_complete_cb_called && msg->upgrade && - parser->upgrade) { + parser.upgrade) { messages[0].upgrade = buf + nread; goto test; } @@ -3520,17 +4087,16 @@ test_message_pause (const struct message *msg) if (nread < buflen) { // Not much do to if we failed a strict-mode check - if (HTTP_PARSER_ERRNO(parser) == HPE_STRICT) { - parser_free(); + if (HTTP_PARSER_ERRNO(&parser) == HPE_STRICT) { return; } - assert (HTTP_PARSER_ERRNO(parser) == HPE_PAUSED); + assert (HTTP_PARSER_ERRNO(&parser) == HPE_PAUSED); } buf += nread; buflen -= nread; - http_parser_pause(parser, 0); + http_parser_pause(&parser, 0); } while (buflen > 0); nread = parse_pause(NULL, 0); @@ -3542,18 +4108,32 @@ test_message_pause (const struct message *msg) abort(); } - if(!message_eq(0, msg)) abort(); + if(!message_eq(0, 0, msg)) abort(); +} + +/* Verify that body and next message won't be parsed in responses to CONNECT */ +void +test_message_connect (const struct message *msg) +{ + char *buf = (char*) msg->raw; + size_t buflen = strlen(msg->raw); + + parser_init(msg->type); + + parse_connect(buf, buflen); + + if (num_messages != 1) { + printf("\n*** num_messages != 1 after testing '%s' ***\n\n", msg->name); + abort(); + } - parser_free(); + if(!message_eq(0, 1, msg)) abort(); } int main (void) { - parser = NULL; - int i, j, k; - int request_count; - int response_count; + unsigned i, j, k; unsigned long version; unsigned major; unsigned minor; @@ -3567,18 +4147,17 @@ main (void) printf("sizeof(http_parser) = %u\n", (unsigned int)sizeof(http_parser)); - for (request_count = 0; requests[request_count].name; request_count++); - for (response_count = 0; responses[response_count].name; response_count++); - //// API test_preserve_data(); test_parse_url(); test_method_str(); + test_status_str(); //// NREAD test_header_nread_value(); //// OVERFLOW CONDITIONS + test_no_overflow_parse_url(); test_header_overflow_error(HTTP_REQUEST); test_no_overflow_long_body(HTTP_REQUEST, 1000); @@ -3591,21 +4170,86 @@ main (void) test_header_content_length_overflow_error(); test_chunk_content_length_overflow_error(); + //// HEADER FIELD CONDITIONS + test_double_content_length_error(HTTP_REQUEST); + test_chunked_content_length_error(HTTP_REQUEST); + test_header_cr_no_lf_error(HTTP_REQUEST); + test_invalid_header_field_token_error(HTTP_REQUEST); + test_invalid_header_field_content_error(HTTP_REQUEST); + test_double_content_length_error(HTTP_RESPONSE); + test_chunked_content_length_error(HTTP_RESPONSE); + test_header_cr_no_lf_error(HTTP_RESPONSE); + test_invalid_header_field_token_error(HTTP_RESPONSE); + test_invalid_header_field_content_error(HTTP_RESPONSE); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length:\r\n" // empty + "\r\n", + HPE_INVALID_CONTENT_LENGTH, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 42 \r\n" // Note the surrounding whitespace. + "\r\n", + HPE_OK, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 4 2\r\n" + "\r\n", + HPE_INVALID_CONTENT_LENGTH, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 13 37\r\n" + "\r\n", + HPE_INVALID_CONTENT_LENGTH, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 42\r\n" + " Hello world!\r\n", + HPE_INVALID_CONTENT_LENGTH, + HTTP_REQUEST); + + test_simple_type( + "POST / HTTP/1.1\r\n" + "Content-Length: 42\r\n" + " \r\n", + HPE_OK, + HTTP_REQUEST); + //// RESPONSES - for (i = 0; i < response_count; i++) { + test_simple_type("HTP/1.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/01.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/11.1 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/1.01 200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("HTTP/1.1\t200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + test_simple_type("\rHTTP/1.1\t200 OK\r\n\r\n", HPE_INVALID_VERSION, HTTP_RESPONSE); + + for (i = 0; i < ARRAY_SIZE(responses); i++) { test_message(&responses[i]); } - for (i = 0; i < response_count; i++) { + for (i = 0; i < ARRAY_SIZE(responses); i++) { test_message_pause(&responses[i]); } - for (i = 0; i < response_count; i++) { + for (i = 0; i < ARRAY_SIZE(responses); i++) { + test_message_connect(&responses[i]); + } + + for (i = 0; i < ARRAY_SIZE(responses); i++) { if (!responses[i].should_keep_alive) continue; - for (j = 0; j < response_count; j++) { + for (j = 0; j < ARRAY_SIZE(responses); j++) { if (!responses[j].should_keep_alive) continue; - for (k = 0; k < response_count; k++) { + for (k = 0; k < ARRAY_SIZE(responses); k++) { test_multiple3(&responses[i], &responses[j], &responses[k]); } } @@ -3665,7 +4309,20 @@ main (void) /// REQUESTS + test_simple("GET / IHTTP/1.0\r\n\r\n", HPE_INVALID_CONSTANT); + test_simple("GET / ICE/1.0\r\n\r\n", HPE_INVALID_CONSTANT); test_simple("GET / HTP/1.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/01.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/11.1\r\n\r\n", HPE_INVALID_VERSION); + test_simple("GET / HTTP/1.01\r\n\r\n", HPE_INVALID_VERSION); + + test_simple("GET / HTTP/1.0\r\nHello: w\1rld\r\n\r\n", HPE_INVALID_HEADER_TOKEN); + test_simple("GET / HTTP/1.0\r\nHello: woooo\2rld\r\n\r\n", HPE_INVALID_HEADER_TOKEN); + + // Extended characters - see nodejs/test/parallel/test-http-headers-obstext.js + test_simple("GET / HTTP/1.1\r\n" + "Test: Düsseldorf\r\n", + HPE_OK); // Well-formed but incomplete test_simple("GET / HTTP/1.1\r\n" @@ -3690,7 +4347,12 @@ main (void) "MOVE", "PROPFIND", "PROPPATCH", + "SEARCH", "UNLOCK", + "BIND", + "REBIND", + "UNBIND", + "ACL", "REPORT", "MKACTIVITY", "CHECKOUT", @@ -3700,6 +4362,10 @@ main (void) "SUBSCRIBE", "UNSUBSCRIBE", "PATCH", + "PURGE", + "MKCALENDAR", + "LINK", + "UNLINK", 0 }; const char **this_method; for (this_method = all_methods; *this_method; this_method++) { @@ -3735,9 +4401,9 @@ main (void) "\r\n", HPE_INVALID_HEADER_TOKEN); - const char *dumbfuck2 = + const char *dumbluck2 = "GET / HTTP/1.1\r\n" - "X-SSL-Bullshit: -----BEGIN CERTIFICATE-----\r\n" + "X-SSL-Nonsense: -----BEGIN CERTIFICATE-----\r\n" "\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n" "\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n" "\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n" @@ -3770,7 +4436,7 @@ main (void) "\tRA==\r\n" "\t-----END CERTIFICATE-----\r\n" "\r\n"; - test_simple(dumbfuck2, HPE_OK); + test_simple(dumbluck2, HPE_OK); const char *corrupted_connection = "GET / HTTP/1.1\r\n" @@ -3804,19 +4470,19 @@ main (void) /* check to make sure our predefined requests are okay */ - for (i = 0; requests[i].name; i++) { + for (i = 0; i < ARRAY_SIZE(requests); i++) { test_message(&requests[i]); } - for (i = 0; i < request_count; i++) { + for (i = 0; i < ARRAY_SIZE(requests); i++) { test_message_pause(&requests[i]); } - for (i = 0; i < request_count; i++) { + for (i = 0; i < ARRAY_SIZE(requests); i++) { if (!requests[i].should_keep_alive) continue; - for (j = 0; j < request_count; j++) { + for (j = 0; j < ARRAY_SIZE(requests); j++) { if (!requests[j].should_keep_alive) continue; - for (k = 0; k < request_count; k++) { + for (k = 0; k < ARRAY_SIZE(requests); k++) { test_multiple3(&requests[i], &requests[j], &requests[k]); } } @@ -3837,7 +4503,7 @@ main (void) printf("request scan 3/4 "); test_scan( &requests[TWO_CHUNKS_MULT_ZERO_END] , &requests[CHUNKED_W_TRAILING_HEADERS] - , &requests[CHUNKED_W_BULLSHIT_AFTER_LENGTH] + , &requests[CHUNKED_W_NONSENSE_AFTER_LENGTH] ); printf("request scan 4/4 "); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..07ab246 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,30 @@ +include_directories(BEFORE ${CMAKE_SOURCE_DIR}/http-parser) + +add_definitions( -DQHTTPSERVER_EXPORT ) + +########### next target ############### + +SET(qhttpserver_LIB_SRCS + qhttpconnection.cpp + qhttprequest.cpp + qhttpresponse.cpp + qhttpserver.cpp + ../http-parser/http_parser.c + ../http-parser/contrib/url_parser.c +) + +add_library(qhttpserver SHARED ${qhttpserver_LIB_SRCS}) + +target_link_libraries(qhttpserver ${Qt6_LIBRARIES}) + +install(TARGETS qhttpserver DESTINATION ${LIB_INSTALL_DIR}) + +########### install files ############### +install(FILES + qhttpconnection.h + qhttprequest.h + qhttpresponse.h + qhttpserver.h + qhttpserverapi.h + qhttpserverfwd.h + DESTINATION include) diff --git a/src/qhttpconnection.cpp b/src/qhttpconnection.cpp index 0b9af6d..915cd9c 100644 --- a/src/qhttpconnection.cpp +++ b/src/qhttpconnection.cpp @@ -77,12 +77,12 @@ void QHttpConnection::socketDisconnected() deleteLater(); } + void QHttpConnection::invalidateRequest() { if (m_request && !m_request->successful()) { Q_EMIT m_request->end(); } - m_request = NULL; } @@ -125,7 +125,6 @@ void QHttpConnection::waitForBytesWritten() { m_socket->waitForBytesWritten(); } - void QHttpConnection::responseDone() { QHttpResponse *response = qobject_cast(QObject::sender()); @@ -184,11 +183,9 @@ int QHttpConnection::MessageBegin(http_parser *parser) // The QHttpRequest should not be parented to this, since it's memory // management is the responsibility of the user of the library. theConnection->m_request = new QHttpRequest(theConnection); - // Invalidate the request when it is deleted to prevent keep-alive requests // from calling a signal on a deleted object. connect(theConnection->m_request, SIGNAL(destroyed(QObject*)), theConnection, SLOT(invalidateRequest())); - return 0; } diff --git a/src/qhttprequest.h b/src/qhttprequest.h index 4f9cc3b..03ee0e4 100644 --- a/src/qhttprequest.h +++ b/src/qhttprequest.h @@ -46,7 +46,7 @@ class QHTTPSERVER_API QHttpRequest : public QObject Q_PROPERTY(HeaderHash headers READ headers) Q_PROPERTY(QString remoteAddress READ remoteAddress) Q_PROPERTY(quint16 remotePort READ remotePort) - Q_PROPERTY(QString method READ method) + Q_PROPERTY(QString method READ methodString) Q_PROPERTY(QUrl url READ url) Q_PROPERTY(QString path READ path) Q_PROPERTY(QString httpVersion READ httpVersion) diff --git a/src/qhttpresponse.cpp b/src/qhttpresponse.cpp index 39367a4..c86c770 100644 --- a/src/qhttpresponse.cpp +++ b/src/qhttpresponse.cpp @@ -74,7 +74,7 @@ void QHttpResponse::writeHeaders() if (m_finished) return; - foreach(const QString & name, m_headers.keys()) { + Q_FOREACH(const QString & name, m_headers.keys()) { QString value = m_headers[name]; if (name.compare("connection", Qt::CaseInsensitive) == 0) { m_sentConnectionHeader = true; diff --git a/src/qhttpresponse.h b/src/qhttpresponse.h index fcf5897..b3a784d 100644 --- a/src/qhttpresponse.h +++ b/src/qhttpresponse.h @@ -126,7 +126,7 @@ public Q_SLOTS: and the connection itself will be closed if this is the last response. - This will emit done() and queue this object + This will Q_EMIT done() and queue this object for deletion. For details see \ref memorymanagement. @param data Optional data to be written before finishing. */ void end(const QByteArray &data = "");