├── .github └── workflows │ └── action.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── experiment ├── httpx_back.nim └── httpy.nim ├── httpbeast.LICENSE ├── httpx.nimble ├── src ├── httpx.nim ├── httpx.nim.cfg └── httpx │ └── parser.nim └── tests ├── benchmark1.nim ├── benchmark2.nim ├── benchmark3.nim ├── helloworld.nim ├── megatest.nim ├── nim.cfg ├── start_server.nim └── test_core └── test_application.nim /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: Test Httpx 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: 15 | - ubuntu-latest 16 | - windows-latest 17 | version: 18 | - devel 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: jiro4989/setup-nim-action@v1 22 | with: 23 | nim-version: ${{ matrix.version }} 24 | - name: Install Packages 25 | run: nimble install -y 26 | - name: Test command 27 | run: nimble tests 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache 2 | testresults 3 | .gitignore 4 | outputGotten.txt 5 | src/httpy.nim 6 | .gitignore 7 | .vscode/settings.json 8 | *.exe 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | cache: 4 | directories: 5 | - .cache 6 | 7 | matrix: 8 | include: 9 | # Build and test against the master (stable) and devel branches of Nim 10 | - os: linux 11 | env: CHANNEL=stable 12 | compiler: gcc 13 | 14 | # On OSX we only test against clang (gcc is mapped to clang by default) 15 | - os: osx 16 | env: CHANNEL=stable 17 | compiler: clang 18 | 19 | - os: windows 20 | env: CHANNEL=stable 21 | compiler: gcc 22 | 23 | 24 | allow_failures: 25 | # Ignore failures when building against the devel Nim branch 26 | # Also ignore OSX, due to very long build queue 27 | - os: windows 28 | 29 | fast_finish: true 30 | 31 | # BEGIN: Assuming you rely on external dependencies 32 | # addons: # This will only be executed on Linux 33 | # apt: 34 | # packages: 35 | # - libzip-dev 36 | 37 | # before_install: 38 | # # If you want to install an OSX Homebrew dependency 39 | # - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi 40 | # - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install libzip; fi 41 | # ## END: Assuming you rely on external dependencies 42 | 43 | install: 44 | - export CHOOSENIM_NO_ANALYTICS=1 45 | - curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh 46 | - sh init.sh -y 47 | - export PATH=~/.nimble/bin:$PATH 48 | - echo "export PATH=~/.nimble/bin:$PATH" >> ~/.profile 49 | - choosenim $CHANNEL 50 | 51 | script: 52 | - nimble install -y 53 | - nimble tests 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httpx 2 | This project is based on Dom96's perfect work on [httpbeast](https://github.com/dom96/httpbeast) and adds windows support(based on `wepoll` namely IOCP). 3 | 4 | It is also used by [prologue](https://github.com/planety/prologue). 5 | 6 | ## Installation 7 | 8 | ``` 9 | nimble install httpx 10 | ``` 11 | 12 | ## Notes 13 | 14 | Notes that multi-threads may be slower than single-thread! 15 | 16 | 17 | ## Usage 18 | 19 | ### Change server info name 20 | 21 | ``` 22 | -d:serverInfo:serverName 23 | ``` 24 | 25 | ### Enable threads 26 | 27 | ``` 28 | --threads:on 29 | ``` 30 | 31 | ## Hello world 32 | 33 | ```nim 34 | import options, asyncdispatch 35 | 36 | import httpx 37 | 38 | proc onRequest(req: Request): Future[void] = 39 | if req.httpMethod == some(HttpGet): 40 | case req.path.get() 41 | of "/": 42 | req.send("Hello World") 43 | else: 44 | req.send(Http404) 45 | 46 | run(onRequest) 47 | ``` 48 | 49 | ## Websocket support 50 | https://github.com/ringabout/websocketx 51 | 52 | ``` 53 | nimble install https://github.com/ringabout/websocketx 54 | ``` 55 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | strategy: 2 | maxParallel: 10 3 | matrix: 4 | Windows_stable_64bit: 5 | VM: 'windows-latest' 6 | UCPU: amd64 7 | CHANNEL: stable 8 | TEST_LANG: c 9 | Windows_devel_64bit: 10 | VM: 'windows-latest' 11 | UCPU: amd64 12 | CHANNEL: devel 13 | TEST_LANG: c 14 | Windows_cpp_devel_64bit: 15 | VM: 'windows-latest' 16 | UCPU: amd64 17 | CHANNEL: devel 18 | TEST_LANG: cpp 19 | Linux_stable_64bit: 20 | VM: 'ubuntu-latest' 21 | UCPU: amd64 22 | CHANNEL: stable 23 | TEST_LANG: c 24 | Linux_devel_64bit: 25 | VM: 'ubuntu-latest' 26 | UCPU: amd64 27 | CHANNEL: devel 28 | TEST_LANG: c 29 | Linux_cpp_devel_64bit: 30 | VM: 'ubuntu-latest' 31 | UCPU: amd64 32 | CHANNEL: devel 33 | TEST_LANG: cpp 34 | # MacOS_stable_64bit: 35 | # VM: 'macOS-latest' 36 | # UCPU: amd64 37 | # CHANNEL: stable 38 | # TEST_LANG: c 39 | # MacOS_devel_64bit: 40 | # VM: 'macOS-latest' 41 | # UCPU: amd64 42 | # CHANNEL: devel 43 | # TEST_LANG: c 44 | pool: 45 | vmImage: $(VM) 46 | 47 | steps: 48 | - task: CacheBeta@1 49 | displayName: 'cache Nim binaries' 50 | inputs: 51 | key: NimBinaries | $(Agent.OS) | $(CHANNEL) | $(UCPU) 52 | path: NimBinaries 53 | 54 | - task: CacheBeta@1 55 | displayName: 'cache MinGW-w64' 56 | inputs: 57 | key: mingwCache | 8_1_0 | $(UCPU) 58 | path: mingwCache 59 | condition: eq(variables['Agent.OS'], 'Windows_NT') 60 | 61 | - powershell: | 62 | Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 63 | displayName: 'long path support' 64 | condition: eq(variables['Agent.OS'], 'Windows_NT') 65 | - bash: | 66 | echo "PATH=${PATH}" 67 | set -e 68 | echo "Installing MinGW-w64" 69 | if [[ $UCPU == "i686" ]]; then 70 | MINGW_FILE="i686-8.1.0-release-posix-dwarf-rt_v6-rev0.7z" 71 | MINGW_URL="https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/8.1.0/threads-posix/dwarf/${MINGW_FILE}" 72 | MINGW_DIR="mingw32" 73 | else 74 | MINGW_FILE="x86_64-8.1.0-release-posix-seh-rt_v6-rev0.7z" 75 | MINGW_URL="https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win64/Personal%20Builds/mingw-builds/8.1.0/threads-posix/seh/${MINGW_FILE}" 76 | MINGW_DIR="mingw64" 77 | fi 78 | mkdir -p mingwCache 79 | pushd mingwCache 80 | if [[ ! -e "$MINGW_FILE" ]]; then 81 | rm -f *.7z 82 | curl -OLsS "$MINGW_URL" 83 | fi 84 | 7z x -y -bd "$MINGW_FILE" >/dev/null 85 | mkdir -p /c/custom 86 | mv "$MINGW_DIR" /c/custom/ 87 | popd 88 | # Workaround https://developercommunity.visualstudio.com/content/problem/891929/windows-2019-cygheap-base-mismatch-detected-git-ba.html 89 | echo "##vso[task.prependpath]/usr/bin" 90 | echo "##vso[task.prependpath]/mingw64/bin" 91 | echo "##vso[task.setvariable variable=MINGW_DIR;]$MINGW_DIR" 92 | displayName: 'Install dependencies (Windows)' 93 | condition: eq(variables['Agent.OS'], 'Windows_NT') 94 | - powershell: | 95 | # export custom mingw PATH to other tasks 96 | echo "##vso[task.prependpath]c:\custom\$(MINGW_DIR)\bin" 97 | displayName: 'Mingw PATH (Windows)' 98 | condition: eq(variables['Agent.OS'], 'Windows_NT') 99 | - bash: | 100 | echo "PATH=${PATH}" 101 | export ncpu= 102 | case '$(Agent.OS)' in 103 | 'Linux') 104 | ncpu=$(nproc) 105 | ;; 106 | 'Darwin') 107 | ncpu=$(sysctl -n hw.ncpu) 108 | ;; 109 | 'Windows_NT') 110 | ncpu=$NUMBER_OF_PROCESSORS 111 | ;; 112 | esac 113 | [[ -z "$ncpu" || $ncpu -le 0 ]] && ncpu=1 114 | echo "Found ${ncpu} cores" 115 | echo "##vso[task.setvariable variable=ncpu;]$ncpu" 116 | displayName: 'Detecting number of cores' 117 | - bash: | 118 | echo "PATH=${PATH}" 119 | gcc -v 120 | export ucpu=${UCPU} 121 | if [ "${CHANNEL}" = stable ]; then 122 | BRANCH="v$(curl https://nim-lang.org/channels/stable)" 123 | else 124 | BRANCH="${CHANNEL}" 125 | fi 126 | mkdir -p NimBinaries 127 | pushd NimBinaries 128 | if [ ! -x "nim-${CHANNEL}/bin/nim" ]; then 129 | git clone -b "${BRANCH}" https://github.com/nim-lang/nim "nim-${CHANNEL}/" 130 | pushd "nim-${CHANNEL}" 131 | git clone --depth 1 https://github.com/nim-lang/csources csources/ 132 | pushd csources 133 | make -j $ncpu ucpu=${UCPU} CC=gcc 134 | popd 135 | rm -rf csources 136 | bin/nim c koch 137 | ./koch boot -d:release 138 | ./koch tools 139 | else 140 | pushd "nim-${CHANNEL}" 141 | git fetch origin "${BRANCH}" 142 | if [[ $(git merge FETCH_HEAD | grep -c "Already up to date.") -ne 1 ]]; then 143 | bin/nim c koch 144 | ./koch boot -d:release 145 | ./koch tools 146 | fi 147 | fi 148 | popd # exit nim-${CHANNEL} 149 | popd # exit NimBinaries 150 | displayName: 'Building Nim' 151 | - powershell: | 152 | echo "##vso[task.prependpath]$pwd\NimBinaries\nim-$(CHANNEL)\bin" 153 | displayName: 'Set env variable (Windows)' 154 | condition: eq(variables['Agent.OS'], 'Windows_NT') 155 | - bash: | 156 | echo "##vso[task.prependpath]$PWD/NimBinaries/nim-${CHANNEL}/bin" 157 | displayName: 'Set env variable (Posix)' 158 | condition: ne(variables['Agent.OS'], 'Windows_NT') 159 | - bash: | 160 | echo "PATH=${PATH}" 161 | nimble refresh 162 | nimble install -y 163 | displayName: 'Building the package dependencies' 164 | - bash: | 165 | echo "PATH=${PATH}" 166 | export ucpu=${UCPU} 167 | nimble tests 168 | displayName: 'Testing the package' 169 | -------------------------------------------------------------------------------- /experiment/httpx_back.nim: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2020 Dominik Picheta 3 | 4 | # Copyright 2020 Zeshen Xing 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | import net, nativesockets, os, httpcore, asyncdispatch, strutils, options, logging, times 20 | 21 | from deques import len 22 | 23 | import ioselectors 24 | 25 | import httpx/parser 26 | 27 | 28 | when defined(windows): 29 | import sets 30 | else: 31 | import posix 32 | from osproc import countProcessors 33 | 34 | 35 | export httpcore 36 | 37 | 38 | type 39 | FdKind = enum 40 | Server, Client, Dispatcher 41 | 42 | Data = object 43 | fdKind: FdKind ## Determines the fd kind (server, client, dispatcher) 44 | ## - Client specific data. 45 | ## A queue of data that needs to be sent when the FD becomes writeable. 46 | sendQueue: string 47 | ## The number of characters in `sendQueue` that have been sent already. 48 | bytesSent: int 49 | ## Big chunk of data read from client during request. 50 | data: string 51 | ## Determines whether `data` contains "\c\l\c\l". 52 | headersFinished: bool 53 | ## Determines position of the end of "\c\l\c\l". 54 | headersFinishPos: int 55 | ## The address that a `client` connects from. 56 | ip: string 57 | 58 | type 59 | Request* = object 60 | selector: Selector[Data] 61 | client*: SocketHandle 62 | # Determines where in the data buffer this request starts. 63 | # Only used for HTTP pipelining. 64 | start: int 65 | 66 | OnRequest* = proc (req: Request): Future[void] {.gcsafe.} 67 | 68 | Settings* = object 69 | port*: Port 70 | bindAddr*: string 71 | numThreads: int 72 | maxBody: int ## The maximum content-length that will be read for the body. 73 | 74 | const 75 | serverInfo {.strdefine.} = "Nim-HTTPX" 76 | clientBufSzie = 256 77 | 78 | var serverDate {.threadvar.}: string 79 | 80 | 81 | func initSettings*(port = Port(8080), 82 | bindAddr = "", 83 | numThreads = 0, 84 | maxBody: Natural = 8388608 85 | ): Settings = 86 | result = Settings( 87 | port: port, 88 | bindAddr: bindAddr, 89 | numThreads: numThreads, 90 | maxBody: maxBody 91 | ) 92 | 93 | func initData(fdKind: FdKind, ip = ""): Data = 94 | result = Data(fdKind: fdKind, 95 | sendQueue: "", 96 | bytesSent: 0, 97 | data: "", 98 | headersFinished: false, 99 | headersFinishPos: -1, ## By default we assume the fast case: end of data. 100 | ip: ip 101 | ) 102 | 103 | #[ API start ]# 104 | 105 | proc unsafeSend*(req: Request, data: string) {.inline.} = 106 | ## Sends the specified data on the request socket. 107 | ## 108 | ## This function can be called as many times as necessary. 109 | ## 110 | ## It does not check whether the socket is in a state 111 | ## that can be written so be careful when using it. 112 | if req.client notin req.selector: 113 | return 114 | 115 | req.selector.getData(req.client).sendQueue.add(data) 116 | req.selector.updateHandle(req.client, {Event.Read, Event.Write}) 117 | 118 | proc send*(req: Request, code: HttpCode, body: string, contentLength: Option[string], headers = "") {.inline.} = 119 | ## Responds with the specified HttpCode and body. 120 | ## 121 | ## **Warning:** This can only be called once in the OnRequest callback. 122 | 123 | if req.client notin req.selector: 124 | return 125 | 126 | template reqGetData(): var Data = 127 | req.selector.getData(req.client) 128 | 129 | assert reqGetData.headersFinished, "Selector not ready to send." 130 | 131 | let otherHeaders = 132 | if likely(headers.len != 0): 133 | "\c\L" & headers 134 | else: 135 | "" 136 | 137 | let text = 138 | if contentLength.isNone: 139 | ( 140 | "HTTP/1.1 $#\c\LContent-Length: $#\c\LServer: $#\c\LDate: $#$#\c\L\c\L$#" 141 | ) % [$code, $body.len, serverInfo, serverDate, otherHeaders, body] 142 | else: 143 | ( 144 | "HTTP/1.1 $#\c\LContent-Length: $#\c\LServer: $#\c\LDate: $#$#\c\L\c\L$#" 145 | ) % [$code, contentLength.get, serverInfo, serverDate, otherHeaders, body] 146 | 147 | reqGetData.sendQueue.add(text) 148 | req.selector.updateHandle(req.client, {Event.Read, Event.Write}) 149 | 150 | template send*(req: Request, code: HttpCode, body: string, headers = "") = 151 | ## Responds with the specified HttpCode and body. 152 | ## 153 | ## **Warning:** This can only be called once in the OnRequest callback. 154 | 155 | req.send(code, body, none(string), headers) 156 | 157 | proc send*(req: Request, code: HttpCode) = 158 | ## Responds with the specified HttpCode. The body of the response 159 | ## is the same as the HttpCode description. 160 | req.send(code, $code) 161 | 162 | proc send*(req: Request, body: string, code = Http200) {.inline.} = 163 | ## Sends a HTTP 200 OK response with the specified body. 164 | ## 165 | ## **Warning:** This can only be called once in the OnRequest callback. 166 | req.send(code, body) 167 | 168 | template acceptClient() = 169 | let (client, address) = fd.SocketHandle.accept 170 | if client == osInvalidSocket: 171 | let lastError = osLastError() 172 | 173 | when defined(posix): 174 | if lastError.int32 == EMFILE: 175 | warn("Ignoring EMFILE error: ", osErrorMsg(lastError)) 176 | return 177 | 178 | raiseOSError(lastError) 179 | setBlocking(client, false) 180 | selector.registerHandle(client, {Event.Read}, 181 | initData(Client, ip = address)) 182 | 183 | template closeClient(selector: Selector[Data], 184 | fd: SocketHandle|int, 185 | inLoop = true) = 186 | # TODO: Can POST body be sent with Connection: Close? 187 | 188 | selector.unregister(fd) 189 | close(fd.SocketHandle) 190 | logging.debug($fd & " is closed!") 191 | 192 | when inLoop: 193 | break 194 | else: 195 | return 196 | 197 | proc onRequestFutureComplete(theFut: Future[void], 198 | selector: Selector[Data], fd: int) = 199 | if theFut.failed: 200 | raise theFut.error 201 | 202 | template fastHeadersCheck(data: ptr Data): bool = 203 | let res = data.data[^1] == '\l' and data.data[^2] == '\c' and 204 | data.data[^3] == '\l' and data.data[^4] == '\c' 205 | if res: 206 | data.headersFinishPos = data.data.len 207 | res 208 | 209 | template methodNeedsBody(data: ptr Data): bool = 210 | # Only idempotent methods can be pipelined (GET/HEAD/PUT/DELETE), they 211 | # never need a body, so we just assume `start` at 0. 212 | let reqMthod = parseHttpMethod(data.data, start = 0) 213 | reqMthod.isSome and (reqMthod.get in {HttpPost, HttpPut, HttpConnect, HttpPatch}) 214 | 215 | proc slowHeadersCheck(data: ptr Data): bool = 216 | if unlikely(methodNeedsBody(data)): 217 | # Look for \c\l\c\l inside data. 218 | data.headersFinishPos = 0 219 | template ch(i: int): char = 220 | let pos = data.headersFinishPos + i 221 | if pos >= data.data.len: 222 | '\0' 223 | else: 224 | data.data[pos] 225 | 226 | while data.headersFinishPos < data.data.len: 227 | case ch(0) 228 | of '\c': 229 | if ch(1) == '\l' and ch(2) == '\c' and ch(3) == '\l': 230 | data.headersFinishPos.inc(4) 231 | return true 232 | else: 233 | discard 234 | inc data.headersFinishPos 235 | 236 | data.headersFinishPos = -1 237 | 238 | proc bodyInTransit(data: ptr Data, maxBody: int, overLimitation: var bool): bool = 239 | # get, head, put, delete 240 | assert methodNeedsBody(data), "Calling bodyInTransit now is inefficient." 241 | assert data.headersFinished 242 | 243 | overLimitation = false 244 | 245 | if data.headersFinishPos == -1: 246 | return false 247 | 248 | let trueLen = parseContentLength(data.data, start = 0) 249 | 250 | if trueLen > maxBody: 251 | overLimitation = true 252 | 253 | let bodyLen = data.data.len - data.headersFinishPos 254 | assert(not (bodyLen > trueLen)) 255 | result = bodyLen != trueLen 256 | 257 | proc validateRequest(req: Request): bool {.gcsafe.} 258 | 259 | proc processEvents(selector: Selector[Data], 260 | events: array[64, ReadyKey], count: int, 261 | onRequest: OnRequest, 262 | maxBody: int) = 263 | for i in 0 ..< count: 264 | let fd = events[i].fd 265 | var data: ptr Data = addr(getData(selector, fd)) 266 | # Handle error events first. 267 | if Event.Error in events[i].events: 268 | if isDisconnectionError({SocketFlag.SafeDisconn}, 269 | events[i].errorCode): 270 | closeClient(selector, fd) 271 | raiseOSError(events[i].errorCode) 272 | 273 | case data.fdKind 274 | of Server: 275 | if Event.Read in events[i].events: 276 | acceptClient() 277 | else: 278 | doAssert false, "Only Read events are expected for the server" 279 | of Dispatcher: 280 | # Run the dispatcher loop. 281 | when defined(posix): 282 | assert events[i].events == {Event.Read} 283 | asyncdispatch.poll(0) 284 | else: 285 | discard 286 | of Client: 287 | if Event.Read in events[i].events: 288 | var buf: array[clientBufSzie, char] 289 | # Read until EAGAIN. We take advantage of the fact that the client 290 | # will wait for a response after they send a request. So we can 291 | # comfortably continue reading until the message ends with \c\l 292 | # \c\l. 293 | var overLimitation = false 294 | while true: 295 | let ret = recv(fd.SocketHandle, addr buf[0], clientBufSzie, 0.cint) 296 | if ret == 0: 297 | closeClient(selector, fd) 298 | 299 | if ret == -1: 300 | # Error! 301 | let lastError = osLastError() 302 | 303 | when defined(posix): 304 | if lastError.int32 in {EWOULDBLOCK, EAGAIN}: 305 | break 306 | else: 307 | if lastError.int == WSAEWOULDBLOCK: 308 | break 309 | 310 | if isDisconnectionError({SocketFlag.SafeDisconn}, lastError): 311 | closeClient(selector, fd) 312 | raiseOSError(lastError) 313 | 314 | # Write buffer to our data. 315 | if not overLimitation: 316 | let origLen = data.data.len 317 | data.data.setLen(origLen + ret) 318 | for i in 0 ..< ret: 319 | data.data[origLen + i] = buf[i] 320 | 321 | if fastHeadersCheck(data) or slowHeadersCheck(data): 322 | # First line and headers for request received. 323 | data.headersFinished = true 324 | when not defined(release): 325 | if data.sendQueue.len != 0: 326 | logging.warn("sendQueue isn't empty.") 327 | if data.bytesSent != 0: 328 | logging.warn("bytesSent isn't empty.") 329 | 330 | let waitingForBody = methodNeedsBody(data) and bodyInTransit(data, maxBody, overLimitation) 331 | 332 | if likely(not waitingForBody): 333 | for start in parseRequests(data.data): 334 | # For pipelined requests, we need to reset this flag. 335 | data.headersFinished = true 336 | 337 | let request = Request( 338 | selector: selector, 339 | client: fd.SocketHandle, 340 | start: start 341 | ) 342 | 343 | template validateResponse() = 344 | data.headersFinished = false 345 | 346 | if validateRequest(request): 347 | let fut = onRequest(request) 348 | if fut != nil: 349 | fut.callback = 350 | proc (theFut: Future[void]) = 351 | onRequestFutureComplete(theFut, selector, fd) 352 | validateResponse() 353 | else: 354 | validateResponse() 355 | elif overLimitation: 356 | data.headersFinished = true 357 | 358 | let request = Request( 359 | selector: selector, 360 | client: fd.SocketHandle, 361 | start: 0 362 | ) 363 | 364 | request.send413(Http413, $Http413, none(string)) 365 | 366 | if ret != clientBufSzie: 367 | # Assume there is nothing else for us right now and break. 368 | break 369 | elif Event.Write in events[i].events: 370 | assert data.sendQueue.len > 0 371 | assert data.bytesSent < data.sendQueue.len 372 | # Write the sendQueue. 373 | 374 | let leftover = 375 | when defined(posix): 376 | data.sendQueue.len - data.bytesSent 377 | else: 378 | cint(data.sendQueue.len - data.bytesSent) 379 | 380 | let ret = send(fd.SocketHandle, addr data.sendQueue[data.bytesSent], 381 | leftover, 0) 382 | if ret == -1: 383 | # Error! 384 | let lastError = osLastError() 385 | 386 | when defined(posix): 387 | if lastError.int32 in {EWOULDBLOCK, EAGAIN}: 388 | break 389 | else: 390 | if lastError.int == WSAEWOULDBLOCK: 391 | break 392 | 393 | if isDisconnectionError({SocketFlag.SafeDisconn}, lastError): 394 | closeClient(selector, fd) 395 | raiseOSError(lastError) 396 | 397 | data.bytesSent.inc(ret) 398 | 399 | if data.sendQueue.len == data.bytesSent: 400 | data.bytesSent = 0 401 | data.sendQueue.setLen(0) 402 | data.data.setLen(0) 403 | selector.updateHandle(fd.SocketHandle, 404 | {Event.Read}) 405 | else: 406 | assert false 407 | 408 | proc updateDate(fd: AsyncFD): bool = 409 | result = false # Returning true signifies we want timer to stop. 410 | serverDate = now().utc().format("ddd, dd MMM yyyy HH:mm:ss 'GMT'") 411 | 412 | proc eventLoop(params: (OnRequest, Settings)) = 413 | let 414 | (onRequest, settings) = params 415 | selector = newSelector[Data]() 416 | server = newSocket() 417 | 418 | server.setSockOpt(OptReuseAddr, true) 419 | server.setSockOpt(OptReusePort, true) 420 | server.bindAddr(settings.port, settings.bindAddr) 421 | server.listen() 422 | server.getFd.setBlocking(false) 423 | selector.registerHandle(server.getFd, {Event.Read}, initData(Server)) 424 | 425 | # Set up timer to get current date/time. 426 | discard updateDate(0.AsyncFD) 427 | asyncdispatch.addTimer(1000, false, updateDate) 428 | 429 | when defined(posix): 430 | let disp = getGlobalDispatcher() 431 | selector.registerHandle(disp.getIoHandler.getFd, {Event.Read}, 432 | initData(Dispatcher)) 433 | 434 | var events: array[64, ReadyKey] 435 | while true: 436 | let ret = selector.selectInto(-1, events) 437 | processEvents(selector, events, ret, onRequest, settings.maxBody) 438 | 439 | # Ensure callbacks list doesn't grow forever in asyncdispatch. 440 | # See https://github.com/nim-lang/Nim/issues/7532. 441 | # Not processing callbacks can also lead to exceptions being silently 442 | # lost! 443 | if unlikely(asyncdispatch.getGlobalDispatcher().callbacks.len > 0): 444 | asyncdispatch.poll(0) 445 | else: 446 | var events: array[64, ReadyKey] 447 | while true: 448 | let ret = selector.selectInto(100, events) 449 | if ret > 0: 450 | processEvents(selector, events, ret, onRequest, settings.maxBody) 451 | asyncdispatch.poll(0) 452 | 453 | func httpMethod*(req: Request): Option[HttpMethod] {.inline.} = 454 | ## Parses the request's data to find the request HttpMethod. 455 | parseHttpMethod(req.selector.getData(req.client).data, req.start) 456 | 457 | func path*(req: Request): Option[string] {.inline.} = 458 | ## Parses the request's data to find the request target. 459 | if unlikely(req.client notin req.selector): 460 | return 461 | parsePath(req.selector.getData(req.client).data, req.start) 462 | 463 | func headers*(req: Request): Option[HttpHeaders] = 464 | ## Parses the request's data to get the headers. 465 | if unlikely(req.client notin req.selector): 466 | return 467 | parseHeaders(req.selector.getData(req.client).data, req.start) 468 | 469 | func body*(req: Request): Option[string] = 470 | ## Retrieves the body of the request. 471 | let pos = req.selector.getData(req.client).headersFinishPos 472 | if pos == -1: 473 | return none(string) 474 | result = some(req.selector.getData(req.client).data[pos .. ^1]) 475 | 476 | when not defined(release): 477 | let length = 478 | if req.headers.get.hasKey("Content-Length"): 479 | req.headers.get["Content-Length"].parseInt 480 | else: 481 | 0 482 | doAssert result.get.len == length 483 | 484 | func ip*(req: Request): string = 485 | ## Retrieves the IP address that the request was made from. 486 | req.selector.getData(req.client).ip 487 | 488 | proc forget*(req: Request) = 489 | ## Unregisters the underlying request's client socket from httpx's 490 | ## event loop. 491 | ## 492 | ## This is useful when you want to register ``req.client`` in your own 493 | ## event loop, for example when wanting to integrate httpx into a 494 | ## websocket library. 495 | req.selector.unregister(req.client) 496 | 497 | proc validateRequest(req: Request): bool = 498 | ## Handles protocol-mandated responses. 499 | ## 500 | ## Returns ``false`` when the request has been handled. 501 | result = true 502 | 503 | # From RFC7231: "When a request method is received 504 | # that is unrecognized or not implemented by an origin server, the 505 | # origin server SHOULD respond with the 501 (Not Implemented) status 506 | # code." 507 | if req.httpMethod.isNone: 508 | req.send(Http501) 509 | result = false 510 | 511 | proc run*(onRequest: OnRequest, settings: Settings) = 512 | ## Starts the HTTP server and calls `onRequest` for each request. 513 | ## 514 | ## The ``onRequest`` procedure returns a ``Future[void]`` type. But 515 | ## unlike most asynchronous procedures in Nim, it can return ``nil`` 516 | ## for better performance, when no async operations are needed. 517 | when not defined(windows): 518 | when compileOption("threads"): 519 | let numThreads = 520 | if settings.numThreads == 0: 521 | countProcessors() 522 | else: 523 | settings.numThreads 524 | else: 525 | let numThreads = 1 526 | 527 | logging.debug("Starting ", numThreads, " threads") 528 | 529 | if numThreads > 1: 530 | when compileOption("threads"): 531 | var threads = newSeq[Thread[(OnRequest, Settings)]](numThreads) 532 | for i in 0 ..< numThreads: 533 | createThread[(OnRequest, Settings)]( 534 | threads[i], eventLoop, (onRequest, settings) 535 | ) 536 | 537 | logging.debug("Listening on port ", 538 | settings.port) # This line is used in the tester to signal readiness. 539 | 540 | joinThreads(threads) 541 | else: 542 | doAssert false, "Please enable threads when numThreads is greater than 1!" 543 | else: 544 | eventLoop((onRequest, settings)) 545 | else: 546 | eventLoop((onRequest, settings)) 547 | logging.debug("Starting ", 1, " threads") 548 | 549 | proc run*(onRequest: OnRequest) {.inline.} = 550 | ## Starts the HTTP server with default settings. Calls `onRequest` for each 551 | ## request. 552 | ## 553 | ## See the other ``run`` proc for more info. 554 | run(onRequest, Settings(port: Port(8080), bindAddr: "")) 555 | 556 | when false: 557 | proc close*(port: Port) = 558 | ## Closes an httpx server that is running on the specified port. 559 | ## 560 | ## **NOTE:** This is not yet implemented. 561 | 562 | doAssert false 563 | # TODO: Figure out the best way to implement this. One way is to use async 564 | # events to signal our `eventLoop`. Maybe it would be better not to support 565 | # multiple servers running at the same time? 566 | -------------------------------------------------------------------------------- /experiment/httpy.nim: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2020 Dominik Picheta 3 | 4 | # Copyright 2020 Zeshen Xing 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | import net, nativesockets, os, httpcore, strutils 20 | 21 | import options, logging 22 | 23 | from deques import len 24 | 25 | import chronos 26 | 27 | import ioselectors 28 | 29 | 30 | when defined(windows): 31 | import sets 32 | else: 33 | import posix 34 | 35 | from osproc import countProcessors 36 | 37 | import times 38 | 39 | export httpcore 40 | 41 | import ../src/httpx/parser 42 | 43 | type 44 | FdKind = enum 45 | Server, Client, Dispatcher 46 | 47 | Data = object 48 | fdKind: FdKind ## Determines the fd kind (server, client, dispatcher) 49 | ## - Client specific data. 50 | ## A queue of data that needs to be sent when the FD becomes writeable. 51 | sendQueue: string 52 | ## The number of characters in `sendQueue` that have been sent already. 53 | bytesSent: int 54 | ## Big chunk of data read from client during request. 55 | data: string 56 | ## Determines whether `data` contains "\c\l\c\l". 57 | headersFinished: bool 58 | ## Determines position of the end of "\c\l\c\l". 59 | headersFinishPos: int 60 | ## The address that a `client` connects from. 61 | ip: string 62 | 63 | type 64 | Request* = object 65 | selector: Selector[Data] 66 | client*: SocketHandle 67 | # Determines where in the data buffer this request starts. 68 | # Only used for HTTP pipelining. 69 | start: int 70 | 71 | OnRequest* = proc (req: Request): Future[void] {.gcsafe.} 72 | 73 | Settings* = object 74 | port*: Port 75 | bindAddr*: string 76 | numThreads: int 77 | 78 | const 79 | serverInfo {.strdefine.} = "Nim-Httpx" 80 | 81 | proc initSettings*(port: Port = Port(8080), 82 | bindAddr: string = "", 83 | numThreads: int = 0): Settings {.inline.} = 84 | Settings( 85 | port: port, 86 | bindAddr: bindAddr, 87 | numThreads: numThreads, 88 | ) 89 | 90 | proc initData(fdKind: FdKind, ip = ""): Data {.inline.} = 91 | Data(fdKind: fdKind, 92 | sendQueue: "", 93 | bytesSent: 0, 94 | data: "", 95 | headersFinished: false, 96 | headersFinishPos: -1, ## By default we assume the fast case: end of data. 97 | ip: ip 98 | ) 99 | 100 | template handleAccept() = 101 | let (client, address) = fd.SocketHandle.accept() 102 | if client == osInvalidSocket: 103 | let lastError = osLastError() 104 | 105 | when defined(posix): 106 | if lastError.int32 == EMFILE: 107 | warn("Ignoring EMFILE error: ", osErrorMsg(lastError)) 108 | return 109 | 110 | raiseOSError(lastError) 111 | setBlocking(client, false) 112 | selector.registerHandle(client, {Event.Read}, 113 | initData(Client, ip=address)) 114 | 115 | template handleClientClosure(selector: Selector[Data], 116 | fd: SocketHandle|int, 117 | inLoop=true) = 118 | # TODO: Logging that the socket was closed. 119 | 120 | # TODO: Can POST body be sent with Connection: Close? 121 | 122 | selector.unregister(fd) 123 | fd.SocketHandle.close() 124 | when inLoop: 125 | break 126 | else: 127 | return 128 | 129 | proc onRequestFutureComplete(theFut: Future[void], 130 | selector: Selector[Data], fd: int) {.inline.} = 131 | if theFut.failed: 132 | raise theFut.error 133 | 134 | template fastHeadersCheck(data: ptr Data): untyped = 135 | (let res = data.data[^1] == '\l' and data.data[^2] == '\c' and 136 | data.data[^3] == '\l' and data.data[^4] == '\c'; 137 | if res: data.headersFinishPos = data.data.len; 138 | res) 139 | 140 | template methodNeedsBody(data: ptr Data): untyped = 141 | ( 142 | # Only idempotent methods can be pipelined (GET/HEAD/PUT/DELETE), they 143 | # never need a body, so we just assume `start` at 0. 144 | let m = parseHttpMethod(data.data, start=0); 145 | m.isSome() and m.get() in {HttpPost, HttpPut, HttpConnect, HttpPatch} 146 | ) 147 | 148 | proc slowHeadersCheck(data: ptr Data): bool {.inline.} = 149 | # TODO: See how this `unlikely` affects ASM. 150 | if unlikely(methodNeedsBody(data)): 151 | # Look for \c\l\c\l inside data. 152 | data.headersFinishPos = 0 153 | template ch(i): untyped = 154 | ( 155 | let pos = data.headersFinishPos+i; 156 | if pos >= data.data.len: '\0' else: data.data[pos] 157 | ) 158 | while data.headersFinishPos < data.data.len: 159 | case ch(0) 160 | of '\c': 161 | if ch(1) == '\l' and ch(2) == '\c' and ch(3) == '\l': 162 | data.headersFinishPos.inc(4) 163 | return true 164 | else: discard 165 | data.headersFinishPos.inc() 166 | 167 | data.headersFinishPos = -1 168 | 169 | proc bodyInTransit(data: ptr Data): bool = 170 | assert methodNeedsBody(data), "Calling bodyInTransit now is inefficient." 171 | assert data.headersFinished 172 | 173 | if data.headersFinishPos == -1: return false 174 | 175 | var trueLen = parseContentLength(data.data, start=0) 176 | 177 | let bodyLen = data.data.len - data.headersFinishPos 178 | assert(not (bodyLen > trueLen)) 179 | return bodyLen != trueLen 180 | 181 | proc validateRequest(req: Request): bool {.gcsafe.} 182 | 183 | proc processEvents(selector: Selector[Data], 184 | events: array[64, ReadyKey], count: int, 185 | onRequest: OnRequest) = 186 | for i in 0 ..< count: 187 | let fd = events[i].fd 188 | var data: ptr Data = addr(getData(selector, fd)) 189 | # Handle error events first. 190 | if Event.Error in events[i].events: 191 | if isDisconnectionError({SocketFlag.SafeDisconn}, 192 | events[i].errorCode): 193 | handleClientClosure(selector, fd) 194 | raiseOSError(events[i].errorCode) 195 | 196 | case data.fdKind 197 | of Server: 198 | if Event.Read in events[i].events: 199 | handleAccept() 200 | else: 201 | assert false, "Only Read events are expected for the server" 202 | of Dispatcher: 203 | # Run the dispatcher loop. 204 | assert events[i].events == {Event.Read} 205 | chronos.poll() 206 | of Client: 207 | if Event.Read in events[i].events: 208 | const size = 256 209 | var buf: array[size, char] 210 | # Read until EAGAIN. We take advantage of the fact that the client 211 | # will wait for a response after they send a request. So we can 212 | # comfortably continue reading until the message ends with \c\l 213 | # \c\l. 214 | while true: 215 | let ret = recv(fd.SocketHandle, addr buf[0], size, 0.cint) 216 | if ret == 0: 217 | handleClientClosure(selector, fd) 218 | 219 | if ret == -1: 220 | # Error! 221 | let lastError = osLastError() 222 | 223 | when defined(posix): 224 | if lastError.int32 in {EWOULDBLOCK, EAGAIN}: 225 | break 226 | else: 227 | if lastError.int == WSAEWOULDBLOCK: 228 | break 229 | 230 | if isDisconnectionError({SocketFlag.SafeDisconn}, lastError): 231 | handleClientClosure(selector, fd) 232 | raiseOSError(lastError) 233 | 234 | # Write buffer to our data. 235 | let origLen = data.data.len 236 | data.data.setLen(origLen + ret) 237 | for i in 0 ..< ret: 238 | data.data[origLen + i] = buf[i] 239 | 240 | if fastHeadersCheck(data) or slowHeadersCheck(data): 241 | # First line and headers for request received. 242 | data.headersFinished = true 243 | when not defined(release): 244 | if data.sendQueue.len != 0: 245 | logging.warn("sendQueue isn't empty.") 246 | if data.bytesSent != 0: 247 | logging.warn("bytesSent isn't empty.") 248 | 249 | let waitingForBody = methodNeedsBody(data) and bodyInTransit(data) 250 | if likely(not waitingForBody): 251 | for start in parseRequests(data.data): 252 | # For pipelined requests, we need to reset this flag. 253 | data.headersFinished = true 254 | 255 | let request = Request( 256 | selector: selector, 257 | client: fd.SocketHandle, 258 | start: start 259 | ) 260 | 261 | template validateResponse(): untyped = 262 | data.headersFinished = false 263 | 264 | if validateRequest(request): 265 | let fut = onRequest(request) 266 | if not fut.isNil: 267 | fut.callback = 268 | proc (arg: pointer = nil) {.gcsafe.} = 269 | onRequestFutureComplete(cast[Future[void]](arg), selector, fd) 270 | validateResponse() 271 | else: 272 | validateResponse() 273 | 274 | if ret != size: 275 | # Assume there is nothing else for us right now and break. 276 | break 277 | elif Event.Write in events[i].events: 278 | assert data.sendQueue.len > 0 279 | assert data.bytesSent < data.sendQueue.len 280 | # Write the sendQueue. 281 | 282 | let leftover = 283 | when defined(posix): 284 | data.sendQueue.len - data.bytesSent 285 | else: 286 | cint(data.sendQueue.len - data.bytesSent) 287 | 288 | let ret = send(fd.SocketHandle, addr data.sendQueue[data.bytesSent], 289 | leftover, 0) 290 | if ret == -1: 291 | # Error! 292 | let lastError = osLastError() 293 | 294 | when defined(posix): 295 | if lastError.int32 in {EWOULDBLOCK, EAGAIN}: 296 | break 297 | else: 298 | if lastError.int == WSAEWOULDBLOCK: 299 | break 300 | 301 | if isDisconnectionError({SocketFlag.SafeDisconn}, lastError): 302 | handleClientClosure(selector, fd) 303 | raiseOSError(lastError) 304 | 305 | data.bytesSent.inc(ret) 306 | 307 | if data.sendQueue.len == data.bytesSent: 308 | data.bytesSent = 0 309 | data.sendQueue.setLen(0) 310 | data.data.setLen(0) 311 | selector.updateHandle(fd.SocketHandle, 312 | {Event.Read}) 313 | else: 314 | assert false 315 | 316 | var serverDate {.threadvar.}: string 317 | 318 | proc updateDate(arg: pointer = nil) {.gcsafe.} = 319 | # result = false # Returning true signifies we want timer to stop. 320 | serverDate = now().utc().format("ddd, dd MMM yyyy HH:mm:ss 'GMT'") 321 | 322 | proc eventLoop(params: (OnRequest, Settings)) = 323 | let (onRequest, settings) = params 324 | 325 | let selector = newSelector[Data]() 326 | 327 | let server = newSocket() 328 | server.setSockOpt(OptReuseAddr, true) 329 | server.setSockOpt(OptReusePort, true) 330 | server.bindAddr(settings.port, settings.bindAddr) 331 | server.listen() 332 | server.getFd.setBlocking(false) 333 | selector.registerHandle(server.getFd, {Event.Read}, initData(Server)) 334 | 335 | let disp = getGlobalDispatcher() 336 | 337 | when defined(posix): 338 | selector.registerHandle(disp.getIoHandler.getFd, {Event.Read}, 339 | initData(Dispatcher)) 340 | else: 341 | for h in disp.handles.items: 342 | selector.registerHandle(nativesockets.SocketHandle(h), {Event.Read}, 343 | initData(Dispatcher)) 344 | 345 | 346 | # Set up timer to get current date/time. 347 | updateDate(cast[pointer](0)) 348 | chronos.addTimer(1000.int64, updateDate) 349 | 350 | var events: array[64, ReadyKey] 351 | while true: 352 | let ret = selector.selectInto(-1, events) 353 | processEvents(selector, events, ret, onRequest) 354 | 355 | # Ensure callbacks list doesn't grow forever in chronos. 356 | # See https://github.com/nim-lang/Nim/issues/7532. 357 | # Not processing callbacks can also lead to exceptions being silently 358 | # lost! 359 | if unlikely(chronos.getGlobalDispatcher().callbacks.len > 0): 360 | chronos.poll() 361 | 362 | #[ API start ]# 363 | 364 | proc unsafeSend*(req: Request, data: string) {.inline.} = 365 | ## Sends the specified data on the request socket. 366 | ## 367 | ## This function can be called as many times as necessary. 368 | ## 369 | ## It does not 370 | ## check whether the socket is in a state that can be written so be 371 | ## careful when using it. 372 | if req.client notin req.selector: 373 | return 374 | req.selector.getData(req.client).sendQueue.add(data) 375 | req.selector.updateHandle(req.client, {Event.Read, Event.Write}) 376 | 377 | proc send*(req: Request, code: HttpCode, body: string, headers="") = 378 | ## Responds with the specified HttpCode and body. 379 | ## 380 | ## **Warning:** This can only be called once in the OnRequest callback. 381 | 382 | if req.client notin req.selector: 383 | return 384 | 385 | # TODO: Reduce the amount of `getData` accesses. 386 | template getData: var Data = 387 | req.selector.getData(req.client) 388 | 389 | assert getData.headersFinished, "Selector not ready to send." 390 | 391 | let otherHeaders = 392 | if likely(headers.len == 0): 393 | "" 394 | else: 395 | "\c\L" & headers 396 | 397 | var 398 | text = ( 399 | "HTTP/1.1 $#\c\L" & 400 | "Content-Length: $#\c\LServer: $#\c\LDate: $#$#\c\L\c\L$#" 401 | ) % [$code, $body.len, serverInfo, serverDate, otherHeaders, body] 402 | 403 | getData.sendQueue.add(text) 404 | req.selector.updateHandle(req.client, {Event.Read, Event.Write}) 405 | 406 | proc send*(req: Request, code: HttpCode) {.inline.} = 407 | ## Responds with the specified HttpCode. The body of the response 408 | ## is the same as the HttpCode description. 409 | req.send(code, $code) 410 | 411 | proc send*(req: Request, body: string, code = Http200) {.inline.} = 412 | ## Sends a HTTP 200 OK response with the specified body. 413 | ## 414 | ## **Warning:** This can only be called once in the OnRequest callback. 415 | req.send(code, body) 416 | 417 | proc httpMethod*(req: Request): Option[HttpMethod] {.inline.} = 418 | ## Parses the request's data to find the request HttpMethod. 419 | parseHttpMethod(req.selector.getData(req.client).data, req.start) 420 | 421 | proc path*(req: Request): Option[string] {.inline.} = 422 | ## Parses the request's data to find the request target. 423 | if unlikely(req.client notin req.selector): return 424 | parsePath(req.selector.getData(req.client).data, req.start) 425 | 426 | proc headers*(req: Request): Option[HttpHeaders] = 427 | ## Parses the request's data to get the headers. 428 | if unlikely(req.client notin req.selector): return 429 | parseHeaders(req.selector.getData(req.client).data, req.start) 430 | 431 | proc body*(req: Request): Option[string] = 432 | ## Retrieves the body of the request. 433 | let pos = req.selector.getData(req.client).headersFinishPos 434 | if pos == -1: return none(string) 435 | result = req.selector.getData(req.client).data[ 436 | pos .. ^1 437 | ].some() 438 | 439 | when not defined(release): 440 | let length = 441 | if req.headers.get.hasKey("Content-Length"): 442 | req.headers.get["Content-Length"].parseInt 443 | else: 444 | 0 445 | assert result.get.len == length 446 | 447 | proc ip*(req: Request): string {.inline.} = 448 | ## Retrieves the IP address that the request was made from. 449 | req.selector.getData(req.client).ip 450 | 451 | proc forget*(req: Request) {.inline.} = 452 | ## Unregisters the underlying request's client socket from httpx's 453 | ## event loop. 454 | ## 455 | ## This is useful when you want to register ``req.client`` in your own 456 | ## event loop, for example when wanting to integrate httpx into a 457 | ## websocket library. 458 | req.selector.unregister(req.client) 459 | 460 | proc validateRequest(req: Request): bool = 461 | ## Handles protocol-mandated responses. 462 | ## 463 | ## Returns ``false`` when the request has been handled. 464 | result = true 465 | 466 | # From RFC7231: "When a request method is received 467 | # that is unrecognized or not implemented by an origin server, the 468 | # origin server SHOULD respond with the 501 (Not Implemented) status 469 | # code." 470 | if req.httpMethod.isNone: 471 | req.send(Http501) 472 | result = false 473 | 474 | proc run*(onRequest: OnRequest, settings: Settings) = 475 | ## Starts the HTTP server and calls `onRequest` for each request. 476 | ## 477 | ## The ``onRequest`` procedure returns a ``Future[void]`` type. But 478 | ## unlike most asynchronous procedures in Nim, it can return ``nil`` 479 | ## for better performance, when no async operations are needed. 480 | when compileOption("threads"): 481 | let numThreads = 482 | if settings.numThreads == 0: countProcessors() 483 | else: settings.numThreads 484 | else: 485 | let numThreads = 1 486 | 487 | echo("Starting ", numThreads, " threads") 488 | if numThreads > 1: 489 | when compileOption("threads"): 490 | var threads = newSeq[Thread[(OnRequest, Settings)]](numThreads) 491 | for i in 0 ..< numThreads: 492 | createThread[(OnRequest, Settings)]( 493 | threads[i], eventLoop, (onRequest, settings) 494 | ) 495 | echo("Listening on port ", settings.port) # This line is used in the tester to signal readiness. 496 | joinThreads(threads) 497 | else: 498 | assert false 499 | else: 500 | eventLoop((onRequest, settings)) 501 | 502 | proc run*(onRequest: OnRequest) {.inline.} = 503 | ## Starts the HTTP server with default settings. Calls `onRequest` for each 504 | ## request. 505 | ## 506 | ## See the other ``run`` proc for more info. 507 | run(onRequest, Settings(port: Port(8080), bindAddr: "")) 508 | 509 | when false: 510 | proc close*(port: Port) = 511 | ## Closes an httpx server that is running on the specified port. 512 | ## 513 | ## **NOTE:** This is not yet implemented. 514 | 515 | assert false 516 | # TODO: Figure out the best way to implement this. One way is to use async 517 | # events to signal our `eventLoop`. Maybe it would be better not to support 518 | # multiple servers running at the same time? 519 | -------------------------------------------------------------------------------- /httpbeast.LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dominik Picheta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /httpx.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.3.8" 4 | author = "ringabout" 5 | description = "A super-fast epoll-backed and parallel HTTP server." 6 | license = "Apache 2.0" 7 | 8 | srcDir = "src" 9 | 10 | # Dependencies 11 | 12 | 13 | requires "nim >= 1.6.12" 14 | requires "ioselectors >= 0.2.0" 15 | 16 | 17 | task helloworld, "Compiles and executes the hello world server.": 18 | exec "nim c -d:release -r tests/helloworld" 19 | 20 | task dispatcher, "Compiles and executes the dispatcher test server.": 21 | exec "nim c -d:release -r tests/dispatcher" 22 | 23 | task tests, "Runs the test suite.": 24 | exec "testament all" 25 | -------------------------------------------------------------------------------- /src/httpx.nim: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2020 Dominik Picheta 3 | 4 | # Copyright 2020 Zeshen Xing 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | import net, nativesockets, os, httpcore, asyncdispatch, strutils 20 | import options, logging, times, heapqueue, std/monotimes 21 | import std/sugar 22 | 23 | from deques import len 24 | 25 | import ioselectors 26 | 27 | import httpx/parser 28 | 29 | const useWinVersion = defined(windows) or defined(nimdoc) 30 | const usePosixVersion = defined(posix) and not defined(nimdoc) 31 | 32 | when useWinVersion: 33 | import sets 34 | else: 35 | import posix 36 | from osproc import countProcessors 37 | 38 | 39 | export httpcore 40 | 41 | 42 | type 43 | FdKind = enum 44 | Server, Client, Dispatcher 45 | 46 | Data = object 47 | fdKind: FdKind ## Determines the fd kind (server, client, dispatcher) 48 | ## - Client specific data. 49 | ## A queue of data that needs to be sent when the FD becomes writeable. 50 | sendQueue: string 51 | ## The number of characters in `sendQueue` that have been sent already. 52 | bytesSent: int 53 | ## Big chunk of data read from client during request. 54 | data: string 55 | ## Determines whether `data` contains "\c\l\c\l". 56 | headersFinished: bool 57 | ## Determines position of the end of "\c\l\c\l". 58 | headersFinishPos: int 59 | ## The address that a `client` connects from. 60 | ip: string 61 | ## Future for onRequest handler (may be nil). 62 | reqFut: Future[void] 63 | ## Identifier for current request. Mainly for better detection of cross-talk. 64 | requestID: uint 65 | 66 | type 67 | Request* = object 68 | ## An HTTP request 69 | 70 | selector: Selector[Data] 71 | 72 | client*: SocketHandle 73 | ## The underlying operating system socket handle associated with the request client connection. 74 | ## May be closed; you should check by calling "closed" on this Request object before interacting with its client socket. 75 | 76 | # Identifier used to distinguish requests. 77 | requestID: uint 78 | 79 | OnRequest* = proc (req: Request): Future[void] {.gcsafe, gcsafe.} 80 | ## Callback used to handle HTTP requests 81 | 82 | ThreadVars = tuple 83 | event: Option[AsyncEvent] 84 | 85 | Startup = proc () {.closure, gcsafe.} 86 | 87 | Settings* = object 88 | ## HTTP server settings 89 | 90 | port*: Port 91 | ## The port to bind to 92 | 93 | bindAddr*: string 94 | ## The address to bind to 95 | 96 | listener*: Socket 97 | ## Socket to use, if nil the socket is created from bindAddr and port 98 | ## The socket must be listening, and it will be set non blocking 99 | 100 | numThreads: int 101 | ## The number of threads to serve on 102 | 103 | startup: Startup 104 | 105 | HttpxDefect* = ref object of Defect 106 | ## Defect raised when something HTTPX-specific fails 107 | 108 | const httpxDefaultServerName* = "Nim-HTTPX" 109 | ## The default server name sent in the Server header in responses. 110 | ## A custom name can be set by defining httpxServerName at compile time. 111 | 112 | const serverInfo {.strdefine.}: string = httpxDefaultServerName 113 | ## Alias to httpxServerName, use that instead 114 | 115 | const httpxServerName* {.strdefine.} = 116 | when serverInfo != httpxDefaultServerName: 117 | {.warning: "Setting the server name with serverInfo is deprecated. You should use httpxServerName instead.".} 118 | serverInfo 119 | else: 120 | httpxDefaultServerName 121 | ## The server name sent in the Server header in responses. 122 | ## If not defined, the value of httpxDefaultServerName will be used. 123 | ## If the value is empty, no Server header will be sent. 124 | 125 | const httpxClientBufDefaultSize* = 256 126 | ## The default size of the client read buffer. 127 | ## A custom size can be set by defining httpxClientBufSize. 128 | 129 | const httpxClientBufSize* {.intdefine.} = httpxClientBufDefaultSize 130 | ## The size of the client read buffer. 131 | ## Defaults to httpxClientBufDefaultSize. 132 | 133 | when httpxClientBufSize < 3: 134 | {.fatal: "Client buffer size must be at least 3, and ideally at least 256.".} 135 | elif httpxClientBufSize < httpxClientBufDefaultSize: 136 | {.warning: "You should set your client read buffer size to at least 256 bytes. Smaller buffers will harm performance.".} 137 | 138 | const httpxSendServerDate* {.booldefine.} = true 139 | ## Whether to send the current server date along with requests. 140 | ## Defaults to true. 141 | 142 | when httpxSendServerDate: 143 | # We store the current server date here as a thread var and update it periodically to avoid checking the date each time we respond to a request. 144 | # The date is updated every second from within the event loop. 145 | var serverDate {.threadvar.}: string 146 | 147 | 148 | when usePosixVersion: 149 | let osMaxFdCount = selectors.maxDescriptors() 150 | ## The maximum number of file descriptors allowed at one time by the OS 151 | 152 | proc doNothing(): Startup {.gcsafe.} = 153 | result = proc () {.closure, gcsafe.} = 154 | discard 155 | 156 | func initSettings*(port = Port(8080), 157 | bindAddr = "", 158 | numThreads = 0, 159 | startup: Startup = doNothing(), 160 | listener: Socket = nil 161 | ): Settings = 162 | ## Creates a new HTTP server Settings object with the provided options. 163 | 164 | result = Settings( 165 | port: port, 166 | bindAddr: bindAddr, 167 | listener: listener, 168 | numThreads: numThreads, 169 | startup: startup 170 | ) 171 | 172 | func initData(fdKind: FdKind, ip = ""): Data = 173 | result = Data(fdKind: fdKind, 174 | sendQueue: "", 175 | bytesSent: 0, 176 | data: "", 177 | headersFinished: false, 178 | headersFinishPos: -1, ## By default we assume the fast case: end of data. 179 | ip: ip 180 | ) 181 | 182 | 183 | template withRequestData(req: Request, body: untyped) = 184 | let requestData {.inject.} = addr req.selector.getData(req.client) 185 | body 186 | 187 | #[ API start ]# 188 | 189 | func closed*(req: Request): bool {.inline, raises: [].} = 190 | ## If the client has disconnected from the server or not. 191 | result = req.client notin req.selector 192 | 193 | proc unsafeSend*(req: Request, data: string) {.inline.} = 194 | ## Sends the specified data on the request socket. 195 | ## 196 | ## This function can be called as many times as necessary. 197 | ## 198 | ## It does not check whether the socket is in a state 199 | ## that can be written so be careful when using it. 200 | if req.closed: 201 | return 202 | 203 | withRequestData(req): 204 | requestData.sendQueue.add(data) 205 | req.selector.updateHandle(req.client, {Event.Read, Event.Write}) 206 | 207 | proc send*(req: Request, code: HttpCode, body: string, contentLength: Option[int], headers = "") {.inline.} = 208 | ## Responds with the specified HttpCode and body. 209 | ## 210 | ## **Warning:** This can only be called once in the OnRequest callback. 211 | 212 | if req.closed: 213 | return 214 | 215 | withRequestData(req): 216 | assert requestData.headersFinished, "Selector for $1 not ready to send." % $req.client.int 217 | if requestData.requestID != req.requestID: 218 | raise HttpxDefect(msg: "You are attempting to send data to a stale request.") 219 | 220 | let otherHeaders = 221 | if likely(headers.len != 0): 222 | "\c\L" & headers 223 | else: 224 | "" 225 | 226 | var text = "" 227 | text &= "HTTP/1.1 " 228 | text.addInt code.int 229 | if contentLength.isSome: 230 | text &= "\c\LContent-Length: " 231 | text.addInt contentLength.unsafeGet() 232 | 233 | when httpxServerName != "": 234 | text &= "\c\LServer: " & httpxServerName 235 | 236 | when httpxSendServerDate: 237 | text &= "\c\LDate: " & serverDate 238 | 239 | text &= otherHeaders 240 | text &= "\c\L\c\L" 241 | text &= body 242 | 243 | requestData.sendQueue.add(text) 244 | req.selector.updateHandle(req.client, {Event.Read, Event.Write}) 245 | 246 | proc send*(req: Request, code: HttpCode, body: string, contentLength: Option[string], 247 | headers = "") {.inline, deprecated: "Use Option[int] for contentLength parameter".} = 248 | req.send(code, body, some parseInt(contentLength.get($body.len)), headers) 249 | 250 | template send*(req: Request, code: HttpCode, body: string, headers = "") = 251 | ## Responds with the specified HttpCode and body. 252 | ## 253 | ## **Warning:** This can only be called once in the OnRequest callback. 254 | 255 | req.send(code, body, some body.len, headers) 256 | 257 | proc send*(req: Request, code: HttpCode) = 258 | ## Responds with the specified HttpCode. The body of the response 259 | ## is the same as the HttpCode description. 260 | assert req.selector.getData(req.client).requestID == req.requestID 261 | req.send(code, $code) 262 | 263 | proc send*(req: Request, body: string, code = Http200) {.inline.} = 264 | ## Sends a HTTP 200 OK response with the specified body. 265 | ## 266 | ## **Warning:** This can only be called once in the OnRequest callback. 267 | req.send(code, body) 268 | 269 | template tryAcceptClient() = 270 | ## Tries to accept a client, but does nothing if one cannot be accepted (due to file descriptor exhaustion, etc) 271 | 272 | let (client, address) = fd.SocketHandle.accept 273 | if client == osInvalidSocket: 274 | let lastError = osLastError() 275 | 276 | when usePosixVersion: 277 | if lastError.int32 == EAGAIN: 278 | # this is not an error as multiple threads can wake up for the same 279 | # event (in case the listener socket is shared) and only a single thread 280 | # can successfully accept the connection. 281 | # Skip and try again next time. 282 | return 283 | if lastError.int32 == EMFILE: 284 | warn("Ignoring EMFILE error: ", osErrorMsg(lastError)) 285 | return 286 | 287 | raiseOSError(lastError) 288 | 289 | setBlocking(client, false) 290 | 291 | template regHandle() = 292 | selector.registerHandle(client, {Event.Read}, initData(Client, ip = address)) 293 | 294 | when usePosixVersion: 295 | # Only register the handle if the file descriptor count has not been reached 296 | if likely(client.int < osMaxFdCount): 297 | regHandle() 298 | else: 299 | regHandle() 300 | 301 | 302 | template closeClient(selector: Selector[Data], 303 | fd: SocketHandle|int, 304 | inLoop = true) = 305 | # TODO: Can POST body be sent with Connection: Close? 306 | var data: ptr Data = addr selector.getData(fd) 307 | let isRequestComplete = data.reqFut.isNil or data.reqFut.finished 308 | if isRequestComplete: 309 | # The `onRequest` callback isn't in progress, so we can close the socket. 310 | selector.unregister(fd) 311 | fd.SocketHandle.close() 312 | else: 313 | # Close the socket only once the `onRequest` callback completes. 314 | data.reqFut.addCallback( 315 | proc (fut: Future[void]) = 316 | fd.SocketHandle.close() 317 | ) 318 | # Unregister fd so that we don't receive any more events for it. 319 | # Once we do so the `data` will no longer be accessible. 320 | selector.unregister(fd) 321 | 322 | logging.debug("socket: " & $fd & " is closed!") 323 | 324 | when inLoop: 325 | break 326 | else: 327 | return 328 | 329 | proc onRequestFutureComplete(theFut: Future[void], 330 | selector: Selector[Data], fd: int) = 331 | if theFut.failed: 332 | raise theFut.error 333 | 334 | template fastHeadersCheck(data: ptr Data): bool = 335 | let res = data.data[^1] == '\l' and data.data[^2] == '\c' and 336 | data.data[^3] == '\l' and data.data[^4] == '\c' 337 | if res: 338 | data.headersFinishPos = data.data.len 339 | res 340 | 341 | template methodNeedsBody(data: ptr Data): bool = 342 | # Only idempotent methods can be pipelined (GET/HEAD/PUT/DELETE), they 343 | # never need a body, so we just assume `start` at 0. 344 | let reqMthod = parseHttpMethod(data.data) 345 | reqMthod.isSome and (reqMthod.get in {HttpPost, HttpPut, HttpConnect, HttpPatch}) 346 | 347 | proc slowHeadersCheck(data: ptr Data): bool = 348 | if unlikely(methodNeedsBody(data)): 349 | # Look for \c\l\c\l inside data. 350 | data.headersFinishPos = 0 351 | template ch(i: int): char = 352 | let pos = data.headersFinishPos + i 353 | if pos >= data.data.len: 354 | '\0' 355 | else: 356 | data.data[pos] 357 | 358 | while data.headersFinishPos < data.data.len: 359 | case ch(0) 360 | of '\c': 361 | if ch(1) == '\l' and ch(2) == '\c' and ch(3) == '\l': 362 | data.headersFinishPos.inc(4) 363 | return true 364 | else: 365 | discard 366 | inc data.headersFinishPos 367 | 368 | data.headersFinishPos = -1 369 | 370 | proc bodyInTransit(data: ptr Data): bool = 371 | # get, head, put, delete 372 | assert methodNeedsBody(data), "Calling bodyInTransit now is inefficient." 373 | assert data.headersFinished 374 | 375 | if data.headersFinishPos == -1: 376 | return false 377 | 378 | let trueLen = parseContentLength(data.data) 379 | 380 | let bodyLen = data.data.len - data.headersFinishPos 381 | assert(not (bodyLen > trueLen)) 382 | result = bodyLen != trueLen 383 | 384 | var requestCounter: uint = 0 385 | proc genRequestID(): uint = 386 | if requestCounter == high(uint): 387 | requestCounter = 0 388 | requestCounter += 1 389 | return requestCounter 390 | 391 | proc validateRequest(req: Request): bool {.gcsafe.} 392 | 393 | proc processEvents(selector: Selector[Data], 394 | events: array[64, ReadyKey], count: int, 395 | onRequest: OnRequest) = 396 | for i in 0 ..< count: 397 | let fd = events[i].fd 398 | var data: ptr Data = addr(getData(selector, fd)) 399 | # Handle error events first. 400 | if Event.Error in events[i].events: 401 | if isDisconnectionError({SocketFlag.SafeDisconn}, 402 | events[i].errorCode): 403 | closeClient(selector, fd) 404 | raiseOSError(events[i].errorCode) 405 | 406 | case data.fdKind 407 | of Server: 408 | if Event.Read in events[i].events: 409 | tryAcceptClient() 410 | else: 411 | doAssert false, "Only Read events are expected for the server" 412 | of Dispatcher: 413 | # Run the dispatcher loop. 414 | when usePosixVersion: 415 | assert events[i].events == {Event.Read} 416 | asyncdispatch.poll(0) 417 | else: 418 | discard 419 | of Client: 420 | if Event.Read in events[i].events: 421 | var buf: array[httpxClientBufSize, char] 422 | # Read until EAGAIN. We take advantage of the fact that the client 423 | # will wait for a response after they send a request. So we can 424 | # comfortably continue reading until the message ends with \c\l 425 | # \c\l. 426 | while true: 427 | let ret = recv(fd.SocketHandle, addr buf[0], httpxClientBufSize, 0.cint) 428 | if ret == 0: 429 | closeClient(selector, fd) 430 | 431 | if ret == -1: 432 | # Error! 433 | let lastError = osLastError() 434 | 435 | when usePosixVersion: 436 | if lastError.int32 in [EWOULDBLOCK, EAGAIN]: 437 | break 438 | else: 439 | if lastError.int == WSAEWOULDBLOCK: 440 | break 441 | 442 | if isDisconnectionError({SocketFlag.SafeDisconn}, lastError): 443 | closeClient(selector, fd) 444 | raiseOSError(lastError) 445 | 446 | # Write buffer to our data. 447 | let origLen = data.data.len 448 | data.data.setLen(origLen + ret) 449 | for i in 0 ..< ret: 450 | data.data[origLen + i] = buf[i] 451 | 452 | if data.data.len >= 4 and fastHeadersCheck(data) or slowHeadersCheck(data): 453 | # First line and headers for request received. 454 | data.headersFinished = true 455 | when not defined(release): 456 | if data.sendQueue.len != 0: 457 | logging.warn("sendQueue isn't empty.") 458 | if data.bytesSent != 0: 459 | logging.warn("bytesSent isn't empty.") 460 | 461 | let waitingForBody = methodNeedsBody(data) and bodyInTransit(data) 462 | if likely(not waitingForBody): 463 | # For pipelined requests, we need to reset this flag. 464 | data.headersFinished = true 465 | data.requestID = genRequestID() 466 | 467 | let request = Request( 468 | selector: selector, 469 | client: fd.SocketHandle, 470 | requestID: data.requestID 471 | ) 472 | 473 | template validateResponse(data: ptr Data): untyped = 474 | if data.requestID == request.requestID: 475 | data.headersFinished = false 476 | 477 | if validateRequest(request): 478 | data.reqFut = onRequest(request) 479 | if not data.reqFut.isNil: 480 | capture data: 481 | data.reqFut.addCallback( 482 | proc (fut: Future[void]) = 483 | onRequestFutureComplete(fut, selector, fd) 484 | validateResponse(data) 485 | ) 486 | else: 487 | validateResponse(data) 488 | 489 | if ret != httpxClientBufSize: 490 | # Assume there is nothing else for us right now and break. 491 | break 492 | elif Event.Write in events[i].events: 493 | assert data.sendQueue.len > 0 494 | assert data.bytesSent < data.sendQueue.len 495 | # Write the sendQueue. 496 | 497 | let leftover = 498 | when usePosixVersion: 499 | data.sendQueue.len - data.bytesSent 500 | else: 501 | cint(data.sendQueue.len - data.bytesSent) 502 | let ret = send(fd.SocketHandle, addr data.sendQueue[data.bytesSent], 503 | leftover, 0) 504 | if ret == -1: 505 | # Error! 506 | let lastError = osLastError() 507 | 508 | when usePosixVersion: 509 | if lastError.int32 in [EWOULDBLOCK, EAGAIN]: 510 | break 511 | else: 512 | if lastError.int == WSAEWOULDBLOCK: 513 | break 514 | 515 | if isDisconnectionError({SocketFlag.SafeDisconn}, lastError): 516 | closeClient(selector, fd) 517 | raiseOSError(lastError) 518 | 519 | data.bytesSent.inc(ret) 520 | 521 | if data.sendQueue.len == data.bytesSent: 522 | data.bytesSent = 0 523 | data.sendQueue.setLen(0) 524 | data.data.setLen(0) 525 | selector.updateHandle(fd.SocketHandle, 526 | {Event.Read}) 527 | else: 528 | assert false 529 | 530 | when httpxSendServerDate: 531 | proc updateDate(fd: AsyncFD): bool = 532 | result = false # Returning true signifies we want timer to stop. 533 | serverDate = now().utc().format("ddd, dd MMM yyyy HH:mm:ss 'GMT'") 534 | 535 | proc eventLoop(params: (OnRequest, Settings, ThreadVars)) = 536 | let 537 | (onRequest, settings, threadVars) = params 538 | selector = newSelector[Data]() 539 | 540 | if settings.startup != nil: 541 | settings.startup() 542 | 543 | var server: Socket = settings.listener; 544 | if server == nil: 545 | server = newSocket() 546 | server.setSockOpt(OptReuseAddr, true) 547 | server.setSockOpt(OptReusePort, true) 548 | server.bindAddr(settings.port, settings.bindAddr) 549 | server.listen() 550 | 551 | server.getFd.setBlocking(false) 552 | selector.registerHandle(server.getFd, {Event.Read}, initData(Server)) 553 | 554 | when httpxSendServerDate: 555 | # Set up timer to get current date/time. 556 | discard updateDate(0.AsyncFD) 557 | asyncdispatch.addTimer(1000, false, updateDate) 558 | 559 | let disp = getGlobalDispatcher() 560 | 561 | when usePosixVersion: 562 | selector.registerHandle(disp.getIoHandler.getFd, {Event.Read}, 563 | initData(Dispatcher)) 564 | 565 | var events: array[64, ReadyKey] 566 | while true: 567 | let ret = selector.selectInto(-1, events) 568 | processEvents(selector, events, ret, onRequest) 569 | 570 | # Ensure callbacks list doesn't grow forever in asyncdispatch. 571 | # See https://github.com/nim-lang/Nim/issues/7532. 572 | # Not processing callbacks can also lead to exceptions being silently 573 | # lost! 574 | if unlikely(disp.callbacks.len > 0): 575 | asyncdispatch.poll(0) 576 | else: 577 | var events: array[64, ReadyKey] 578 | while true: 579 | let ret = 580 | if disp.timers.len > 0: 581 | selector.selectInto((disp.timers[0].finishAt - getMonoTime()).inMilliseconds.int, events) 582 | else: 583 | selector.selectInto(20, events) 584 | if ret > 0: 585 | processEvents(selector, events, ret, onRequest) 586 | asyncdispatch.poll(0) 587 | 588 | if threadVars.event.isSome(): 589 | threadVars.event.get().trigger() 590 | 591 | proc httpMethod*(req: Request): Option[HttpMethod] {.inline.} = 592 | ## Parses the request's data to find the request HttpMethod. 593 | parseHttpMethod(req.selector.getData(req.client).data) 594 | 595 | proc path*(req: Request): Option[string] {.inline.} = 596 | ## Parses the request's data to find the request target. 597 | if unlikely(req.client notin req.selector): 598 | return 599 | parsePath(req.selector.getData(req.client).data) 600 | 601 | proc headers*(req: Request): Option[HttpHeaders] = 602 | ## Parses the request's data to get the headers. 603 | if unlikely(req.client notin req.selector): 604 | return 605 | parseHeaders(req.selector.getData(req.client).data) 606 | 607 | proc body*(req: Request): Option[string] = 608 | ## Retrieves the body of the request. 609 | let pos = req.selector.getData(req.client).headersFinishPos 610 | if pos == -1: 611 | return none(string) 612 | result = some(req.selector.getData(req.client).data[pos .. ^1]) 613 | 614 | when not defined(release): 615 | let length = 616 | if req.headers.get.hasKey("Content-Length"): 617 | req.headers.get["Content-Length"].parseInt 618 | else: 619 | 0 620 | doAssert result.get.len == length 621 | 622 | proc ip*(req: Request): string = 623 | ## Retrieves the IP address that the request was made from. 624 | req.selector.getData(req.client).ip 625 | 626 | proc forget*(req: Request) = 627 | ## Unregisters the underlying request's client socket from httpx's 628 | ## event loop. 629 | ## 630 | ## This is useful when you want to register ``req.client`` in your own 631 | ## event loop, for example when wanting to integrate httpx into a 632 | ## websocket library. 633 | req.selector.unregister(req.client) 634 | 635 | proc validateRequest(req: Request): bool = 636 | ## Handles protocol-mandated responses. 637 | ## 638 | ## Returns ``false`` when the request has been handled. 639 | result = true 640 | 641 | # From RFC7231: "When a request method is received 642 | # that is unrecognized or not implemented by an origin server, the 643 | # origin server SHOULD respond with the 501 (Not Implemented) status 644 | # code." 645 | if req.httpMethod.isNone: 646 | req.send(Http501) 647 | result = false 648 | 649 | proc run*(onRequest: OnRequest, settings: Settings) = 650 | ## Starts the HTTP server and calls `onRequest` for each request. 651 | ## 652 | ## The ``onRequest`` procedure returns a ``Future[void]`` type. But 653 | ## unlike most asynchronous procedures in Nim, it can return ``nil`` 654 | ## for better performance, when no async operations are needed. 655 | when not useWinVersion: 656 | when compileOption("threads"): 657 | let numThreads = 658 | if settings.numThreads == 0: 659 | countProcessors() 660 | else: 661 | settings.numThreads 662 | else: 663 | let numThreads = 1 664 | 665 | logging.debug("Starting ", numThreads, " threads") 666 | 667 | if numThreads > 1: 668 | when compileOption("threads"): 669 | var threads = newSeq[Thread[(OnRequest, Settings, ThreadVars)]](numThreads) 670 | for i in 0 ..< numThreads: 671 | createThread[(OnRequest, Settings, ThreadVars)]( 672 | threads[i], eventLoop, (onRequest, settings, (none(AsyncEvent),)) 673 | ) 674 | 675 | logging.debug("Listening on port ", 676 | settings.port) # This line is used in the tester to signal readiness. 677 | 678 | joinThreads(threads) 679 | else: 680 | doAssert false, "Please enable threads when numThreads is greater than 1!" 681 | else: 682 | eventLoop((onRequest, settings, (none(AsyncEvent),))) 683 | else: 684 | eventLoop((onRequest, settings, (none(AsyncEvent),))) 685 | logging.debug("Starting ", 1, " threads") 686 | 687 | proc run*(onRequest: OnRequest) {.inline.} = 688 | ## Starts the HTTP server with default settings. Calls `onRequest` for each 689 | ## request. 690 | ## 691 | ## See the other ``run`` proc for more info. 692 | run(onRequest, Settings(port: Port(8080), bindAddr: "", startup: doNothing())) 693 | 694 | proc waitEvent(ev: AsyncEvent): Future[void] = 695 | var fut = newFuture[void]("waitEvent") 696 | proc cb(fd: AsyncFD): bool = fut.complete(); return true 697 | addEvent(ev, cb) 698 | return fut 699 | 700 | proc runAsync*(onRequest: OnRequest, settings: Settings) {.async.} = 701 | ## Starts the HTTP server and calls `onRequest` for each request. 702 | ## 703 | ## The ``onRequest`` procedure returns a ``Future[void]`` type. But 704 | ## unlike most asynchronous procedures in Nim, it can return ``nil`` 705 | ## for better performance, when no async operations are needed. 706 | when compileOption("threads"): 707 | let numThreads = 708 | when useWinVersion: 709 | 1 710 | else: 711 | if settings.numThreads == 0: 712 | countProcessors() 713 | else: 714 | settings.numThreads 715 | 716 | logging.debug("Starting ", numThreads, " threads") 717 | 718 | var futures: seq[Future[void]] = @[] 719 | var threads = newSeq[Thread[(OnRequest, Settings, ThreadVars)]](numThreads) 720 | for i in 0 ..< numThreads: 721 | let ev = newAsyncEvent() 722 | createThread[(OnRequest, Settings, ThreadVars)]( 723 | threads[i], eventLoop, (onRequest, settings, (some(ev),)) 724 | ) 725 | futures.add(waitEvent(ev)) 726 | 727 | logging.debug("Listening on port ", settings.port) # This line is used in the tester to signal readiness. 728 | 729 | await all(futures) 730 | 731 | else: 732 | doAssert false, "Please enable threads when using runAsync!" 733 | 734 | proc runAsync*(onRequest: OnRequest) {.inline, async.} = 735 | ## Starts the HTTP server with default settings. Calls `onRequest` for each 736 | ## request. 737 | ## 738 | ## See the other ``run`` proc for more info. 739 | await runAsync(onRequest, Settings(port: Port(8080), bindAddr: "", startup: doNothing())) 740 | 741 | when false: 742 | proc close*(port: Port) = 743 | ## Closes an httpx server that is running on the specified port. 744 | ## 745 | ## **NOTE:** This is not yet implemented. 746 | 747 | doAssert false 748 | # TODO: Figure out the best way to implement this. One way is to use async 749 | # events to signal our `eventLoop`. Maybe it would be better not to support 750 | # multiple servers running at the same time? 751 | -------------------------------------------------------------------------------- /src/httpx.nim.cfg: -------------------------------------------------------------------------------- 1 | @if windows: 2 | --threads:off 3 | @else: 4 | --threads:on 5 | @end -------------------------------------------------------------------------------- /src/httpx/parser.nim: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # Copyright (c) 2020 Dominik Picheta 3 | 4 | # Copyright 2020 Zeshen Xing 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | import options, httpcore, parseutils 20 | 21 | 22 | func parseHttpMethod*(data: string): Option[HttpMethod] = 23 | ## Parses the data to find the request HttpMethod. 24 | 25 | # HTTP methods are case sensitive. 26 | # (RFC7230 3.1.1. "The request method is case-sensitive.") 27 | case data[0] 28 | of 'G': 29 | if data[1] == 'E' and data[2] == 'T': 30 | result = some(HttpGet) 31 | of 'H': 32 | if data[1] == 'E' and data[2] == 'A' and data[3] == 'D': 33 | result = some(HttpHead) 34 | of 'P': 35 | if data[1] == 'O' and data[2] == 'S' and data[3] == 'T': 36 | result = some(HttpPost) 37 | if data[1] == 'U' and data[2] == 'T': 38 | result = some(HttpPut) 39 | if data[1] == 'A' and data[2] == 'T' and 40 | data[3] == 'C' and data[4] == 'H': 41 | result = some(HttpPatch) 42 | of 'D': 43 | if data[1] == 'E' and data[2] == 'L' and 44 | data[3] == 'E' and data[4] == 'T' and 45 | data[5] == 'E': 46 | result = some(HttpDelete) 47 | of 'O': 48 | if data[1] == 'P' and data[2] == 'T' and 49 | data[3] == 'I' and data[4] == 'O' and 50 | data[5] == 'N' and data[6] == 'S': 51 | result = some(HttpOptions) 52 | else: 53 | result = none(HttpMethod) 54 | 55 | func parsePath*(data: string): Option[string] = 56 | ## Parses the request path from the specified data. 57 | if unlikely(data.len == 0): 58 | return 59 | 60 | # Find the first ' '. 61 | # We can actually start ahead a little here. Since we know 62 | # the shortest HTTP method: 'GET'/'PUT'. 63 | var i = 2 64 | while data[i] notin {' ', '\0'}: 65 | inc i 66 | 67 | if likely(data[i] == ' '): 68 | # Find the second ' '. 69 | inc i # Skip first ' '. 70 | let start = i 71 | while data[i] notin {' ', '\0'}: 72 | inc i 73 | 74 | if likely(data[i] == ' '): 75 | return some(data[start..