├── .appveyor.yml ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── altlib.sh ├── images ├── mofuw.png └── tokio-minihttp.png ├── mofuw.nimble ├── setup.sh ├── src ├── mofuw.nim ├── mofuw.nim.cfg ├── mofuw │ ├── jesterUtils.nim │ ├── middleware │ │ └── auth │ │ │ ├── README.md │ │ │ ├── mofuwAuth.nim │ │ │ └── randUtils.nim │ ├── nest.nim │ ├── private │ │ └── websocket │ │ │ ├── hex.nim │ │ │ ├── wsserver.nim │ │ │ └── wsshared.nim │ └── websocket.nim └── private │ ├── ctx.nim │ ├── ctxpool.nim │ ├── etags.nim │ ├── handler.nim │ ├── http.nim │ ├── io.nim │ ├── log.nim │ ├── route.nim │ ├── server.nim │ └── sysutils.nim └── tests ├── SSLapp ├── app.nim └── nim.cfg ├── helloworld ├── minimal.nim └── nim.cfg ├── routing ├── nim.cfg └── simple.nim ├── staticServe ├── nim.cfg ├── public │ └── index.html └── static.nim ├── techempower ├── nim.cfg └── techempower.nim ├── vhost ├── nim.cfg └── vhost.nim └── websocket ├── README.md ├── nim.cfg └── ws.nim /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | 3 | cache: 4 | - nim-0.18.0_x64.zip 5 | - x86_64-7.2.0-release-win32-seh-rt_v5-rev1.7z 6 | 7 | matrix: 8 | fast_finish: true 9 | 10 | environment: 11 | matrix: 12 | - MINGW_ARCHIVE: x86_64-7.2.0-release-win32-seh-rt_v5-rev1.7z 13 | MINGW_DIR: mingw64 14 | MINGW_URL: https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win64/Personal%20Builds/mingw-builds/7.2.0/threads-win32/seh/x86_64-7.2.0-release-win32-seh-rt_v5-rev1.7z/download 15 | NIM_ARCHIVE: nim-0.18.0_x64.zip 16 | NIM_DIR: nim-0.18.0 17 | NIM_URL: https://nim-lang.org/download/nim-0.18.0_x64.zip 18 | platform: x64 19 | 20 | install: 21 | - MKDIR %CD%\tools_tmp 22 | - IF not exist "%MINGW_ARCHIVE%" appveyor DownloadFile "%MINGW_URL%" -FileName "%MINGW_ARCHIVE%" 23 | - 7z x -y "%MINGW_ARCHIVE%" -o"%CD%\tools_tmp"> nul 24 | - IF not exist "%NIM_ARCHIVE%" appveyor DownloadFile "%NIM_URL%" -FileName "%NIM_ARCHIVE%" 25 | - 7z x -y "%NIM_ARCHIVE%" -o"%CD%\tools_tmp"> nul 26 | - SET PATH=%CD%\tools_tmp\%NIM_DIR%\bin;%CD%\tools_tmp\%MINGW_DIR%\bin;%PATH% 27 | - nimble.exe install -y 28 | 29 | build_script: 30 | - nim.exe c -f -o:"mofuw.out" src/mofuw 31 | 32 | deploy: off -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Copied from https://github.com/nim-lang/Nim/wiki/TravisCI 2 | os: 3 | - linux 4 | - osx 5 | language: c 6 | env: 7 | # Build and test against the master and devel branches of Nim 8 | - BRANCH=master 9 | compiler: 10 | # Build and test using both gcc and clang 11 | - gcc 12 | matrix: 13 | fast_finish: false 14 | install: 15 | - | 16 | if [ ! -x nim-$BRANCH/bin/nim ]; then 17 | git clone -b $BRANCH --depth 1 git://github.com/nim-lang/nim nim-$BRANCH/ 18 | cd nim-$BRANCH 19 | git clone --depth 1 git://github.com/nim-lang/csources csources/ 20 | cd csources 21 | sh build.sh 22 | cd .. 23 | rm -rf csources 24 | bin/nim c koch 25 | ./koch boot -d:release 26 | ./koch tools 27 | else 28 | cd nim-$BRANCH 29 | git fetch origin 30 | if ! git merge FETCH_HEAD | grep "Already up-to-date"; then 31 | bin/nim c koch 32 | ./koch boot -d:release 33 | ./koch tools 34 | fi 35 | fi 36 | cd .. 37 | before_script: 38 | - export PATH="nim-$BRANCH/bin${PATH:+:$PATH}" 39 | script: 40 | - chmod +x altlib.sh 41 | - ./altlib.sh 42 | # Replace uppercase strings! 43 | - nim c --cc:$CC --verbosity:0 -f -o:"mofuw.out" src/mofuw 44 | # Optional: build docs. 45 | # - nim doc --docSeeSrcUrl:https://github.com/AUTHOR/MYPROJECT/blob/master --project MYFILE.nim 46 | cache: 47 | directories: 48 | - nim-master 49 | branches: 50 | except: 51 | - gh-pages -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 momf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Do something 2 | -------------------------------------------------------------------------------- /altlib.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | trap 'echo setup script failed at line $LINENO' ERR 6 | 7 | git clone https://github.com/2vg/mofuparser 8 | 9 | git clone https://github.com/2vg/mofuhttputils 10 | 11 | mv mofuparser/src/mofuparser.nim ./src/ 12 | 13 | mv mofuparser/src/private/SIMD ./src/ 14 | 15 | mv mofuhttputils/src/mofuhttputils.nim ./src/ 16 | -------------------------------------------------------------------------------- /images/mofuw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2vg/mofuw/33e6c1c066129cd87bbf8e7b9b5818540001f71c/images/mofuw.png -------------------------------------------------------------------------------- /images/tokio-minihttp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2vg/mofuw/33e6c1c066129cd87bbf8e7b9b5818540001f71c/images/tokio-minihttp.png -------------------------------------------------------------------------------- /mofuw.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "2.0.0" 4 | author = "2vg" 5 | description = "more faster, ultra performance webserver" 6 | license = "MIT" 7 | srcDir = "src" 8 | skipDirs = @["images"] 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 0.18.0" 13 | requires "https://github.com/2vg/mofuparser" 14 | requires "https://github.com/2vg/mofuhttputils" -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | trap 'echo setup script failed at line $LINENO' ERR 6 | 7 | PWD="$(pwd)" 8 | 9 | if [ ! -d $PWD/"nim" ]; then 10 | git clone -b devel https://github.com/nim-lang/Nim.git nim 11 | pushd nim 12 | git clone --depth 1 https://github.com/nim-lang/csources.git 13 | pushd csources 14 | sh build.sh 15 | popd 16 | bin/nim c koch 17 | ./koch boot -d:release 18 | ./koch tools 19 | else 20 | pushd nim 21 | git fetch origin 22 | if ! git merge FETCH_HEAD | grep "Already up-to-date"; then 23 | bin/nim c koch 24 | ./koch boot -d:release 25 | fi 26 | fi 27 | popd 28 | -------------------------------------------------------------------------------- /src/mofuw.nim: -------------------------------------------------------------------------------- 1 | import uri, strtabs, critbits, asyncdispatch 2 | 3 | import 4 | mofuhttputils, 5 | mofuw/nest, 6 | mofuw/jesterUtils 7 | 8 | export 9 | uri, 10 | nest, 11 | strtabs, 12 | critbits, 13 | mofuhttputils, 14 | asyncdispatch 15 | 16 | import private/[ctx, ctxpool, route, handler, http, io, server] 17 | export ctx, http, io, server, route -------------------------------------------------------------------------------- /src/mofuw.nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on -------------------------------------------------------------------------------- /src/mofuw/jesterUtils.nim: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Dominik Picheta 2 | # MIT License - Look at license.txt for details. 3 | # modified by 2vg 4 | import parseutils, strtabs, strutils, tables 5 | from cgi import decodeUrl 6 | 7 | type 8 | MultiData* = OrderedTable[string, tuple[fields: StringTableRef, body: string]] 9 | 10 | proc parseUrlQuery*(query: string, result: var StringTableRef) = 11 | var i = 0 12 | i = query.skip("?") 13 | while i < query.len()-1: 14 | var key = "" 15 | var val = "" 16 | i += query.parseUntil(key, '=', i) 17 | if query[i] != '=': 18 | raise newException(ValueError, "Expected '=' at " & $i & 19 | " but got: " & $query[i]) 20 | inc(i) # Skip = 21 | i += query.parseUntil(val, '&', i) 22 | inc(i) # Skip & 23 | result[decodeUrl(key)] = decodeUrl(val) 24 | 25 | template parseContentDisposition(): typed = 26 | var hCount = 0 27 | while hCount < hValue.len()-1: 28 | var key = "" 29 | hCount += hValue.parseUntil(key, {';', '='}, hCount) 30 | if hValue[hCount] == '=': 31 | var value = hvalue.captureBetween('"', start = hCount) 32 | hCount += value.len+2 33 | inc(hCount) # Skip ; 34 | hCount += hValue.skipWhitespace(hCount) 35 | if key == "name": name = value 36 | newPart[0][key] = value 37 | else: 38 | inc(hCount) 39 | hCount += hValue.skipWhitespace(hCount) 40 | 41 | proc parseMultiPart*(body: string, boundary: string): MultiData = 42 | result = initOrderedTable[string, tuple[fields: StringTableRef, body: string]]() 43 | var mboundary = "--" & boundary 44 | 45 | var i = 0 46 | var partsLeft = true 47 | while partsLeft: 48 | var firstBoundary = body.skip(mboundary, i) 49 | if firstBoundary == 0: 50 | raise newException(ValueError, "Expected boundary. Got: " & body.substr(i, i+25)) 51 | i += firstBoundary 52 | i += body.skipWhitespace(i) 53 | 54 | # Headers 55 | var newPart: tuple[fields: StringTableRef, body: string] = ({:}.newStringTable, "") 56 | var name = "" 57 | while true: 58 | if body[i] == '\c': 59 | inc(i, 2) # Skip \c\L 60 | break 61 | var hName = "" 62 | i += body.parseUntil(hName, ':', i) 63 | if body[i] != ':': 64 | raise newException(ValueError, "Expected : in headers.") 65 | inc(i) # Skip : 66 | i += body.skipWhitespace(i) 67 | var hValue = "" 68 | i += body.parseUntil(hValue, {'\c', '\L'}, i) 69 | if toLowerAscii(hName) == "content-disposition": 70 | parseContentDisposition() 71 | newPart[0][hName] = hValue 72 | i += body.skip("\c\L", i) # Skip *one* \c\L 73 | 74 | # Parse body. 75 | while true: 76 | if body[i] == '\c' and body[i+1] == '\L' and 77 | body.skip(mboundary, i+2) != 0: 78 | if body.skip("--", i+2+mboundary.len) != 0: 79 | partsLeft = false 80 | break 81 | break 82 | else: 83 | newPart[1].add(body[i]) 84 | inc(i) 85 | i += body.skipWhitespace(i) 86 | 87 | result.add(name, newPart) 88 | 89 | proc parseMPFD*(contentType: string, body: string): MultiData = 90 | var boundaryEqIndex = contentType.find("boundary=")+9 91 | var boundary = contentType.substr(boundaryEqIndex, contentType.len()-1) 92 | return parseMultiPart(body, boundary) 93 | 94 | when not declared(tables.getOrDefault): 95 | template getOrDefault*(tab, key): untyped = tab[key] 96 | 97 | when isMainModule: 98 | var r = {:}.newStringTable 99 | parseUrlQuery("FirstName=Mickey", r) 100 | echo r -------------------------------------------------------------------------------- /src/mofuw/middleware/auth/README.md: -------------------------------------------------------------------------------- 1 | # mofuwAuth 2 | 3 | > implemention Session for like login system. 4 | 5 | ### warning 6 | 7 | if your gcc is very old, maybe cant compile this module. 8 | 9 | ### feature 10 | 11 | - gen random secure string for session key. 12 | - gen like cookie for session. 13 | - get session key from request header. 14 | 15 | ### example 16 | ```nim 17 | import mofuwAuth 18 | 19 | echo genSessionCookie() 20 | 21 | # value of MOFUW_SESSION is generated with a random 32 length value each time calling genSessionCookie. 22 | # 23 | # (name: "Set-Cookie", 24 | # value: "MOFUW_SESSION=lLLdcmi7KmwGxcR1qfyLRuu8XxKiAUtZ; HttpOnly", 25 | # session: "lLLdcmi7KmwGxcR1qfyLRuu8XxKiAUtZ") 26 | 27 | # return variable is tuple[name, value, session: string] 28 | # so, you can write this: 29 | # 30 | # let (x, y, z) = genSessionCookie() 31 | # echo x <- "Set-Cookie" 32 | # echo y <- "MOFUW_SESSION=lLLdcmi7KmwGxcR1qfyLRuu8XxKiAUtZ; HttpOnly" 33 | # echo z <- "lLLdcmi7KmwGxcR1qfyLRuu8XxKiAUtZ" 34 | ``` 35 | 36 | ### like login system 37 | 38 | implementing something like a login system is easy. 39 | 40 | (as long as you understand cookies and sessions) 41 | 42 | save the user information used for login in a file or DB and link it with the generated session key. 43 | 44 | after linking, add the session key to the response header. 45 | 46 | On the server side, by checking the client's header and obtaining the session key from the cookie, you can determine if there is a session key, check if it is a saved session key. 47 | 48 | i will proceed with development to make it easier to construct a login system with DB as the back end. -------------------------------------------------------------------------------- /src/mofuw/middleware/auth/mofuwAuth.nim: -------------------------------------------------------------------------------- 1 | import strtabs 2 | import randUtils 3 | 4 | from cookies import parseCookies 5 | 6 | const 7 | sessionName = "MOFUW_SESSION" 8 | 9 | proc setCookie*(name, value: string, 10 | expires = "", maxAges = "", 11 | domain = "", path = "", 12 | secure = false, httpOnly = false): 13 | tuple[name, value, session: string] = 14 | 15 | var session = "" 16 | 17 | session.add(name) 18 | session.add("=") 19 | session.add(value) 20 | 21 | if domain != "": 22 | session.add("; Domain=") 23 | session.add(domain) 24 | 25 | if path != "": 26 | session.add("; Path=") 27 | session.add(path) 28 | 29 | if expires != "": 30 | session.add("; Expires=") 31 | session.add(expires) 32 | 33 | if secure: 34 | session.add("; Secure") 35 | 36 | if httpOnly: 37 | session.add("; HttpOnly") 38 | 39 | result = ("Set-Cookie", session, value) 40 | 41 | proc setAuth*(name, value: string, 42 | expires = "", maxAges = "", 43 | domain = "", path = "", 44 | secure = false, httpOnly = true): 45 | tuple[name, value, session: string] = 46 | 47 | result = setCookie( 48 | name, 49 | value, 50 | expires, 51 | maxAges, 52 | domain, 53 | path, 54 | secure, 55 | httpOnly 56 | ) 57 | 58 | proc genSessionString*(len: int = 32): string = 59 | result = randString(len) 60 | 61 | proc getSession*(cookies: StringTableRef): string = 62 | if not cookies.hasKey(sessionName): 63 | return "" 64 | return cookies[sessionName] 65 | 66 | proc genSessionCookie*(): tuple[name, value, session: string] = 67 | setAuth(sessionName, genSessionString()) 68 | 69 | when isMainModule: 70 | echo genSessionCookie() -------------------------------------------------------------------------------- /src/mofuw/middleware/auth/randUtils.nim: -------------------------------------------------------------------------------- 1 | import random, sequtils 2 | 3 | randomize() 4 | 5 | const allC = {'0'..'9', 'a'..'z', 'A'..'Z', '#', '$', '%', '.'} 6 | 7 | var charArr: seq[char] = @[] 8 | 9 | for v in allC: 10 | charArr.add($v) 11 | 12 | proc randString*(len: int): string = 13 | result = "" 14 | 15 | for i in 1 .. len: 16 | shuffle(charArr) 17 | result.add($charArr[(rand(10000).int mod 66).int]) 18 | 19 | when isMainModule: 20 | echo randString(32) -------------------------------------------------------------------------------- /src/mofuw/nest.nim: -------------------------------------------------------------------------------- 1 | ## HTTP router based on routing trees 2 | 3 | import strutils, parseutils, strtabs, sequtils, httpcore 4 | import critbits 5 | import URI 6 | 7 | export URI, strtabs 8 | 9 | # 10 | # Type Declarations 11 | # 12 | 13 | const pathSeparator = '/' 14 | const allowedCharsInUrl = {'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~', pathSeparator} 15 | const wildcard = '*' 16 | const startParam = '{' 17 | const endParam = '}' 18 | const greedyIndicator = '$' 19 | const specialSectionStartChars = {pathSeparator, wildcard, startParam} 20 | const allowedCharsInPattern = allowedCharsInUrl + {wildcard, startParam, endParam, greedyIndicator} 21 | 22 | 23 | type 24 | HttpVerb* = enum ## Available methods to associate a mapped handler with 25 | GET = "get" 26 | HEAD = "head" 27 | OPTIONS = "options" 28 | PUT = "put" 29 | POST = "post" 30 | DELETE = "delete" 31 | 32 | PatternMatchingType = enum ## Kinds of elements that may appear in a mapping 33 | ptrnWildcard 34 | ptrnParam 35 | ptrnText 36 | ptrnStartHeaderConstraint 37 | ptrnEndHeaderConstraint 38 | 39 | # Structures for setting up the mappings 40 | MapperKnot = object ## A token within a URL to be mapped. The URL is broken into 'knots' that make up a 'rope' (``seq[MapperKnot]``) 41 | isGreedy : bool 42 | case kind : PatternMatchingType: 43 | of ptrnParam, ptrnText: 44 | value : string 45 | of ptrnWildcard, ptrnEndHeaderConstraint: 46 | discard 47 | of ptrnStartHeaderConstraint: 48 | headerName : string 49 | 50 | # Structures for holding fully parsed mappings 51 | PatternNode[H] = ref object ## A node within a routing tree, usually constructed from a ``MapperKnot`` 52 | isGreedy : bool 53 | case kind : PatternMatchingType: # TODO: should be able to extend MapperKnot to get this, compiler wont let me, investigate further. Nim compiler bug maybe? 54 | of ptrnParam, ptrnText: 55 | value : string 56 | of ptrnWildcard, ptrnEndHeaderConstraint: 57 | discard 58 | of ptrnStartHeaderConstraint: 59 | headerName : string 60 | case isLeaf : bool: #a leaf node is one with no children 61 | of true: 62 | discard 63 | of false: 64 | children : seq[PatternNode[H]] 65 | case isTerminator : bool: # a terminator is a node that can be considered a mapping on its own, matching could stop at this node or continue. If it is not a terminator, matching can only continue 66 | of true: 67 | handler : H 68 | of false: 69 | discard 70 | 71 | 72 | # Router Structures 73 | Router*[H] = ref object ## Container that holds HTTP mappings to handler procs 74 | verbTrees : CritBitTree[PatternNode[H]] 75 | 76 | RoutingArgs* = object ## Arguments extracted from a request while routing it 77 | pathArgs* : StringTableRef 78 | queryArgs* : StringTableRef 79 | 80 | RoutingResultType* = enum ## Possible results of a routing operation 81 | routingSuccess 82 | routingFailure 83 | RoutingResult*[H] = object ## Encapsulates the results of a routing operation 84 | case status* : RoutingResultType: 85 | of routingSuccess: 86 | handler* : H 87 | arguments* : RoutingArgs 88 | of routingFailure: 89 | discard 90 | 91 | # Exceptions 92 | MappingError* = object of Exception ## Indicates an error while creating a new mapping 93 | 94 | # 95 | # Stringification / other operators 96 | # 97 | proc `$`(piece : MapperKnot) : string = 98 | case piece.kind: 99 | of ptrnParam, ptrnText: 100 | result = $(piece.kind) & ":" & piece.value 101 | of ptrnWildcard, ptrnEndHeaderConstraint: 102 | result = $(piece.kind) 103 | of ptrnStartHeaderConstraint: 104 | result = $(piece.kind) & ":" & piece.headerName 105 | proc `$`[H](node : PatternNode[H]) : string = 106 | case node.kind: 107 | of ptrnParam, ptrnText: 108 | result = $(node.kind) & ":value=" & node.value & ", " 109 | of ptrnWildcard, ptrnEndHeaderConstraint: 110 | result = $(node.kind) & ":" 111 | of ptrnStartHeaderConstraint: 112 | result = $(node.kind) & ":" & node.headerName & ", " 113 | result = result & "leaf=" & $node.isLeaf & ", terminator=" & $node.isTerminator & ", greedy=" & $node.isGreedy 114 | proc `==`[H](node : PatternNode[H], knot : MapperKnot) : bool = 115 | result = (node.kind == knot.kind) 116 | 117 | if (result): 118 | case node.kind: 119 | of ptrnText, ptrnParam: 120 | result = (node.value == knot.value) 121 | of ptrnStartHeaderConstraint: 122 | result = (node.headerName == knot.headerName) 123 | else: 124 | discard 125 | 126 | # 127 | # Debugging routines 128 | # 129 | 130 | proc printRoutingTree[H](node : PatternNode[H], tabs : int = 0) = 131 | debugEcho ' '.repeat(tabs), $node 132 | if not node.isLeaf: 133 | for child in node.children: 134 | printRoutingTree(child, tabs + 1) 135 | 136 | proc printRoutingTree*[H](router : Router[H]) = 137 | for verb, tree in pairs(router.verbTrees): 138 | debugEcho verb.toUpper() 139 | printRoutingTree(tree) 140 | 141 | # 142 | # Constructors 143 | # 144 | proc newRouter*[H]() : Router[H] = 145 | ## Creates a new ``Router`` instance 146 | result = Router[H](verbTrees:CritBitTree[PatternNode[H]]()) 147 | 148 | # 149 | # Rope procedures. A rope is a chain of tokens representing the url 150 | # 151 | 152 | proc ensureCorrectRoute( 153 | path : string 154 | ) : string {.noSideEffect, raises:[MappingError].} = 155 | ## Verifies that this given path is a valid path, strips trailing slashes, and guarantees leading slashes 156 | if(not path.allCharsInSet(allowedCharsInPattern)): 157 | raise newException(MappingError, "Illegal characters occurred in the mapped pattern, please restrict to alphanumerics, or the following: - . _ ~ /") 158 | 159 | result = path 160 | 161 | if result.len == 1 and result[0] == '/': 162 | return 163 | if result[^1] == pathSeparator: #patterns should not end in a separator, it's redundant 164 | result = result[0..^2] 165 | if not (result[0] == '/'): #ensure each pattern is relative to root 166 | result.insert("/") 167 | 168 | proc emptyKnotSequence( 169 | knotSeq : seq[MapperKnot] 170 | ) : bool {.noSideEffect.} = 171 | ## A knot sequence is empty if it A) contains no elements or B) it contains a single text element with no value 172 | result = (knotSeq.len == 0 or (knotSeq.len == 1 and knotSeq[0].kind == ptrnText and knotSeq[0].value == "")) 173 | 174 | proc generateRope( 175 | pattern : string, 176 | startIndex : int = 0 177 | ) : seq[MapperKnot] {.noSideEffect, raises: [MappingError].} = 178 | ## Translates the string form of a pattern into a sequence of MapperKnot objects to be parsed against 179 | var token : string 180 | let tokenSize = pattern.parseUntil(token, specialSectionStartChars, startIndex) 181 | var newStartIndex = startIndex + tokenSize 182 | 183 | if newStartIndex < pattern.len: # we encountered a wildcard or parameter def, there could be more left 184 | let specialChar = pattern[newStartIndex] 185 | newStartIndex += 1 186 | 187 | var scanner : MapperKnot 188 | 189 | if specialChar == wildcard: 190 | if pattern[newStartIndex] == greedyIndicator: 191 | newStartIndex += 1 192 | if pattern.len != newStartIndex: 193 | raise newException(MappingError, "$ found before end of route") 194 | scanner = MapperKnot(kind:ptrnWildcard, isGreedy:true) 195 | else: 196 | scanner = MapperKnot(kind:ptrnWildcard) 197 | elif specialChar == startParam: 198 | var paramName : string 199 | let paramNameSize = pattern.parseUntil(paramName, endParam, newStartIndex) 200 | newStartIndex += (paramNameSize + 1) 201 | if pattern.len > newStartIndex and pattern[newStartIndex] == greedyIndicator: 202 | newStartIndex += 1 203 | if pattern.len != newStartIndex: 204 | raise newException(MappingError, "$ found before end of route") 205 | scanner = MapperKnot(kind:ptrnParam, value:paramName, isGreedy:true) 206 | else: 207 | scanner = MapperKnot(kind:ptrnParam, value:paramName) 208 | elif specialChar == pathSeparator: 209 | scanner = MapperKnot(kind:ptrnText, value:($pathSeparator)) 210 | else: 211 | raise newException(MappingError, "Unrecognized special character") 212 | 213 | var prefix : seq[MapperKnot] 214 | if tokenSize > 0: 215 | prefix = @[MapperKnot(kind:ptrnText, value:token), scanner] 216 | else: 217 | prefix = @[scanner] 218 | 219 | let suffix = generateRope(pattern, newStartIndex) 220 | 221 | if emptyKnotSequence(suffix): 222 | return prefix 223 | else: 224 | return concat(prefix, suffix) 225 | 226 | else: #no more wildcards or parameter defs, the rest is static text 227 | result = newSeq[MapperKnot](token.len) 228 | for i, c in pairs(token): 229 | result[i] = MapperKnot(kind:ptrnText, value:($c)) 230 | 231 | # 232 | # Node procedures. A pattern node represents part of a chain representing a matchable path 233 | # 234 | 235 | proc terminatingPatternNode[H]( 236 | oldNode : PatternNode[H], 237 | knot : MapperKnot, 238 | handler : H 239 | ) : PatternNode[H] {.raises: [MappingError].} = 240 | ## Turns the given node into a terminating node ending at the given knot/handler pair. If it is already a terminator, throws an exception 241 | if oldNode.isTerminator: # Already mapped 242 | raise newException(MappingError, "Duplicate mapping detected") 243 | case knot.kind: 244 | of ptrnText: 245 | result = PatternNode[H](kind: ptrnText, value: knot.value, isLeaf: oldNode.isLeaf, isTerminator: true, handler: handler) 246 | of ptrnParam: 247 | result = PatternNode[H](kind: ptrnParam, value: knot.value, isLeaf: oldNode.isLeaf, isTerminator: true, handler: handler, isGreedy : knot.isGreedy) 248 | of ptrnWildcard: 249 | result = PatternNode[H](kind: ptrnWildcard, isLeaf: oldNode.isLeaf, isTerminator: true, handler: handler, isGreedy : knot.isGreedy) 250 | of ptrnEndHeaderConstraint: 251 | result = PatternNode[H](kind: ptrnEndHeaderConstraint, isLeaf: oldNode.isLeaf, isTerminator: true, handler: handler) 252 | of ptrnStartHeaderConstraint: 253 | result = PatternNode[H](kind: ptrnStartHeaderConstraint, headerName: knot.headerName, isLeaf: oldNode.isLeaf, isTerminator: true, handler: handler) 254 | 255 | result.handler = handler 256 | 257 | if not result.isLeaf: 258 | result.children = oldNode.children 259 | 260 | proc parentalPatternNode[H](oldNode : PatternNode[H]) : PatternNode[H] = 261 | ## Turns the given node into a parent node. If it not a leaf node, this returns a new copy of the input. 262 | case oldNode.kind: 263 | of ptrnText: 264 | result = PatternNode[H](kind: ptrnText, value: oldNode.value, isLeaf: false, children: newSeq[PatternNode[H]](), isTerminator: oldNode.isTerminator) 265 | of ptrnParam: 266 | result = PatternNode[H](kind: ptrnParam, value: oldNode.value, isLeaf: false, children: newSeq[PatternNode[H]](), isTerminator: oldNode.isTerminator, isGreedy: oldNode.isGreedy) 267 | of ptrnWildcard: 268 | result = PatternNode[H](kind: ptrnWildcard, isLeaf: false, children: newSeq[PatternNode[H]](), isTerminator: oldNode.isTerminator, isGreedy: oldNode.isGreedy) 269 | of ptrnEndHeaderConstraint: 270 | result = PatternNode[H](kind: ptrnEndHeaderConstraint, isLeaf: false, children: newSeq[PatternNode[H]](), isTerminator: oldNode.isTerminator) 271 | of ptrnStartHeaderConstraint: 272 | result = PatternNode[H](kind: ptrnStartHeaderConstraint, headerName: oldNode.headerName, isLeaf: false, children: newSeq[PatternNode[H]](), isTerminator: oldNode.isTerminator) 273 | 274 | if result.isTerminator: 275 | result.handler = oldNode.handler 276 | 277 | proc indexOf[H](nodes : seq[PatternNode[H]], knot : MapperKnot) : int = 278 | ## Finds the index of nodes that matches the given knot. If none is found, returns -1 279 | for index, child in pairs(nodes): 280 | if child == knot: 281 | return index 282 | return -1 #the 'not found' value 283 | 284 | proc chainTree[H](rope : seq[MapperKnot], handler : H) : PatternNode[H] = 285 | ## Creates a tree made up of single-child nodes that matches the given rope. The last node in the tree is a terminator with the given handler. 286 | let knot = rope[0] 287 | let lastKnot = (rope.len == 1) #since this is a chain tree, the only leaf node is the terminator node, so they are mutually linked, if this is true then it is both 288 | 289 | case knot.kind: 290 | of ptrnText: 291 | result = PatternNode[H](kind: ptrnText, value: knot.value, isLeaf: lastKnot, isTerminator: lastKnot) 292 | of ptrnParam: 293 | result = PatternNode[H](kind: ptrnParam, value: knot.value, isLeaf: lastKnot, isTerminator: lastKnot, isGreedy: knot.isGreedy) 294 | of ptrnWildcard: 295 | result = PatternNode[H](kind: ptrnWildcard, isLeaf: lastKnot, isTerminator: lastKnot, isGreedy: knot.isGreedy) 296 | of ptrnEndHeaderConstraint: 297 | result = PatternNode[H](kind: ptrnEndHeaderConstraint, isLeaf: lastKnot, isTerminator: lastKnot) 298 | of ptrnStartHeaderConstraint: 299 | result = PatternNode[H](kind: ptrnStartHeaderConstraint, headerName: knot.headerName, isLeaf: lastKnot, isTerminator: lastKnot) 300 | 301 | if lastKnot: 302 | result.handler = handler 303 | else: 304 | result.children = @[chainTree(rope[1.. ^1], handler)] #continue the chain 305 | 306 | proc merge[H]( 307 | node : PatternNode[H], 308 | rope : seq[MapperKnot], 309 | handler : H 310 | ) : PatternNode[H] {.noSideEffect, raises: [MappingError].} = 311 | ## Merges the given sequence of MapperKnots into the given tree as a new mapping. This does not mutate the given node, instead it will return a new one 312 | if rope.len == 1: # Terminating knot reached, finish the merge 313 | result = terminatingPatternNode(node, rope[0], handler) 314 | else: 315 | let currentKnot = rope[0] 316 | let nextKnot = rope[1] 317 | let remainder = rope[1.. ^1] 318 | 319 | assert node == currentKnot 320 | 321 | var childIndex = -1 322 | if node.isLeaf: #node isn't a parent yet, make it one to continue the process 323 | result = parentalPatternNode(node) 324 | else: 325 | result = node 326 | childIndex = node.children.indexOf(nextKnot) 327 | 328 | if childIndex == -1: # the next knot doesn't map to a child of this node, needs to me directly translated into a deep tree (one branch per level) 329 | result.children.add(chainTree(remainder, handler)) # make a node containing everything remaining and inject it 330 | else: 331 | result.children[childIndex] = merge(result.children[childIndex], remainder, handler) 332 | 333 | proc contains[H]( 334 | node : PatternNode[H], 335 | rope : seq[MapperKnot] 336 | ) : bool {.noSideEffect.} = 337 | ## Determines whether or not merging rope into node will create a mapping conflict 338 | if rope.len == 0: return 339 | let knot = rope[0] 340 | 341 | # Is this node equal to the knot? 342 | if node.kind == knot.kind: 343 | if node.kind == ptrnText: 344 | result = (node.value == knot.value) 345 | elif node.kind == ptrnStartHeaderConstraint: 346 | result = (node.headerName == knot.headerName) 347 | else: 348 | result = true 349 | else: 350 | if 351 | (node.kind == ptrnWildcard and knot.kind == ptrnParam) or 352 | (node.kind == ptrnParam and knot.kind == ptrnWildcard) or 353 | (node.kind == ptrnWildcard and knot.kind == ptrnText) or 354 | (node.kind == ptrnParam and knot.kind == ptrnText) or 355 | (node.kind == ptrnText and knot.kind == ptrnParam) or 356 | (node.kind == ptrnText and knot.kind == ptrnWildcard): 357 | result = true 358 | else: 359 | result = false 360 | 361 | if not node.isLeaf and result: # if the node has kids, is at least one qual? 362 | if node.children.len > 0: 363 | result = false # false until proven otherwise 364 | for child in node.children: 365 | if child.contains(rope[1.. ^1]): # does the child match the rest of the rope? 366 | result = true 367 | break 368 | elif node.isLeaf and rope.len > 1: # the node is a leaf but we want to map further to it, so it won't conflict 369 | result = false 370 | 371 | # 372 | # Mapping procedures 373 | # 374 | 375 | proc map*[H]( 376 | router : Router[H], 377 | handler : H, 378 | verb: string, 379 | pattern : string, 380 | headers : HttpHeaders = nil 381 | ) {.noSideEffect.} = 382 | ## Add a new mapping to the given ``Router`` instance 383 | var rope = generateRope(ensureCorrectRoute(pattern)) # initial rope 384 | 385 | if not headers.isNil: # extend the rope with any header constraints 386 | for key, value in pairs(headers): 387 | rope.add(MapperKnot(kind:ptrnStartHeaderConstraint, headerName:key)) 388 | rope = concat(rope, generateRope(value)) 389 | rope.add(MapperKnot(kind:ptrnEndHeaderConstraint)) 390 | 391 | var nodeToBeMerged : PatternNode[H] 392 | if router.verbTrees.hasKey(verb): 393 | nodeToBeMerged = router.verbTrees[verb] 394 | if nodeToBeMerged.contains(rope): 395 | raise newException(MappingError, "Duplicate mapping encountered: " & pattern) 396 | else: 397 | nodeToBeMerged = PatternNode[H](kind:ptrnText, value:($pathSeparator), isLeaf:true, isTerminator:false) 398 | 399 | router.verbTrees[verb] = nodeToBeMerged.merge(rope, handler) 400 | 401 | # 402 | # Data extractors and utilities 403 | # 404 | 405 | proc extractEncodedParams(input : string) : StringTableRef {.noSideEffect.} = 406 | var index = 0 407 | result = newStringTable() 408 | 409 | while index < input.len: 410 | var paramValuePair : string 411 | let pairSize = input.parseUntil(paramValuePair, '&', index) 412 | 413 | index += pairSize + 1 414 | 415 | let equalIndex = paramValuePair.find('=') 416 | 417 | if equalIndex == -1: #no equals, just a boolean "existance" variable 418 | result[paramValuePair] = "" #just insert a record into the param table to indicate that it exists 419 | else: #is a 'setter' parameter 420 | let paramName = paramValuePair[0..equalIndex - 1] 421 | let paramValue = paramValuePair[equalIndex + 1.. ^1] 422 | result[paramName] = paramValue 423 | 424 | # 425 | # Compression routines, compression makes matching more efficient. Once compressed, a router is immutable 426 | # 427 | proc compress[H](node : PatternNode[H]) : PatternNode[H] = 428 | ## Finds sequences of single ptrnText nodes and combines them to reduce the depth of the tree 429 | if node.isLeaf: #if it's a leaf, there are clearly no descendents, and if it is a terminator then compression will alter the behavior 430 | return node 431 | elif node.kind == ptrnText and not node.isTerminator and node.children.len == 1: 432 | let compressedChild = compress(node.children[0]) 433 | if compressedChild.kind == ptrnText: 434 | result = compressedChild 435 | result.value = node.value & compressedChild.value 436 | return 437 | 438 | result = node 439 | result.children = map(result.children, compress) 440 | 441 | proc compress*[H](router : Router[H]) = 442 | ## Compresses the entire contents of the given ``Router``. Successive calls will recompress, but may not be efficient, so use this only when mapping is complete for the best effect 443 | for index, existing in pairs(router.verbTrees): 444 | router.verbTrees[index] = compress(existing) 445 | 446 | # 447 | # Procedures to match against paths 448 | # 449 | 450 | proc matchTree[H]( 451 | head : PatternNode[H], 452 | path : string, 453 | headers : HttpHeaders, 454 | pathIndex : int = 0, 455 | pathArgs : StringTableRef = newStringTable() 456 | ) : RoutingResult[H] {.noSideEffect.} = 457 | ## Check whether the given path matches the given tree node starting from pathIndex 458 | var node = head 459 | var pathIndex = pathIndex 460 | 461 | block matching: 462 | while pathIndex >= 0: 463 | case node.kind: 464 | of ptrnText: 465 | if path.continuesWith(node.value, pathIndex): 466 | pathIndex += node.value.len 467 | else: 468 | break matching 469 | of ptrnWildcard: 470 | if node.isGreedy: 471 | pathIndex = path.len 472 | else: 473 | pathIndex = path.find(pathSeparator, pathIndex) #skip forward to the next separator 474 | if pathIndex == -1: 475 | pathIndex = path.len 476 | of ptrnParam: 477 | if node.isGreedy: 478 | pathArgs[node.value] = path[pathIndex.. ^1] 479 | pathIndex = path.len 480 | else: 481 | let newPathIndex = path.find(pathSeparator, pathIndex) #skip forward to the next separator 482 | if newPathIndex == -1: 483 | pathArgs[node.value] = path[pathIndex.. ^1] 484 | pathIndex = path.len 485 | else: 486 | pathArgs[node.value] = path[pathIndex..newPathIndex - 1] 487 | pathIndex = newPathIndex 488 | of ptrnStartHeaderConstraint: 489 | for child in node.children: 490 | var p = "" 491 | if not headers.isNil: 492 | p = toString(headers.getOrDefault(node.headerName)) 493 | let childResult = child.matchTree( 494 | path=p, 495 | pathIndex=0, 496 | headers=headers 497 | ) 498 | 499 | if childResult.status == routingSuccess: 500 | for key, value in childResult.arguments.pathArgs: 501 | pathArgs[key] = value 502 | return RoutingResult[H]( 503 | status:routingSuccess, 504 | handler:childResult.handler, 505 | arguments:RoutingArgs(pathArgs:pathArgs) 506 | ) 507 | return RoutingResult[H](status:routingFailure) 508 | of ptrnEndHeaderConstraint: 509 | if node.isTerminator and node.isLeaf: 510 | return RoutingResult[H]( 511 | status:routingSuccess, 512 | handler:node.handler, 513 | arguments:RoutingArgs(pathArgs:pathArgs) 514 | ) 515 | 516 | if pathIndex == path.len and node.isTerminator: #the path was exhausted and we reached a node that has a handler 517 | return RoutingResult[H]( 518 | status:routingSuccess, 519 | handler:node.handler, 520 | arguments:RoutingArgs(pathArgs:pathArgs) 521 | ) 522 | elif not node.isLeaf: #there is children remaining, could match against children 523 | if node.children.len == 1: #optimization for single child that just points the node forward 524 | node = node.children[0] 525 | else: #more than one child 526 | assert node.children.len != 0 527 | for child in node.children: 528 | result = child.matchTree(path, headers, pathIndex, pathArgs) 529 | if result.status == routingSuccess: 530 | return 531 | break matching #none of the children matched, assume no match 532 | else: #its a leaf and we havent' satisfied the path yet, let the last line handle returning 533 | break matching 534 | 535 | result = RoutingResult[H](status:routingFailure) 536 | 537 | proc route*[H]( 538 | router : Router[H], 539 | requestMethod : string, 540 | requestUri : URI, 541 | requestHeaders : HttpHeaders = newHttpHeaders(), 542 | ) : RoutingResult[H] {.noSideEffect.} = 543 | ## Find a mapping that matches the given request description 544 | try: 545 | let verb = requestMethod.toLowerAscii() 546 | 547 | if router.verbTrees.hasKey(verb): 548 | result = matchTree(router.verbTrees[verb], ensureCorrectRoute(requestUri.path), requestHeaders) 549 | 550 | if result.status == routingSuccess: 551 | result.arguments.queryArgs = extractEncodedParams(requestUri.query) 552 | else: 553 | result = RoutingResult[H](status:routingFailure) 554 | except MappingError: 555 | result = RoutingResult[H](status:routingFailure) 556 | 557 | proc route*[H]( 558 | router : Router[H], 559 | requestMethod : HttpMethod, 560 | requestUri : URI, 561 | requestHeaders : HttpHeaders = newHttpHeaders(), 562 | ) : RoutingResult[H] {.noSideEffect.} = 563 | ## Simple wrapper around the regular route function 564 | route(router, $requestMethod, requestUri, requestHeaders) -------------------------------------------------------------------------------- /src/mofuw/private/websocket/hex.nim: -------------------------------------------------------------------------------- 1 | proc nibbleFromChar(c: char): int = 2 | case c 3 | of '0'..'9': result = ord(c) - ord('0') 4 | of 'a'..'f': result = ord(c) - ord('a') + 10 5 | of 'A'..'F': result = ord(c) - ord('A') + 10 6 | else: discard 255 7 | 8 | proc decodeHex*(str: string): string = 9 | let length = len(str) div 2 10 | result = newString(length) 11 | for i in 0.. 125 and length <= 0x7fff: b1 = 126u8 74 | else: b1 = 127u8 75 | 76 | let b1unmasked = b1 77 | if f.masked: b1 = b1 or (1 shl 7) 78 | 79 | ret.write(b1) 80 | 81 | if b1unmasked == 126u8: 82 | ret.write(length.uint16.htons) 83 | elif b1unmasked == 127u8: 84 | ret.write(length.uint64.htonll) 85 | 86 | var data = f.data 87 | 88 | if f.masked: 89 | # TODO: proper rng 90 | 91 | # for compatibility with renaming of random 92 | template rnd(x: untyped): untyped = 93 | when declared(random.rand): 94 | rand(x-1) 95 | else: 96 | random(x) 97 | 98 | randomize() 99 | let maskingKey = [ rnd(256).char, rnd(256).char, 100 | rnd(256).char, rnd(256).char ] 101 | 102 | for i in 0.. high(int).uint64: 162 | raise newException(IOError, "websocket payload too large") 163 | 164 | realLen.int 165 | else: hdrLen 166 | 167 | f.masked = (b1 and 0x80) == 0x80 168 | var maskingKey = "" 169 | if f.masked: 170 | maskingKey = await ws.recv(4, {}) 171 | # maskingKey = cast[ptr uint32](lenstr[0].addr)[] 172 | 173 | f.data = await ws.recv(finalLen, {}) 174 | if f.data.len != finalLen: raise newException(IOError, "socket closed") 175 | 176 | if f.masked: 177 | for i in 0..= 2: 188 | cast[ptr uint16](addr data[0])[].htons.int 189 | else: 190 | 0 191 | result.reason = if len > 2: data[2..^1] else: "" 192 | 193 | # Internal hashtable that tracks pings sent out, per socket. 194 | # key is the socket fd 195 | type PingRequest = Future[void] # tuple[data: string, fut: Future[void]] 196 | 197 | var reqPing {.threadvar.}: Option[Table[int, PingRequest]] 198 | 199 | proc readData*(ws: AsyncSocket, isClientSocket: bool): 200 | Future[tuple[opcode: Opcode, data: string]] {.async.} = 201 | 202 | ## Reads reassembled data off the websocket and give you joined frame data. 203 | ## 204 | ## Note: You will still see control frames, but they are all handled for you 205 | ## (Ping/Pong, Cont, Close, and so on). 206 | ## 207 | ## The only ones you need to care about are Opcode.Text and Opcode.Binary, the 208 | ## so-called application frames. 209 | ## 210 | ## As per the websocket specifications, all clients need to mask their responses. 211 | ## It is up to you to to set `isClientSocket` with a proper value, depending on 212 | ## if you are reading from a server or client socket. 213 | ## 214 | ## Will raise IOError when the socket disconnects and ProtocolError on any 215 | ## websocket-related issues. 216 | 217 | var resultData = "" 218 | var resultOpcode: Opcode 219 | 220 | if reqPing.isNone: 221 | reqPing = some(initTable[int, PingRequest]()) 222 | 223 | var pingTable = reqPing.unsafeGet() 224 | 225 | while true: 226 | let f = await ws.recvFrame() 227 | # Merge sequentially read frames. 228 | resultData.add(f.data) 229 | 230 | case f.opcode 231 | of Opcode.Ping: 232 | await ws.send(makeFrame(Opcode.Pong, f.data, isClientSocket)) 233 | 234 | of Opcode.Pong: 235 | if pingTable.hasKey(ws.getFD().AsyncFD.int): 236 | pingTable[ws.getFD().AsyncFD.int].complete() 237 | 238 | else: discard # thanks, i guess? 239 | 240 | of Opcode.Cont: 241 | if not f.fin: continue 242 | 243 | of Opcode.Text, Opcode.Binary, Opcode.Close: 244 | resultOpcode = f.opcode 245 | # read another! 246 | if not f.fin: continue 247 | 248 | else: 249 | ws.close() 250 | raise newException(ProtocolError, "received invalid opcode: " & $f.opcode) 251 | 252 | # handle case: ping never arrives and client closes the connection 253 | if resultOpcode == Opcode.Close and pingTable.hasKey(ws.getFD().AsyncFD.int): 254 | let closeData = extractCloseData(resultData) 255 | let ex = newException(IOError, "socket closed while waiting for pong") 256 | if closeData.code != 0: 257 | ex.msg.add(", close code: " & $closeData.code) 258 | if closeData.reason != "": 259 | ex.msg.add(", reason: " & closeData.reason) 260 | pingTable[ws.getFD().AsyncFD.int].fail(ex) 261 | pingTable.del(ws.getFD().AsyncFD.int) 262 | 263 | return (resultOpcode, resultData) 264 | 265 | proc sendText*(ws: AsyncSocket, p: string, masked: bool = false): Future[void] {.async.} = 266 | ## Sends text data. Will only return after all data has been sent out. 267 | await ws.send(makeFrame(Opcode.Text, p, masked)) 268 | 269 | proc sendBinary*(ws: AsyncSocket, p: string, masked: bool = false): Future[void] {.async.} = 270 | ## Sends binary data. Will only return after all data has been sent out. 271 | await ws.send(makeFrame(Opcode.Binary, p, masked)) 272 | 273 | proc sendPing*(ws: AsyncSocket, masked: bool = false, token: string = ""): Future[void] {.async.} = 274 | ## Sends a WS ping message. 275 | ## Will generate a suitable token if you do not provide one. 276 | 277 | let pingId = if token == "": $genOid() else: token 278 | await ws.send(makeFrame(Opcode.Ping, pingId, masked)) 279 | 280 | # Old crud: send/wait. Very deadlocky. 281 | # let start = epochTime() 282 | # let pingId: string = $genOid() 283 | # var fut = newFuture[void]() 284 | # await ws.send(makeFrame(Opcode.Ping, pingId)) 285 | # reqPing[ws.getFD().AsyncFD.int] = fut 286 | # echo "waiting" 287 | # await fut 288 | # reqPing.del(ws.getFD().AsyncFD.int) 289 | # result = ((epochTime() - start).float64 * 1000).int 290 | 291 | proc readData*(ws: AsyncWebSocket): Future[tuple[opcode: Opcode, data: string]] = 292 | ## Reads reassembled data off the websocket and give you joined frame data. 293 | ## 294 | ## Note: You will still see control frames, but they are all handled for you 295 | ## (Ping/Pong, Cont, Close, and so on). 296 | ## 297 | ## The only ones you need to care about are Opcode.Text and Opcode.Binary, the 298 | ## so-called application frames. 299 | ## 300 | ## Will raise IOError when the socket disconnects and ProtocolError on any 301 | ## websocket-related issues. 302 | 303 | result = readData(ws.sock, ws.kind == SocketKind.Client) 304 | 305 | proc sendText*(ws: AsyncWebSocket, p: string, masked: bool = false): Future[void] = 306 | ## Sends text data. Will only return after all data has been sent out. 307 | result = sendText(ws.sock, p, masked) 308 | 309 | proc sendBinary*(ws: AsyncWebSocket, p: string, masked: bool = false): Future[void] = 310 | ## Sends binary data. Will only return after all data has been sent out. 311 | result = sendBinary(ws.sock, p, masked) 312 | 313 | proc sendPing*(ws: AsyncWebSocket, masked: bool = false, token: string = ""): Future[void] = 314 | ## Sends a WS ping message. 315 | ## Will generate a suitable token if you do not provide one. 316 | result = sendPing(ws.sock, masked, token) 317 | 318 | proc closeWebsocket*(ws: AsyncSocket, code = 0, reason = ""): Future[void] {.async.} = 319 | ## Closes the socket. 320 | 321 | defer: ws.close() 322 | 323 | var data = newStringStream() 324 | 325 | if code != 0: 326 | data.write(code.uint16) 327 | 328 | if reason != "": 329 | data.write(reason) 330 | 331 | await ws.send(makeFrame(Opcode.Close, data.readAll(), true)) 332 | 333 | proc close*(ws: AsyncWebSocket, code = 0, reason = ""): Future[void] = 334 | ## Closes the socket. 335 | result = ws.sock.closeWebsocket(code, reason) -------------------------------------------------------------------------------- /src/mofuw/websocket.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch, private/websocket/[wsshared, wsserver] 2 | 3 | export wsshared, wsserver -------------------------------------------------------------------------------- /src/private/ctx.nim: -------------------------------------------------------------------------------- 1 | import strtabs, critbits, asyncdispatch 2 | import mofuparser 3 | 4 | when defined ssl: 5 | import os, net, openssl, nativesockets 6 | export openssl 7 | export net.SslCVerifyMode 8 | 9 | when not declared(SSL_set_SSL_CTX): 10 | proc SSL_set_SSL_CTX*(ssl: SslPtr, ctx: SslCtx): SslCtx 11 | {.cdecl, dynlib: DLLSSLName, importc.} 12 | 13 | type 14 | MofuwHandler* = proc(ctx: MofuwCtx): Future[void] {.gcsafe.} 15 | 16 | VhostTable* = CritBitTree[MofuwHandler] 17 | 18 | ServeCtx* = ref object 19 | servername*: string 20 | port*: int 21 | readBufferSize*, writeBufferSize*, maxBodySize*: int 22 | timeout*: int 23 | poolsize*: int 24 | handler*, hookrequest*, hookresponse*: MofuwHandler 25 | vhostTbl*: VhostTable 26 | isSSL*: bool 27 | when defined ssl: 28 | sslCtxTbl*: CritBitTree[SslCtx] 29 | 30 | MofuwCtx* = ref object 31 | fd*: AsyncFD 32 | mhr*: MPHTTPReq 33 | mc*: MPChunk 34 | ip*: string 35 | buf*, resp*: string 36 | bufLen*, respLen*: int 37 | currentBufPos*: int 38 | bodyStart*: int 39 | maxBodySize*: int 40 | bodyParams*, uriParams*, uriQuerys*: StringTableRef 41 | vhostTbl*: VhostTable 42 | when defined ssl: 43 | isSSL*: bool 44 | sslCtx*: SslCtx 45 | sslHandle*: SslPtr 46 | 47 | proc newServeCtx*(servername = "mofuw", port: int, 48 | handler, 49 | hookrequest, hookresponse: MofuwHandler = nil, 50 | readBufferSize, writeBufferSize = 4096, 51 | maxBodySize = 1024 * 1024 * 5, 52 | timeout = -1, 53 | poolsize = 128, 54 | isSSL = false): ServeCtx = 55 | result = ServeCtx( 56 | servername: servername, 57 | port: port, 58 | handler: handler, 59 | readBufferSize: readBufferSize, 60 | writeBufferSize: writeBufferSize, 61 | maxBodySize: maxBodySize, 62 | timeout: timeout, 63 | poolsize: poolsize, 64 | isSSL: isSSL 65 | ) 66 | 67 | proc newMofuwCtx*(readSize: int, writeSize: int): MofuwCtx = 68 | result = MofuwCtx( 69 | buf: newString(readSize), 70 | resp: newString(writeSize), 71 | bufLen: 0, 72 | respLen: 0, 73 | currentBufPos: 0, 74 | mhr: MPHTTPReq() 75 | ) 76 | 77 | when defined ssl: 78 | # https://mozilla.github.io/server-side-tls/ssl-config-generator/?hsts=no 79 | const strongCipher = 80 | "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:" & 81 | "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" & 82 | "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" 83 | 84 | SSL_library_init() 85 | 86 | # ## 87 | # From net module 88 | # ## 89 | proc loadCertificates(ctx: SSL_CTX, certFile, keyFile: string) = 90 | if certFile != "" and (not existsFile(certFile)): 91 | raise newException(system.IOError, "Certificate file could not be found: " & certFile) 92 | if keyFile != "" and (not existsFile(keyFile)): 93 | raise newException(system.IOError, "Key file could not be found: " & keyFile) 94 | 95 | if certFile != "": 96 | var ret = SSLCTXUseCertificateChainFile(ctx, certFile) 97 | if ret != 1: 98 | raiseSSLError() 99 | 100 | if keyFile != "": 101 | if SSL_CTX_use_PrivateKey_file(ctx, keyFile, 102 | SSL_FILETYPE_PEM) != 1: 103 | raiseSSLError() 104 | 105 | if SSL_CTX_check_private_key(ctx) != 1: 106 | raiseSSLError("Verification of private key file failed.") 107 | 108 | # ## 109 | # From net module 110 | # Edited by @2vg 111 | # ## 112 | proc newSSLContext(cert, key: string, mode = CVerifyNone): SslCtx = 113 | var newCtx: SslCtx 114 | newCTX = SSL_CTX_new(TLS_server_method()) 115 | 116 | if newCTX.SSLCTXSetCipherList(strongCipher) != 1: 117 | raiseSSLError() 118 | case mode 119 | of CVerifyPeer: 120 | newCTX.SSLCTXSetVerify(SSLVerifyPeer, nil) 121 | of CVerifyNone: 122 | newCTX.SSLCTXSetVerify(SSLVerifyNone, nil) 123 | if newCTX == nil: 124 | raiseSSLError() 125 | 126 | discard newCTX.SSLCTXSetMode(SSL_MODE_AUTO_RETRY) 127 | 128 | newCTX.loadCertificates(cert, key) 129 | 130 | return newCtx 131 | 132 | proc serverNameCallback(ssl: SslPtr, cb_id: int, arg: pointer): int {.cdecl.} = 133 | if ssl.isNil: return SSL_TLSEXT_ERR_NOACK 134 | let serverName = SSL_get_servername(ssl) 135 | let sslCtxTable = cast[ptr CritBitTree[SslCtx]](arg)[] 136 | if sslCtxTable.hasKey($serverName): 137 | let newCtx = sslCtxTable[$serverName] 138 | discard ssl.SSL_set_SSL_CTX(newCtx) 139 | return SSL_TLSEXT_ERR_OK 140 | 141 | # ## 142 | # normal fd to sslFD and accept 143 | # ## 144 | proc toSSLSocket*(serverctx: ServeCtx, ctx: MofuwCtx): bool = 145 | result = true 146 | 147 | ctx.sslCtx = serverctx.sslCtxTbl[""] 148 | discard ctx.sslCtx.SSL_CTX_set_tlsext_servername_callback(serverNameCallback) 149 | discard ctx.sslCtx.SSL_CTX_set_tlsext_servername_arg(addr serverctx.sslCtxTbl) 150 | ctx.sslHandle = SSLNew(ctx.sslCtx) 151 | discard SSL_set_fd(ctx.sslHandle, ctx.fd.SocketHandle) 152 | while true: 153 | let sslret = SSL_accept(ctx.sslHandle); 154 | let ssl_eno = SSL_get_error(ctx.sslHandle, sslret) 155 | case ssl_eno: 156 | of SSL_ERROR_NONE: 157 | return true 158 | of SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE, SSL_ERROR_SYSCALL: 159 | continue 160 | else: 161 | return false 162 | 163 | proc addCertAndKey*(serverctx: ServeCtx, cert, key: string, serverName = "", verify = false) = 164 | let ctx = 165 | if verify: newSSLContext(cert, key, CVerifyPeer) 166 | else: newSSLContext(cert, key) 167 | 168 | if serverctx.sslCtxTbl.hasKey(serverName): 169 | raise newException(Exception, "already have callback.") 170 | 171 | serverctx.sslCtxTbl[serverName] = ctx 172 | 173 | if not serverctx.sslCtxTbl.hasKey(""): serverctx.sslCtxTbl[""] = ctx -------------------------------------------------------------------------------- /src/private/ctxpool.nim: -------------------------------------------------------------------------------- 1 | import ctx 2 | import deques 3 | import mofuparser 4 | 5 | var ctxQueue {.threadvar.}: Deque[MofuwCtx] 6 | 7 | proc initCtxPool*(readSize, writeSize: int, cap: int) = 8 | ctxQueue = initDeque[MofuwCtx](cap) 9 | 10 | #[ 11 | for guard memory fragmentation. 12 | ]# 13 | var ctxArray = newSeq[MofuwCtx](cap) 14 | 15 | for i in 0 ..< cap: 16 | ctxArray[i] = newMofuwCtx(readSize, writeSize) 17 | GC_ref(ctxArray[i]) 18 | ctxQueue.addFirst(ctxArray[i]) 19 | 20 | proc createCtx*(readSize, writeSize: int): MofuwCtx = 21 | result = newMofuwCtx(readSize, writeSize) 22 | GC_ref(result) 23 | 24 | proc getCtx*(readSize, writeSize: int): MofuwCtx = 25 | if ctxQueue.len > 0: 26 | return ctxQueue.popFirst() 27 | else: 28 | return createCtx(readSize, writeSize) 29 | 30 | proc freeCtx*(ctx: MofuwCtx) = 31 | ctxQueue.addLast(ctx) -------------------------------------------------------------------------------- /src/private/etags.nim: -------------------------------------------------------------------------------- 1 | from os import FileInfo, getFileInfo 2 | from times import toUnix 3 | import strtabs 4 | import std/sha1 5 | 6 | const 7 | etagLen* : int = 16 8 | etagEnabled* : bool = true 9 | 10 | var 11 | etags {.threadvar.} : StringTableRef 12 | 13 | proc clearEtags* () {.inline thread.} = 14 | etags = {:}.newStringTable 15 | 16 | proc getEtag* (filePath:string) :string {.inline thread.} = 17 | if etags.isNil : clearEtags() 18 | return if etags.hasKey(filePath) : etags[filePath] else : "" 19 | 20 | proc initEtags() {.inline thread.} = 21 | etags = {:}.newStringTable 22 | 23 | # warning! nocheck file exists 24 | proc generateEtag* (filePath:string) :string {.inline thread.} = 25 | let fi:FileInfo = filePath.getFileInfo() 26 | let hash = $ secureHash( $(fi.id.file) & $(fi.lastWriteTime.toUnix) & "s a l t") 27 | return if hash.len < etagLen : hash else : hash[0.. 0: 37 | case ctx.doubleCRLFCheck() 38 | of endReq: 39 | await servectx.handler(ctx) 40 | ctx.currentBufPos += ctx.bodyStart 41 | else: 42 | break 43 | 44 | if ctx.respLen != 0: asyncCheck ctx.mofuwWrite() 45 | ctx.bufLen = 0 46 | ctx.currentBufPos = 0 47 | 48 | if unlikely(not(servectx.hookresponse.isNil)): 49 | await servectx.hookresponse(ctx) -------------------------------------------------------------------------------- /src/private/http.nim: -------------------------------------------------------------------------------- 1 | import ctx, io 2 | import mofuparser, mofuhttputils 3 | import os, macros, strtabs, strutils, parseutils, 4 | mimetypes, asyncdispatch, asyncfile 5 | 6 | from httpcore import HttpHeaders 7 | import etags 8 | 9 | proc getMethod*(ctx: MofuwCtx): string {.inline.} = 10 | result = getMethod(ctx.mhr) 11 | 12 | proc getPath*(ctx: MofuwCtx): string {.inline.} = 13 | result = getPath(ctx.mhr) 14 | 15 | proc getCookie*(ctx: MofuwCtx): string {.inline.} = 16 | result = getHeader(ctx.mhr, "Cookie") 17 | 18 | proc getHeader*(ctx: MofuwCtx, name: string): string {.inline.} = 19 | result = getHeader(ctx.mhr, name) 20 | 21 | proc toHttpHeaders*(ctx: MofuwCtx): HttpHeaders {.inline.} = 22 | result = ctx.mhr.toHttpHeaders() 23 | 24 | proc setParam*(ctx: MofuwCtx, params: StringTableRef) {.inline.} = 25 | ctx.uriParams = params 26 | 27 | proc setQuery*(ctx: MofuwCtx, query: StringTableRef) {.inline.} = 28 | ctx.uriQuerys = query 29 | 30 | proc params*(ctx: MofuwCtx, key: string): string = 31 | ctx.uriParams.getOrDefault(key) 32 | 33 | proc query*(ctx: MofuwCtx, key: string): string = 34 | ctx.uriQuerys.getOrDefault(key) 35 | 36 | proc bodyParse*(query: string):StringTableRef {.inline.} = 37 | result = {:}.newStringTable 38 | var i = 0 39 | while i < query.len()-1: 40 | var key = "" 41 | var val = "" 42 | i += query.parseUntil(key, '=', i) 43 | if query[i] != '=': 44 | raise newException( 45 | ValueError, "Expected '=' at " & $i & " but got: " & $query[i]) 46 | inc(i) # Skip = 47 | i += query.parseUntil(val, '&', i) 48 | inc(i) # Skip & 49 | result[key] = val 50 | 51 | # ## 52 | # get body 53 | # req.body -> all body 54 | # req.body("user") -> get body query "user" 55 | # ## 56 | proc body*(ctx: MofuwCtx, key: string = ""): string = 57 | if key == "": return $ctx.buf[ctx.bodyStart ..< ctx.bufLen] 58 | if ctx.bodyParams.len == 0: ctx.bodyParams = ctx.body.bodyParse 59 | ctx.bodyParams.getOrDefault(key) 60 | 61 | proc notFound*(ctx: MofuwCtx) {.async.} = 62 | await mofuwSend(ctx, notFound()) 63 | await ctx.mofuwWrite() 64 | 65 | proc badRequest*(ctx: MofuwCtx) {.async.} = 66 | await mofuwSend(ctx, badRequest()) 67 | await ctx.mofuwWrite() 68 | 69 | proc bodyTooLarge*(ctx: MofuwCtx) {.async.} = 70 | await mofuwSend(ctx, bodyTooLarge()) 71 | await ctx.mofuwWrite() 72 | 73 | proc badGateway*(ctx: MofuwCtx) {.async.} = 74 | await mofuwSend(ctx, makeResp( 75 | HTTP502, 76 | "text/plain", 77 | "502 Bad Gateway")) 78 | await ctx.mofuwWrite() 79 | 80 | type 81 | ReqState* = enum 82 | badReq = -3, 83 | bodyLarge, 84 | continueReq, 85 | endReq 86 | 87 | proc doubleCRLFCheck*(ctx: MofuwCtx): ReqState = 88 | # ## 89 | # parse request 90 | # ## 91 | let bodyStart = ctx.mhr.mpParseRequest(addr ctx.buf[ctx.currentBufPos], ctx.bufLen - 1) 92 | 93 | # ## 94 | # found HTTP Method, return 95 | # not found, 0 length string 96 | # ## 97 | let hMethod = 98 | if not ctx.mhr.httpMethod.isNil: ctx.getMethod 99 | else: "" 100 | 101 | if likely(hMethod == "GET" or hMethod == "HEAD"): 102 | # ## 103 | # check \r\l\r\l 104 | # ## 105 | let last = ctx.bufLen 106 | if ctx.buf[last-1] == '\l' and ctx.buf[last-2] == '\r' and 107 | ctx.buf[last-3] == '\l' and ctx.buf[last-4] == '\r': 108 | # ## 109 | # if not bodyStart > 0, request is invalid. 110 | # ## 111 | if likely(bodyStart != -1): 112 | ctx.bodyStart = bodyStart 113 | return endReq 114 | else: 115 | return badReq 116 | # ## 117 | # if not end \r\l\r\l, the request may be in progress 118 | # ## 119 | else: return continueReq 120 | else: 121 | if unlikely(hMethod == ""): 122 | template lenCheck(str: string, idx: int): char = 123 | if idx > str.len - 1: '\0' 124 | else: str[idx] 125 | 126 | # ## 127 | # very slow \r\l\r\l check 128 | # ## 129 | for i, ch in ctx.buf: 130 | if ch == '\r': 131 | if ctx.buf.lenCheck(i+1) == '\l' and 132 | ctx.buf.lenCheck(i+2) == '\r' and 133 | ctx.buf.lenCheck(i+3) == '\l': 134 | # ## 135 | # Even if it ends with \r\l\r\l, 136 | # but it is an illegal request because the method is empty 137 | # ## 138 | return badReq 139 | 140 | # ## 141 | # if the method is empty and does not end with \r\l\r\l, 142 | # the request may be in progress 143 | # for example, send it one character at a time (telnet etc.) 144 | # G -> E -> T 145 | # ## 146 | return continueReq 147 | 148 | # ## 149 | # ctx.buf.len - bodyStart = request body size 150 | # ## 151 | if unlikely(ctx.bufLen - bodyStart > ctx.maxBodySize): 152 | return bodyLarge 153 | else: 154 | # ## 155 | # if the body is 0 or more, 156 | # the parse itself is always successful so it is a normal request 157 | # whether the data of the body is insufficient is not to check here 158 | # ## 159 | if likely(bodyStart > 0): 160 | ctx.bodyStart = bodyStart 161 | return endReq 162 | else: 163 | return continueReq 164 | 165 | proc contentLengthCheck*(ctx: MofuwCtx): int = 166 | let cLenHeader = ctx.getHeader("Content-Length") 167 | 168 | if cLenHeader != "": 169 | try: 170 | return parseInt(cLenHeader) 171 | except: 172 | return -1 173 | else: 174 | # ## 175 | # not found "Content-Length 176 | # ## 177 | return -2 178 | 179 | proc haveBodyHandler*(ctx: MofuwCtx, serverctx: ServeCtx, handler: MofuwHandler): Future[bool] {.async.} = 180 | let hasContentLength = ctx.contentLengthCheck() 181 | if hasContentLength != -2: 182 | if hasContentLength != -1: 183 | while not(ctx.bufLen - ctx.bodyStart >= hasContentLength): 184 | let rcv = await ctx.mofuwRead(serverctx.timeOut) 185 | if rcv == 0: ctx.mofuwClose(); return false 186 | await handler(ctx) 187 | asyncCheck ctx.mofuwWrite() 188 | ctx.bufLen = 0 189 | ctx.currentBufPos = 0 190 | return true 191 | else: 192 | # TODO: Content-Length error. 193 | discard 194 | elif ctx.getHeader("Transfer-Encoding") == "chunked": 195 | ctx.mc = MPchunk() 196 | # Parsing chunks already in the buffer 197 | var chunkBuf = ctx.body[0] 198 | var chunkLen = ctx.bufLen - ctx.bodyStart 199 | var parseRes = ctx.mc.mpParseChunk(addr chunkBuf, chunkLen) 200 | 201 | if parseRes == -1: 202 | await ctx.badRequest() 203 | ctx.mofuwClose() 204 | return false 205 | 206 | ctx.bufLen = ctx.bodyStart + chunkLen 207 | 208 | await handler(ctx) 209 | await ctx.mofuwWrite() 210 | 211 | if parseRes == -2: 212 | while true: 213 | chunkBuf = ctx.body[ctx.bufLen] 214 | chunkLen = await ctx.mofuwRead(serverctx.timeOut) 215 | let pRes = ctx.mc.mpParseChunk(addr chunkBuf, chunkLen) 216 | case pRes 217 | of -2: 218 | ctx.bufLen = ctx.bodyStart + chunkLen 219 | await handler(ctx) 220 | await ctx.mofuwWrite() 221 | of -1: 222 | await ctx.badRequest() 223 | ctx.mofuwClose() 224 | return false 225 | else: 226 | if parseRes != 2: 227 | discard await ctx.mofuwRead(serverctx.timeOut) 228 | await handler(ctx) 229 | await ctx.mofuwWrite() 230 | break 231 | 232 | ctx.bufLen = 0 233 | ctx.currentBufPos = 0 234 | return true 235 | else: 236 | await ctx.badRequest() 237 | ctx.mofuwClose() 238 | return false 239 | 240 | proc fileResp(ctx: MofuwCtx, filePath, file: string) {.async.}= 241 | let (_, _, ext) = splitFile(filePath) 242 | 243 | if ext == "": 244 | await ctx.mofuwSend(makeResp( 245 | HTTP200, 246 | "text/plain" & (if etagEnabled : "\c\lEtag:" & filePath.getEtag else :"" ), 247 | file 248 | )) 249 | else: 250 | let mime = newMimetypes() 251 | 252 | await ctx.mofuwSend(makeResp( 253 | HTTP200, 254 | mime.getMimetype(ext[1 .. ^1], default = "application/octet-stream") & (if etagEnabled : "\c\lEtag:" & filePath.getEtag else :"" ), 255 | file 256 | )) 257 | 258 | proc mofuwReadFile*(ctx: MofuwCtx, filePath: string) {.async.} = 259 | let 260 | f = openAsync(filePath, fmRead) 261 | fileSize = getFileSize(filePath).int 262 | (_, _, ext) = splitFile(filePath) 263 | 264 | if fileSize > 1024 * 1024 * 5: 265 | var i = fileSize 266 | if ext == "": 267 | await ctx.mofuwSend(baseResp( 268 | HTTP200, 269 | "text/plain" & (if etagEnabled : "\c\lEtag:" & filePath.getEtag else :""), 270 | fileSize 271 | )) 272 | await ctx.mofuwSend("\r\l") 273 | await ctx.mofuwWrite() 274 | else: 275 | let mime = newMimetypes() 276 | 277 | await ctx.mofuwSend(baseResp( 278 | HTTP200, 279 | mime.getMimetype(ext[1 .. ^1], default = "application/octet-stream") & (if etagEnabled : "\c\lEtag:" & filePath.getEtag else :"" ), 280 | fileSize 281 | )) 282 | await ctx.mofuwSend("\r\l") 283 | await ctx.mofuwWrite() 284 | 285 | while i != 0: 286 | let file = 287 | if i > 1024 * 1024 * 5: 288 | i.dec(1024 * 1024 * 5) 289 | await f.read(1024 * 1024 * 5) 290 | else: 291 | let ii = i 292 | i = 0 293 | await f.read(ii) 294 | await ctx.mofuwSend(file) 295 | await ctx.mofuwWrite() 296 | else: 297 | await ctx.fileResp(filePath, await f.readAll()) 298 | 299 | f.close() 300 | 301 | proc staticServe*(ctx: MofuwCtx, rootPath: string): Future[bool] {.async.} = 302 | var 303 | state = 0 304 | reqPath = getPath(ctx) 305 | filePath = rootPath 306 | 307 | for k, v in reqPath: 308 | if v == '.': 309 | if reqPath[k+1] == '.': 310 | await ctx.mofuwSend(badRequest()) 311 | return true 312 | 313 | if filePath[^1] != '/': 314 | filePath.add("/") 315 | filePath.add(reqPath[state .. ^1]) 316 | else: 317 | filePath.add(reqPath[state .. ^1]) 318 | 319 | if filePath[^1] != '/': 320 | if existsDir(filePath): 321 | # Since the Host header should always exist, 322 | # Nil check is not done here 323 | let host = getHeader(ctx, "Host") 324 | 325 | reqPath.add("/") 326 | 327 | await ctx.mofuwSend(redirectTo( 328 | "http://" / host / reqPath 329 | )) 330 | 331 | return true 332 | if fileExists(filePath): 333 | 334 | # etag 335 | if etagEnabled : 336 | let etag = ctx.getHeader("If-None-Match") 337 | if not isModifiedEtagWithUpdate(filePath, etag) : 338 | await ctx.mofuwSend(makeResp(HTTP304,"","") ) 339 | return true 340 | 341 | await ctx.mofuwReadFile(filePath) 342 | return true 343 | else: 344 | return false 345 | else: 346 | filePath.add("index.html") 347 | if fileExists(filePath): 348 | 349 | # etag 350 | if etagEnabled : 351 | let etag = ctx.getHeader("If-None-Match") 352 | if not isModifiedEtagWithUpdate(filePath, etag) : 353 | await ctx.mofuwSend(makeResp(HTTP304,"","") ) 354 | return true 355 | 356 | await ctx.mofuwReadFile(filePath) 357 | return true 358 | else: 359 | return false -------------------------------------------------------------------------------- /src/private/io.nim: -------------------------------------------------------------------------------- 1 | import ctx, ctxpool 2 | import mofuhttputils 3 | import asyncdispatch 4 | 5 | when defined ssl: 6 | import openssl 7 | 8 | proc asyncSSLRecv*(ctx: MofuwCtx, buf: ptr char, bufLen: int): Future[int] = 9 | var retFuture = newFuture[int]("asyncSSLRecv") 10 | proc cb(fd: AsyncFD): bool = 11 | result = true 12 | let rcv = SSL_read(ctx.sslHandle, buf, bufLen.cint) 13 | if rcv <= 0: 14 | retFuture.complete(0) 15 | else: 16 | retFuture.complete(rcv) 17 | addRead(ctx.fd, cb) 18 | return retFuture 19 | 20 | proc asyncSSLSend*(ctx: MofuwCtx, buf: ptr char, bufLen: int): Future[int] = 21 | var retFuture = newFuture[int]("asyncSSLSend") 22 | proc cb(fd: AsyncFD): bool = 23 | result = true 24 | let rcv = SSL_write(ctx.sslHandle, buf, bufLen.cint) 25 | if rcv <= 0: 26 | retFuture.complete(0) 27 | else: 28 | retFuture.complete(rcv) 29 | addWrite(ctx.fd, cb) 30 | return retFuture 31 | 32 | proc mofuwClose*(ctx: MofuwCtx) = 33 | when defined ssl: 34 | if unlikely ctx.isSSL: 35 | discard SSLShutdown(ctx.sslHandle) 36 | ctx.sslHandle.SSLFree() 37 | ctx.sslHandle = nil 38 | closeSocket(ctx.fd) 39 | ctx.freeCtx() 40 | 41 | proc mofuwRead*(ctx: MofuwCtx, timeOut: int): Future[int] {.async.} = 42 | let rcvLimit = 43 | block: 44 | if unlikely(ctx.buf.len - ctx.bufLen == 0): 45 | ctx.buf.setLen(ctx.buf.len + ctx.buf.len) 46 | ctx.buf.len - ctx.bufLen 47 | 48 | when defined ssl: 49 | if unlikely ctx.isSSL: 50 | let fut = asyncSSLrecv(ctx, addr ctx.buf[ctx.bufLen], rcvLimit) 51 | let rcv = 52 | if timeOut > 0: 53 | let isSuccess = await withTimeout(fut, timeOut) 54 | if isSuccess: fut.read else: 0 55 | else: 56 | await fut 57 | ctx.bufLen += rcv 58 | return rcv 59 | 60 | let fut = recvInto(ctx.fd, addr ctx.buf[ctx.bufLen], rcvLimit) 61 | let rcv = 62 | if timeOut > 0: 63 | let isSuccess = await withTimeout(fut, timeOut) 64 | if isSuccess: fut.read else: 0 65 | else: 66 | await fut 67 | ctx.bufLen += rcv 68 | return rcv 69 | 70 | proc mofuwSend*(ctx: MofuwCtx, body: string) {.async.} = 71 | while unlikely ctx.respLen + body.len > ctx.resp.len: 72 | ctx.resp.setLen(ctx.resp.len + ctx.resp.len) 73 | var buf: string 74 | buf.shallowcopy(body) 75 | let ol = ctx.respLen 76 | copyMem(addr ctx.resp[ol], addr buf[0], buf.len) 77 | ctx.respLen += body.len 78 | 79 | proc mofuwWrite*(ctx: MofuwCtx) {.async.} = 80 | # try send because raise exception. 81 | # buffer not protect, but 82 | # mofuwReq have buffer, so this is safe.(?) 83 | when defined ssl: 84 | if unlikely ctx.isSSL: 85 | try: 86 | discard await asyncSSLSend(ctx, addr(ctx.resp[0]), ctx.respLen) 87 | except: 88 | discard 89 | ctx.respLen = 0 90 | return 91 | 92 | try: 93 | await send(ctx.fd, addr(ctx.resp[0]), ctx.respLen) 94 | except: 95 | discard 96 | ctx.respLen = 0 97 | 98 | template mofuwResp*(status, mime, body: string): typed = 99 | asyncCheck ctx.mofuwSend(makeResp( 100 | status, 101 | mime, 102 | body)) 103 | 104 | template mofuwOK*(body: string, mime: string = "text/plain") = 105 | mofuwResp( 106 | HTTP200, 107 | mime, 108 | body) -------------------------------------------------------------------------------- /src/private/log.nim: -------------------------------------------------------------------------------- 1 | import ctx, http 2 | import times, json, strutils 3 | 4 | proc nowDateTime: (string, string) = 5 | var ti = now() 6 | result = 7 | ($ti.year & '-' & intToStr(ord(ti.month), 2) & 8 | '-' & intToStr(ti.monthday, 2), 9 | intToStr(ti.hour, 2) & ':' & intToStr(ti.minute, 2) & 10 | ':' & intToStr(ti.second, 2)) 11 | 12 | # ## 13 | # still develop 14 | # ## 15 | proc serverLogging*(ctx: MofuwCtx, format: string = nil) = 16 | let (date, time) = nowDateTime() 17 | if format.isNil: 18 | var log = %*{ 19 | "address": ctx.ip, 20 | "request_method": ctx.getMethod, 21 | "request_path": ctx.getPath, 22 | "date": date, 23 | "time": time, 24 | } 25 | 26 | # ## 27 | # return exception msg and stacktrace 28 | # ## 29 | proc serverError*: string = 30 | let exp = getCurrentException() 31 | let stackTrace = exp.getStackTrace() 32 | result = $exp.name & ": " & exp.msg & "\n" & stackTrace -------------------------------------------------------------------------------- /src/private/route.nim: -------------------------------------------------------------------------------- 1 | import ctx, handler 2 | import macros, strutils 3 | 4 | macro handlerMacro*(body: untyped): untyped = 5 | result = newStmtList() 6 | 7 | let lam = newNimNode(nnkProcDef).add( 8 | ident"mofuwHandler",newEmptyNode(),newEmptyNode(), 9 | newNimNode(nnkFormalParams).add( 10 | newEmptyNode(), 11 | newIdentDefs(ident"ctx", ident"MofuwCtx") 12 | ), 13 | newNimNode(nnkPragma).add(ident"async"), 14 | newEmptyNode(), 15 | body 16 | ) 17 | 18 | result.add(lam) 19 | 20 | macro mofuwLambda(body: untyped): untyped = 21 | result = newStmtList() 22 | 23 | let lam = newNimNode(nnkLambda).add( 24 | newEmptyNode(),newEmptyNode(),newEmptyNode(), 25 | newNimNode(nnkFormalParams).add( 26 | newEmptyNode(), 27 | newIdentDefs(ident"ctx", ident"MofuwCtx") 28 | ), 29 | newNimNode(nnkPragma).add(ident"async"), 30 | newEmptyNode(), 31 | body 32 | ) 33 | 34 | result.add(lam) 35 | 36 | macro routesBase*(body: untyped): untyped = 37 | var staticPath = "" 38 | 39 | result = newStmtList() 40 | result.add(parseStmt(""" 41 | let mofuwRouter = newRouter[proc(ctx: MofuwCtx): Future[void]]() 42 | """)) 43 | 44 | # mofuwRouter.map( 45 | # proc(ctx: MofuwCtxs) {.async.} = 46 | # body 47 | # , "METHOD", "PATH") 48 | for i in 0 ..< body.len: 49 | case body[i].kind 50 | of nnkCommand: 51 | let methodName = ($body[i][0]).normalize.toLowerAscii() 52 | let pathName = $body[i][1] 53 | result.add( 54 | newCall("map", ident"mofuwRouter", 55 | getAst(mofuwLambda(body[i][2])), 56 | newLit(methodName), 57 | newLit(pathName) 58 | ) 59 | ) 60 | of nnkCall: 61 | let call = ($body[i][0]).normalize.toLowerAscii() 62 | let path = $body[i][1] 63 | if call == "serve": 64 | staticPath.add(path) 65 | else: 66 | discard 67 | 68 | result.add(newCall(ident"compress", ident"mofuwRouter")) 69 | 70 | let handlerBody = newStmtList() 71 | 72 | handlerBody.add( 73 | parseStmt""" 74 | var headers = ctx.toHttpHeaders() 75 | """, 76 | parseStmt""" 77 | let r = mofuwRouter.route(ctx.getMethod, parseUri(ctx.getPath), headers) 78 | """ 79 | ) 80 | 81 | let staticRoutes = 82 | if staticPath != "": 83 | parseStmt( 84 | "if not (await staticServe(ctx, \"" & staticPath & "\")): await ctx.mofuwSend(notFound())") 85 | else: 86 | parseStmt("await ctx.mofuwSend(notFound())") 87 | 88 | # if r.status == routingFailure: 89 | # await ctx.mofuwSned(notFound()) 90 | # else: 91 | # req.setParam(r.arguments.pathArgs) 92 | # req.setQuery(r.arguments.queryArgs) 93 | # await r.handler(req, ctx) 94 | handlerBody.add( 95 | newNimNode(nnkIfStmt).add( 96 | newNimNode(nnkElifBranch).add( 97 | infix( 98 | newDotExpr(ident"r", ident"status"), 99 | "==", 100 | ident"routingFailure" 101 | ), 102 | newStmtList().add( 103 | staticRoutes 104 | ) 105 | ), 106 | newNimNode(nnkElse).add( 107 | newStmtList( 108 | newCall( 109 | newDotExpr(ident"ctx", ident"setParam"), 110 | newDotExpr(newDotExpr(ident"r", ident"arguments"), ident"pathArgs") 111 | ), 112 | newCall( 113 | newDotExpr(ident"ctx", ident"setQuery"), 114 | newDotExpr(newDotExpr(ident"r", ident"arguments"), ident"queryArgs") 115 | ), 116 | newNimNode(nnkCommand).add( 117 | ident"await", 118 | newCall( 119 | newDotExpr(ident"r", ident"handler"), 120 | ident"ctx" 121 | ) 122 | ) 123 | ) 124 | ) 125 | ) 126 | ) 127 | 128 | result.add(handlerBody) 129 | 130 | macro routes*(body: untyped): untyped = 131 | let base = getAst(routesBase(body)) 132 | result = getAst(handlerMacro(base)) 133 | 134 | when defined vhost: 135 | macro vhosts*(ctx: ServeCtx, body: untyped): untyped = 136 | result = newStmtList() 137 | 138 | for i in 0 ..< body.len: 139 | case body[i].kind 140 | of nnkCommand: 141 | let callName = ($body[i][0]).normalize.toLowerAscii() 142 | let serverName = $body[i][1] 143 | if callName != "host": raise newException(Exception, "can't define except Host.") 144 | 145 | let lam = 146 | if $body[i][2][0][0].toStrLit == "routes": 147 | newNimNode(nnkLambda).add( 148 | newEmptyNode(),newEmptyNode(),newEmptyNode(), 149 | newNimNode(nnkFormalParams).add( 150 | newEmptyNode(), 151 | newIdentDefs(ident"ctx", ident"MofuwCtx") 152 | ), 153 | newNimNode(nnkPragma).add(ident"async"), 154 | newEmptyNode(), 155 | getAst(routesBase(body[i][2][0][1])) 156 | ) 157 | else: 158 | newNimNode(nnkLambda).add( 159 | newEmptyNode(),newEmptyNode(),newEmptyNode(), 160 | newNimNode(nnkFormalParams).add( 161 | newEmptyNode(), 162 | newIdentDefs(ident"ctx", ident"MofuwCtx") 163 | ), 164 | newNimNode(nnkPragma).add(ident"async"), 165 | newEmptyNode(), 166 | body[i][2] 167 | ) 168 | 169 | result.add( 170 | newCall( 171 | "registerCallBack", 172 | `ctx`, 173 | ident(serverName).toStrLit, 174 | lam)) 175 | else: 176 | discard 177 | 178 | var handler = quote do: 179 | let header = ctx.getHeader("Host") 180 | let table = ctx.getCallBackTable() 181 | if table.hasKey(header): 182 | await table[header](ctx) 183 | else: 184 | for cb in table.values: 185 | await cb(ctx) 186 | 187 | let vhostHandler = getAst(mofuwLambda(handler)) 188 | 189 | result.add(quote do: 190 | `ctx`.handler = `vhostHandler` 191 | `ctx`.serve() 192 | ) -------------------------------------------------------------------------------- /src/private/server.nim: -------------------------------------------------------------------------------- 1 | import io, ctx, ctxpool, handler, sysutils 2 | import mofuhttputils 3 | import os, net, critbits, nativesockets, asyncdispatch, threadpool 4 | 5 | when defined(windows): 6 | from winlean import TCP_NODELAY 7 | else: 8 | from posix import TCP_NODELAY 9 | 10 | proc registerCallback*(ctx: ServeCtx, serverName: string, cb: MofuwHandler) = 11 | ctx.vhostTbl[serverName] = cb 12 | if not ctx.vhostTbl.hasKey(""): ctx.vhostTbl[""] = cb 13 | 14 | proc setCallBackTable*(servectx: ServeCtx, ctx: MofuwCtx) = 15 | ctx.vhostTbl = servectx.vhostTbl 16 | 17 | proc getCallBackTable*(ctx: MofuwCtx): VhostTable = 18 | ctx.vhostTbl 19 | 20 | proc updateTime(fd: AsyncFD): bool = 21 | updateServerTime() 22 | return false 23 | 24 | proc newServerSocket*(port: int): SocketHandle = 25 | let server = newSocket() 26 | server.setSockOpt(OptReuseAddr, true) 27 | server.setSockOpt(OptReusePort, true) 28 | server.getFD().setSockOptInt(cint(IPPROTO_TCP), TCP_NODELAY, 1) 29 | server.getFd.setBlocking(false) 30 | server.bindAddr(Port(port)) 31 | server.listen(defaultBacklog().cint) 32 | return server.getFd() 33 | 34 | proc initCtx*(servectx: ServeCtx, ctx: MofuwCtx, fd: AsyncFD, ip: string): MofuwCtx = 35 | ctx.fd = fd 36 | ctx.ip = ip 37 | ctx.bufLen = 0 38 | ctx.respLen = 0 39 | ctx.currentBufPos = 0 40 | if unlikely ctx.buf.len != servectx.readBufferSize: ctx.buf.setLen(servectx.readBufferSize) 41 | if unlikely ctx.resp.len != servectx.writeBufferSize: ctx.buf.setLen(servectx.writeBufferSize) 42 | ctx 43 | 44 | proc mofuwServe*(ctx: ServeCtx, isSSL: bool) {.async.} = 45 | initCtxPool(ctx.readBufferSize, ctx.writeBufferSize, ctx.poolsize) 46 | 47 | let server = ctx.port.newServerSocket().AsyncFD 48 | register(server) 49 | setServerName(ctx.serverName) 50 | updateServerTime() 51 | addTimer(1000, false, updateTime) 52 | 53 | var cantaccept = false 54 | 55 | while true: 56 | if unlikely cantaccept: 57 | await sleepAsync(1) 58 | cantaccept = true 59 | 60 | try: 61 | let data = await acceptAddr(server) 62 | setBlocking(data[1].SocketHandle, false) 63 | let mCtx = ctx.initCtx(getCtx(ctx.readBufferSize, ctx.writeBuffersize), data[1], data[0]) 64 | setCallBackTable(ctx, mCtx) 65 | mCtx.maxBodySize = ctx.maxBodySize 66 | when defined ssl: 67 | if unlikely isSSL: 68 | mCtx.isSSL = true 69 | if not ctx.toSSLSocket(mCtx): mCtx.mofuwClose(); continue 70 | asyncCheck handler(ctx, mCtx) 71 | except: 72 | # TODO async sleep. 73 | # await sleepAsync(10) 74 | cantAccept = true 75 | 76 | proc runServer*(ctx: ServeCtx, isSSL = false) {.thread.} = 77 | if isSSl: 78 | waitFor ctx.mofuwServe(true) 79 | else: 80 | waitFor ctx.mofuwServe(false) 81 | 82 | proc serve*(ctx: ServeCtx) = 83 | if ctx.handler.isNil: 84 | raise newException(Exception, "Callback is nil. please set callback.") 85 | 86 | for _ in 0 ..< countCPUs(): 87 | spawn ctx.runServer(ctx.isSSL) 88 | 89 | when not defined noSync: 90 | sync() -------------------------------------------------------------------------------- /src/private/sysutils.nim: -------------------------------------------------------------------------------- 1 | from osproc import countProcessors 2 | from nativesockets import SOMAXCONN 3 | 4 | when defined(linux): from posix import Pid 5 | 6 | # ## 7 | # sysconf(3) does not respect the affinity mask bits, it's not suitable for containers. 8 | # ## 9 | proc countCPUs*: int = 10 | when defined(linux): 11 | const 12 | schedh = "#define _GNU_SOURCE\n#include " 13 | type CpuSet {.importc: "cpu_set_t", header: schedh.} = object 14 | when defined(linux) and defined(amd64): 15 | abi: array[1024 div (8 * sizeof(culong)), culong] 16 | var set: CpuSet 17 | proc sched_getAffinity(pid: Pid, cpusetsize: int, mask: var CpuSet): cint {. 18 | importc: "sched_getaffinity", header: schedh.} 19 | proc cpusetCount(s: var CpuSet): int {. importc: "CPU_COUNT", header: schedh.} 20 | if sched_getAffinity(0, sizeof(CpuSet), set) == 0.cint: 21 | return cpusetCount(set) 22 | else: 23 | return countProcessors() 24 | else: 25 | return countProcessors() 26 | 27 | # ## 28 | # From man 2 listen, SOMAXCONN is just limit which is hardcoded value 128. 29 | # ## 30 | proc defaultBacklog*: int = 31 | when defined(linux): 32 | proc fscanf(c: File, frmt: cstring): cint {.varargs, importc, header: "".} 33 | 34 | var 35 | backlog: int = SOMAXCONN 36 | f: File 37 | tmp: int 38 | 39 | if f.open("/proc/sys/net/core/somaxconn"): # See `man 2 listen`. 40 | if fscanf(f, "%d", tmp.addr) == cint(1): 41 | backlog = tmp 42 | f.close 43 | return backlog 44 | else: 45 | return SOMAXCONN -------------------------------------------------------------------------------- /tests/SSLapp/app.nim: -------------------------------------------------------------------------------- 1 | import ../../src/mofuw, threadpool 2 | 3 | let sslServer = newServeCtx( 4 | port = 443 5 | ) 6 | 7 | sslServer.addCertAndKey( 8 | servername = "example.com", 9 | cert = "cert.pem", 10 | key = "key.pem" 11 | ) 12 | 13 | vhosts sslServer: 14 | host "example.com": 15 | mofuwOK("Hello, World!") 16 | 17 | let httpServer = newServeCtx( 18 | port = 80 19 | ) 20 | 21 | vhosts httpServer: 22 | host "example.com": 23 | await ctx.mofuwSend(redirectTo("https://example.com")) 24 | 25 | sync() -------------------------------------------------------------------------------- /tests/SSLapp/nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on 2 | -d:ssl 3 | -d:vhost 4 | -d:noSync -------------------------------------------------------------------------------- /tests/helloworld/minimal.nim: -------------------------------------------------------------------------------- 1 | import ../../src/mofuw 2 | 3 | routes: 4 | get "/": 5 | mofuwOK("hello, world!") 6 | 7 | newServeCtx( 8 | port = 8080, 9 | handler = mofuwHandler 10 | ).serve() 11 | -------------------------------------------------------------------------------- /tests/helloworld/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../../" 2 | --threads:on -------------------------------------------------------------------------------- /tests/routing/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../../" 2 | --threads:on -------------------------------------------------------------------------------- /tests/routing/simple.nim: -------------------------------------------------------------------------------- 1 | import ../../src/mofuw 2 | 3 | routes: 4 | get "/": 5 | mofuwOK("Hello, World!") 6 | 7 | get "/user/{id}": 8 | mofuwOK("Hello, " & ctx.params("id") & "!") 9 | 10 | post "/create": 11 | mofuwOK("created: " & ctx.body) 12 | 13 | newServeCtx( 14 | port = 8080, 15 | handler = mofuwHandler 16 | ).serve() -------------------------------------------------------------------------------- /tests/staticServe/nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on -------------------------------------------------------------------------------- /tests/staticServe/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | Hello, World! 3 | 4 | -------------------------------------------------------------------------------- /tests/staticServe/static.nim: -------------------------------------------------------------------------------- 1 | import ../../src/mofuw 2 | 3 | routes: 4 | serve("public") 5 | 6 | get "/foo": 7 | mofuwOK("yay") 8 | 9 | newServeCtx( 10 | port = 8080, 11 | handler = mofuwHandler 12 | ).serve() -------------------------------------------------------------------------------- /tests/techempower/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../../" 2 | --threads:on -------------------------------------------------------------------------------- /tests/techempower/techempower.nim: -------------------------------------------------------------------------------- 1 | import ../../src/mofuw, packedjson 2 | 3 | proc h(ctx: MofuwCtx) {.async.} = 4 | case ctx.getPath 5 | of "/plaintext": 6 | mofuwResp(HTTP200, "text/plain", "Hello, World!") 7 | of "/json": 8 | mofuwResp(HTTP200, "application/json", $(%{"message": %"Hello, World!"})) 9 | else: 10 | mofuwResp(HTTP404, "text/plain", "NOT FOUND") 11 | 12 | newServeCtx( 13 | port = 8080, 14 | handler = h 15 | ).serve() -------------------------------------------------------------------------------- /tests/vhost/nim.cfg: -------------------------------------------------------------------------------- 1 | --threads:on 2 | -d:vhost -------------------------------------------------------------------------------- /tests/vhost/vhost.nim: -------------------------------------------------------------------------------- 1 | import ../../src/mofuw 2 | 3 | let server1 = newServeCtx( 4 | port = 8080 5 | ) 6 | 7 | vhosts server1: 8 | host "localhost:8080": 9 | mofuwOk("I'm local :)") 10 | host "example.com": 11 | mofuwOk("Hello, example!") 12 | 13 | let server2 = newServeCtx( 14 | port = 8081 15 | ) 16 | 17 | vhosts server2: 18 | host "localhost:8081": 19 | mofuwOk("Second server :)") -------------------------------------------------------------------------------- /tests/websocket/README.md: -------------------------------------------------------------------------------- 1 | # WebSocket example 2 | 3 | ## require 4 | - Nim >= 0.18.0 5 | - mofuw >= 1.2.2 6 | 7 | ## usage 8 | ```nim 9 | import mofuw/websocket 10 | ``` 11 | 12 | ## example 13 | ```nim 14 | import mofuw/websocket, mofuw 15 | 16 | mofuwHandler: 17 | let (ws, error) = await verifyWebsocketRequest(req, res) 18 | 19 | if ws.isNil: 20 | echo "WS negotiation failed: ", error 21 | mofuwResp(HTTP400, "text/plain", "Websocket negotiation failed: " & error) 22 | return 23 | 24 | echo "New websocket customer arrived!" 25 | while true: 26 | let (opcode, data) = await ws.readData() 27 | try: 28 | echo "(opcode: ", opcode, ", data length: ", data.len, ")" 29 | 30 | case opcode 31 | of Opcode.Text: 32 | waitFor ws.sendText("thanks for the data!") 33 | of Opcode.Binary: 34 | waitFor ws.sendBinary(data) 35 | of Opcode.Close: 36 | asyncCheck ws.close() 37 | let (closeCode, reason) = extractCloseData(data) 38 | echo "socket went away, close code: ", closeCode, ", reason: ", reason 39 | else: discard 40 | except: 41 | echo "encountered exception: ", getCurrentExceptionMsg() 42 | 43 | mofuwHandler.mofuwRun(8080) 44 | ``` -------------------------------------------------------------------------------- /tests/websocket/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../../" 2 | --threads:on -------------------------------------------------------------------------------- /tests/websocket/ws.nim: -------------------------------------------------------------------------------- 1 | import ../../src/mofuw/websocket, ../../src/mofuw 2 | 3 | proc handler(ctx: MofuwCtx) {.async.} = 4 | let (ws, error) = await verifyWebsocketRequest(ctx) 5 | 6 | if ws.isNil: 7 | echo "WS negotiation failed: ", error 8 | mofuwResp(HTTP400, "text/plain", "Websocket negotiation failed: " & error) 9 | return 10 | 11 | echo "New websocket customer arrived!" 12 | while true: 13 | let (opcode, data) = await ws.readData() 14 | try: 15 | echo "(opcode: ", opcode, ", data length: ", data.len, ")" 16 | 17 | case opcode 18 | of Opcode.Text: 19 | waitFor ws.sendText("thanks for the data!") 20 | of Opcode.Binary: 21 | waitFor ws.sendBinary(data) 22 | of Opcode.Close: 23 | asyncCheck ws.close() 24 | let (closeCode, reason) = extractCloseData(data) 25 | echo "socket went away, close code: ", closeCode, ", reason: ", reason 26 | else: discard 27 | except: 28 | echo "encountered exception: ", getCurrentExceptionMsg() 29 | 30 | newServeCtx( 31 | port = 8080, 32 | handler = handler 33 | ).serve() --------------------------------------------------------------------------------