├── .editorconfig ├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .travis.d ├── before-install.sh └── install.sh ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── MacroExpress │ ├── MacroExpress.swift │ └── README.md ├── connect │ ├── BodyParser.swift │ ├── CORS.swift │ ├── Connect.swift │ ├── CookieParser.swift │ ├── Cookies.swift │ ├── CrossCompile.swift │ ├── Logger.swift │ ├── MethodOverride.swift │ ├── Middleware.swift │ ├── Pause.swift │ ├── QS.swift │ ├── README.md │ ├── ServeStatic.swift │ ├── Session.swift │ └── TypeIs.swift ├── dotenv │ └── dotenv.swift ├── express │ ├── BasicAuth.swift │ ├── ErrorMiddleware.swift │ ├── Express.swift │ ├── ExpressWrappedDictionary.swift │ ├── IncomingMessage.swift │ ├── JSON.swift │ ├── MiddlewareObject.swift │ ├── Module.swift │ ├── Mustache.swift │ ├── README.md │ ├── Render.swift │ ├── Route.swift │ ├── RouteFactories.swift │ ├── RouteKeeper.swift │ ├── RouteMounts.swift │ ├── RoutePattern.swift │ ├── Router.swift │ ├── ServerResponse.swift │ └── Settings.swift ├── mime │ └── MIME.swift └── multer │ ├── DiskStorage.swift │ ├── File.swift │ ├── IncomingMessageMulter.swift │ ├── Limits.swift │ ├── MemoryStorage.swift │ ├── Middleware.swift │ ├── Multer.swift │ ├── MulterError.swift │ ├── MulterStorage.swift │ ├── MultiPartParser.swift │ ├── PartType.swift │ ├── ProcessingContext.swift │ ├── README.md │ └── Utilities.swift └── Tests ├── LinuxMain.swift ├── RouteTests ├── ErrorMiddlewareTests.swift ├── RouteMountingTests.swift ├── SimpleRouteTests.swift └── XCTestManifests.swift ├── bodyParserTests ├── XCTestManifests.swift └── bodyParserTests.swift ├── dotenvTests ├── XCTestManifests.swift └── dotenvTests.swift ├── mimeTests ├── XCTestManifests.swift └── mimeTests.swift └── multerTests ├── Fixtures.swift ├── MultiPartParserTests.swift ├── XCTestManifests.swift └── multerTests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | tab_width = 8 5 | max_line_length = 80 6 | trim_trailing_whitespace = false 7 | end_of_line = lf 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "30 9 * * 1" 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | image: 16 | - swift:5.9.2-focal 17 | - swift:6.1-noble 18 | container: ${{ matrix.image }} 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v4 22 | - name: Build Swift Debug Package 23 | run: swift build -c debug 24 | - name: Build Swift Release Package 25 | run: swift build -c release 26 | - name: Run Tests 27 | run: swift test --enable-test-discovery 28 | nextstep: 29 | runs-on: macos-latest 30 | steps: 31 | - name: Select latest available Xcode 32 | uses: maxim-lobanov/setup-xcode@v1.5.1 33 | with: 34 | xcode-version: latest 35 | - name: Checkout Repository 36 | uses: actions/checkout@v4 37 | - name: Build Swift Debug Package 38 | run: swift build -c debug 39 | - name: Build Swift Release Package 40 | run: swift build -c release 41 | - name: Run Tests 42 | run: swift test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | # hh 93 | Package.resolved 94 | xcuserdata 95 | .docker.build 96 | .swiftpm 97 | .vscode 98 | 99 | -------------------------------------------------------------------------------- /.travis.d/before-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$TRAVIS_OS_NAME" == "Linux" ]]; then 4 | sudo apt-get install -y wget \ 5 | clang-3.6 libc6-dev make git libicu52 libicu-dev \ 6 | git autoconf libtool pkg-config \ 7 | libblocksruntime-dev \ 8 | libkqueue-dev \ 9 | libpthread-workqueue-dev \ 10 | systemtap-sdt-dev \ 11 | libbsd-dev libbsd0 libbsd0-dbg \ 12 | curl libcurl4-openssl-dev \ 13 | libedit-dev \ 14 | python2.7 python2.7-dev \ 15 | libxml2 16 | 17 | sudo update-alternatives --quiet --install /usr/bin/clang clang /usr/bin/clang-3.6 100 18 | sudo update-alternatives --quiet --install /usr/bin/clang++ clang++ /usr/bin/clang++-3.6 100 19 | fi 20 | -------------------------------------------------------------------------------- /.travis.d/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # our path is: 4 | # /home/travis/build/NozeIO/Noze.io/ 5 | 6 | if ! test -z "$SWIFT_SNAPSHOT_NAME"; then 7 | # Install Swift 8 | wget "${SWIFT_SNAPSHOT_NAME}" 9 | 10 | TARBALL="`ls swift-*.tar.gz`" 11 | echo "Tarball: $TARBALL" 12 | 13 | TARPATH="$PWD/$TARBALL" 14 | 15 | cd $HOME # expand Swift tarball in $HOME 16 | tar zx --strip 1 --file=$TARPATH 17 | pwd 18 | 19 | export PATH="$PWD/usr/bin:$PATH" 20 | which swift 21 | 22 | if [ `which swift` ]; then 23 | echo "Installed Swift: `which swift`" 24 | else 25 | echo "Failed to install Swift?" 26 | exit 42 27 | fi 28 | fi 29 | 30 | swift --version 31 | 32 | 33 | # Environment 34 | 35 | TT_SWIFT_BINARY=`which swift` 36 | 37 | echo "${TT_SWIFT_BINARY}" 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | notifications: 4 | slack: nozeio:LIFY1Jtkx0FRcLq3u1WliHRZ 5 | 6 | matrix: 7 | include: 8 | - os: Linux 9 | dist: trusty 10 | env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-5.0.3-release/ubuntu1404/swift-5.0.3-RELEASE/swift-5.0.3-RELEASE-ubuntu14.04.tar.gz" 11 | sudo: required 12 | - os: Linux 13 | dist: trusty 14 | env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-5.1.5-release/ubuntu1404/swift-5.1.5-RELEASE/swift-5.1.5-RELEASE-ubuntu14.04.tar.gz" 15 | sudo: required 16 | - os: Linux 17 | dist: xenial 18 | env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-5.2.4-release/ubuntu1604/swift-5.2.4-RELEASE/swift-5.2.4-RELEASE-ubuntu16.04.tar.gz" 19 | sudo: required 20 | - os: Linux 21 | dist: xenial 22 | env: SWIFT_SNAPSHOT_NAME="https://swift.org/builds/swift-5.3.1-release/ubuntu1604/swift-5.3.1-RELEASE/swift-5.3.1-RELEASE-ubuntu16.04.tar.gz" 23 | sudo: required 24 | - os: osx 25 | osx_image: xcode12 26 | - os: osx 27 | osx_image: xcode11.4 28 | 29 | before_install: 30 | - ./.travis.d/before-install.sh 31 | 32 | install: 33 | - ./.travis.d/install.sh 34 | 35 | script: 36 | - export PATH="$HOME/usr/bin:$PATH" 37 | - swift build -c release 38 | - swift build -c debug 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to ZeeZide and the community, and agree by submitting the patch 5 | that your contributions are licensed under the Apache 2.0 license. 6 | 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | # local config 4 | SWIFT_BUILD=swift build 5 | SWIFT_CLEAN=swift package clean 6 | SWIFT_BUILD_DIR=.build 7 | SWIFT_TEST=swift test 8 | CONFIGURATION=release 9 | 10 | # docker config 11 | #SWIFT_BUILD_IMAGE="swift:5.7.2" 12 | SWIFT_BUILD_IMAGE="helje5/arm64v8-swift-dev:5.5.3" 13 | DOCKER_BUILD_DIR=".docker.build" 14 | DOCKER_PLATFORM=aarch64 15 | #DOCKER_PLATFORM="x86_64" 16 | SWIFT_DOCKER_BUILD_DIR="$(DOCKER_BUILD_DIR)/$(DOCKER_PLATFORM)-unknown-linux/$(CONFIGURATION)" 17 | DOCKER_BUILD_PRODUCT="$(DOCKER_BUILD_DIR)/$(TOOL_NAME)" 18 | 19 | XENIAL_DESTINATION=/usr/local/lib/swift/dst/x86_64-unknown-linux/swift-5.3-ubuntu16.04.xtoolchain/destination.json 20 | AWS_DESTINATION=/usr/local/lib/swift/dst/x86_64-unknown-linux/swift-5.2-amazonlinux2.xtoolchain/destination.json 21 | 22 | 23 | SWIFT_SOURCES=\ 24 | Sources/*/*/*.swift \ 25 | Sources/*/*/*/*.swift 26 | 27 | all: 28 | $(SWIFT_BUILD) -c $(CONFIGURATION) 29 | 30 | # Cannot test in `release` configuration?! 31 | test: 32 | $(SWIFT_TEST) 33 | 34 | clean : 35 | $(SWIFT_CLEAN) 36 | # We have a different definition of "clean", might be just German 37 | # pickyness. 38 | rm -rf $(SWIFT_BUILD_DIR) 39 | 40 | 41 | # Building for Linux 42 | 43 | amazon-linux: 44 | $(SWIFT_BUILD) -c $(CONFIGURATION) --destination $(AWS_DESTINATION) 45 | 46 | xc-xenial: 47 | $(SWIFT_BUILD) -c $(CONFIGURATION) --destination $(XENIAL_DESTINATION) 48 | 49 | $(DOCKER_BUILD_PRODUCT): $(SWIFT_SOURCES) 50 | docker run --rm \ 51 | -v "$(PWD):/src" \ 52 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \ 53 | "$(SWIFT_BUILD_IMAGE)" \ 54 | bash -c 'cd /src && swift build -c $(CONFIGURATION)' 55 | 56 | docker-all: $(DOCKER_BUILD_PRODUCT) 57 | 58 | docker-test: $(DOCKER_BUILD_PRODUCT) 59 | docker run --rm \ 60 | -v "$(PWD):/src" \ 61 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \ 62 | "$(SWIFT_BUILD_IMAGE)" \ 63 | bash -c 'cd /src && swift test --enable-test-discovery -c $(CONFIGURATION)' 64 | 65 | docker-clean: 66 | rm $(DOCKER_BUILD_PRODUCT) 67 | 68 | docker-distclean: 69 | rm -rf $(DOCKER_BUILD_DIR) 70 | 71 | distclean: clean docker-distclean 72 | 73 | docker-emacs: 74 | docker run --rm -it \ 75 | -v "$(PWD):/src" \ 76 | -v "$(PWD)/$(DOCKER_BUILD_DIR):/src/.build" \ 77 | "$(SWIFT_BUILD_IMAGE)" \ 78 | emacs /src 79 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | 7 | name: "MacroExpress", 8 | 9 | products: [ 10 | .library(name: "MacroExpress", targets: [ "MacroExpress" ]), 11 | .library(name: "express", targets: [ "express" ]), 12 | .library(name: "connect", targets: [ "connect" ]), 13 | .library(name: "mime", targets: [ "mime" ]), 14 | .library(name: "dotenv", targets: [ "dotenv" ]), 15 | .library(name: "multer", targets: [ "multer" ]) 16 | ], 17 | 18 | dependencies: [ 19 | .package(url: "https://github.com/Macro-swift/Macro.git", 20 | from: "1.0.4"), 21 | .package(url: "https://github.com/AlwaysRightInstitute/mustache.git", 22 | from: "1.0.2") 23 | ], 24 | 25 | targets: [ 26 | .target(name: "mime", dependencies: []), 27 | .target(name: "dotenv", dependencies: [ 28 | .product(name: "MacroCore", package: "Macro"), 29 | .product(name: "fs", package: "Macro") 30 | ]), 31 | .target(name: "multer", dependencies: [ 32 | .product(name: "MacroCore", package: "Macro"), 33 | .product(name: "fs", package: "Macro"), 34 | .product(name: "http", package: "Macro"), 35 | "mime", "connect" 36 | ], exclude: [ "README.md" ]), 37 | .target(name: "connect", dependencies: [ 38 | .product(name: "MacroCore", package: "Macro"), 39 | .product(name: "fs", package: "Macro"), 40 | .product(name: "http", package: "Macro"), 41 | "mime" 42 | ], exclude: [ "README.md" ]), 43 | .target(name: "express", dependencies: [ 44 | .product(name: "MacroCore", package: "Macro"), 45 | .product(name: "fs", package: "Macro"), 46 | .product(name: "http", package: "Macro"), 47 | "connect", "mime", 48 | .product(name: "Mustache", package: "Mustache") 49 | ], exclude: [ "README.md" ]), 50 | .target(name: "MacroExpress", dependencies: [ 51 | .product(name: "MacroCore", package: "Macro"), 52 | .product(name: "fs", package: "Macro"), 53 | .product(name: "http", package: "Macro"), 54 | .product(name: "xsys", package: "Macro"), 55 | "dotenv", "mime", "connect", "express", "multer" 56 | ], exclude: [ "README.md" ]), 57 | 58 | .testTarget(name: "mimeTests", dependencies: [ "mime" ]), 59 | .testTarget(name: "multerTests", dependencies: [ "multer" ]), 60 | .testTarget(name: "bodyParserTests", dependencies: [ "connect", "Macro" ]), 61 | .testTarget(name: "dotenvTests", dependencies: [ "dotenv" ]), 62 | .testTarget(name: "RouteTests", dependencies: [ 63 | .product(name: "MacroTestUtilities", package: "Macro"), 64 | "express" 65 | ]) 66 | ] 67 | ) 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

MacroExpress 2 | 4 |

5 | 6 | A small, unopinionated "don't get into my way" / "I don't wanna `wait`" 7 | asynchronous web framework for Swift. 8 | With a strong focus on replicating the Node APIs in Swift. 9 | But in a typesafe, and fast way. 10 | 11 | MacroExpress is a more capable variant of 12 | [µExpress](https://github.com/NozeIO/MicroExpress). 13 | The goal is still to keep a small core, but add some 14 | [Noze.io](http://noze.io) 15 | modules and concepts. 16 | 17 | MacroExpress adds the web framework components to 18 | [Macro](https://github.com/Macro-swift/Macro/) 19 | (kinda like `Express.js` adds to `Node.js`). 20 | 21 | [MacroLambda](https://github.com/Macro-swift/MacroLambda) has the bits to 22 | directly deploy MacroExpress applications on AWS Lambda. 23 | [MacroApp](https://github.com/Macro-swift/MacroApp) adds a SwiftUI-style 24 | declarative DSL to setup MacroExpress routes. 25 | 26 | 27 | ## What does it look like? 28 | 29 | The Macro [Examples](https://github.com/Macro-swift/Examples) package 30 | contains a few examples which all can run straight from the source as 31 | [swift-sh](https://github.com/mxcl/swift-sh) scripts. 32 | 33 | ```swift 34 | #!/usr/bin/swift sh 35 | import MacroExpress // @Macro-swift 36 | 37 | let app = express() 38 | app.use(logger("dev")) 39 | app.use(bodyParser.urlencoded()) 40 | app.use(serveStatic(__dirname() + "/public")) 41 | 42 | app.get("/hello") { req, res, next in 43 | res.send("Hello World!") 44 | } 45 | app.get { req, res, next in 46 | res.render("index") 47 | } 48 | 49 | app.listen(1337) 50 | ``` 51 | 52 | ## Environment Variables 53 | 54 | - `macro.core.numthreads` 55 | - `macro.core.iothreads` 56 | - `macro.core.retain.debug` 57 | - `macro.concat.maxsize` 58 | - `macro.streams.debug.rc` 59 | - `macro.router.debug` 60 | - `macro.router.matcher.debug` 61 | - `macro.router.walker.debug` 62 | 63 | ### Links 64 | 65 | - [Macro](https://github.com/Macro-swift/Macro/) 66 | - [µExpress](http://www.alwaysrightinstitute.com/microexpress-nio2/) 67 | - [Noze.io](http://noze.io) 68 | - [SwiftNIO](https://github.com/apple/swift-nio) 69 | - JavaScript Originals 70 | - [Connect](https://github.com/senchalabs/connect) 71 | - [Express.js](http://expressjs.com/en/starter/hello-world.html) 72 | - Swift Apache 73 | - [mod_swift](http://mod-swift.org) 74 | - [ApacheExpress](http://apacheexpress.io) 75 | 76 | ### Who 77 | 78 | **MacroExpress** is brought to you by 79 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 80 | We like feedback, GitHub stars, cool contract work, 81 | presumably any form of praise you can think of. 82 | 83 | **Want to support my work**? 84 | Buy an app: 85 | [Code for SQLite3](https://apps.apple.com/us/app/code-for-sqlite3/id1638111010), 86 | [Past for iChat](https://apps.apple.com/us/app/past-for-ichat/id1554897185), 87 | [SVG Shaper](https://apps.apple.com/us/app/svg-shaper-for-swiftui/id1566140414), 88 | [HMScriptEditor](https://apps.apple.com/us/app/hmscripteditor/id1483239744). 89 | You don't have to use it! 😀 90 | -------------------------------------------------------------------------------- /Sources/MacroExpress/MacroExpress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacroExpress.swift 3 | // MacroExpress 4 | // 5 | // Created by Helge Heß. 6 | // Copyright © 2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | @_exported import func MacroCore.nextTick 10 | @_exported import func MacroCore.setTimeout 11 | @_exported import let MacroCore.console 12 | @_exported import enum MacroCore.process 13 | @_exported import func MacroCore.concat 14 | @_exported import enum MacroCore.json 15 | @_exported import struct MacroCore.Buffer 16 | @_exported import enum MacroCore.ReadableError 17 | @_exported import enum MacroCore.WritableError 18 | @_exported import func MacroCore.__dirname 19 | @_exported import protocol NIO.EventLoop 20 | @_exported import protocol NIO.EventLoopGroup 21 | 22 | @_exported import enum dotenv.dotenv 23 | 24 | @_exported import class http.IncomingMessage 25 | @_exported import class http.OutgoingMessage 26 | @_exported import class http.ServerResponse 27 | 28 | @_exported import enum mime.mime 29 | @_exported import func connect.connect 30 | @_exported import typealias connect.Middleware 31 | @_exported import typealias connect.Next 32 | @_exported import func connect.cookieParser 33 | @_exported import let connect.cookies 34 | @_exported import func connect.cors 35 | @_exported import func connect.logger 36 | @_exported import func connect.methodOverride 37 | @_exported import func connect.pause 38 | @_exported import enum connect.qs 39 | @_exported import func connect.serveStatic 40 | @_exported import func connect.session 41 | @_exported import func connect.typeIs 42 | 43 | @_exported import class express.Express 44 | @_exported import typealias express.ExpressEngine 45 | @_exported import typealias express.Router 46 | @_exported import class express.Route 47 | 48 | @_exported import struct multer.multer 49 | 50 | // We need to import those fully, because they contain query a few extensions, 51 | // which we can't import selectively :-/ 52 | @_exported import MacroCore 53 | @_exported import connect 54 | @_exported import express 55 | 56 | // MARK: - Submodules in `fs` Target 57 | 58 | import enum fs.FileSystemModule 59 | public typealias fs = FileSystemModule 60 | import enum fs.PathModule 61 | public typealias path = PathModule 62 | import enum fs.JSONFileModule 63 | public typealias jsonfile = JSONFileModule 64 | 65 | // MARK: - Submodules in `http` Target 66 | 67 | import enum http.HTTPModule 68 | public typealias http = HTTPModule 69 | import enum express.BasicAuthModule 70 | public typealias expressBasicAuth = express.expressBasicAuth 71 | import enum http.QueryStringModule 72 | public typealias querystring = QueryStringModule 73 | 74 | // MARK: - Process stuff 75 | 76 | @inlinable 77 | public var argv : [ String ] { return process.argv } 78 | @inlinable 79 | public var env : [ String : String ] { return process.env } 80 | -------------------------------------------------------------------------------- /Sources/MacroExpress/README.md: -------------------------------------------------------------------------------- 1 | # MacroExpress 2 | 3 | MacroExpress is the package you may usually want to import. 4 | It imports ALL MacroExpress submodules and re-exports their functions. 5 | -------------------------------------------------------------------------------- /Sources/connect/CORS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CORS.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 02/06/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import enum NIOHTTP1.HTTPMethod 10 | 11 | fileprivate let defaultMethods : [ HTTPMethod ] = [ 12 | .GET, .HEAD, .POST, .DELETE, .OPTIONS, .PUT, .PATCH 13 | ] 14 | fileprivate let defaultHeaders = [ "Accept", "Content-Type" ] 15 | 16 | public func cors(allowOrigin origin : String, 17 | allowHeaders headers : [ String ]? = nil, 18 | allowMethods methods : [ HTTPMethod ]? = nil, 19 | handleOptions : Bool = false) 20 | -> Middleware 21 | { 22 | return { req, res, next in 23 | let sHeaders = (headers ?? defaultHeaders).joined(separator: ", ") 24 | let sMethods = (methods ?? defaultMethods).map { $0.rawValue } 25 | .joined(separator: ",") 26 | 27 | res.setHeader("Access-Control-Allow-Origin", origin) 28 | res.setHeader("Access-Control-Allow-Headers", sHeaders) 29 | res.setHeader("Access-Control-Allow-Methods", sMethods) 30 | 31 | if req.method == "OPTIONS" { // we handle the options 32 | // Note: This is off by default. OPTIONS is handled differently by the 33 | // Express final handler (it passes with a 200). 34 | if handleOptions { 35 | res.setHeader("Allow", sMethods) 36 | res.writeHead(200) 37 | res.end() 38 | } 39 | else { 40 | next() // bubble up, there may be more OPTIONS stuff 41 | } 42 | } 43 | else { 44 | next() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/connect/Connect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connect.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 5/3/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import class http.Server 10 | import func http.createServer 11 | 12 | @_exported import class http.IncomingMessage 13 | @_exported import class http.ServerResponse 14 | 15 | public enum ConnectModule {} 16 | 17 | public extension ConnectModule { 18 | 19 | @inlinable 20 | static func connect(middleware: Middleware...) -> Connect { 21 | let app = Connect() 22 | middleware.forEach { app.use($0) } 23 | return app 24 | } 25 | } 26 | 27 | @inlinable 28 | public func connect(middleware: Middleware...) -> Connect { 29 | let app = Connect() 30 | middleware.forEach { app.use($0) } 31 | return app 32 | } 33 | 34 | public class Connect { 35 | 36 | struct MiddlewareEntry { 37 | 38 | let urlPrefix : String? 39 | let middleware : Middleware 40 | 41 | init(middleware: @escaping Middleware) { 42 | self.middleware = middleware 43 | self.urlPrefix = nil 44 | } 45 | 46 | init(urlPrefix: String, middleware: @escaping Middleware) { 47 | self.urlPrefix = urlPrefix 48 | self.middleware = middleware 49 | } 50 | 51 | func matches(request rq: IncomingMessage) -> Bool { 52 | if urlPrefix != nil && !rq.url.isEmpty { 53 | guard rq.url.hasPrefix(urlPrefix!) else { return false } 54 | } 55 | 56 | return true 57 | } 58 | 59 | } 60 | 61 | var middlewarez = ContiguousArray() 62 | 63 | public init() {} 64 | 65 | 66 | // MARK: - use() 67 | 68 | @discardableResult 69 | public func use(_ cb: @escaping Middleware) -> Self { 70 | middlewarez.append(MiddlewareEntry(middleware: cb)) 71 | return self 72 | } 73 | @discardableResult 74 | public func use(_ p: String, _ cb: @escaping Middleware) -> Self { 75 | middlewarez.append(MiddlewareEntry(urlPrefix: p, middleware: cb)) 76 | return self 77 | } 78 | 79 | 80 | // MARK: - Closures to pass on 81 | 82 | public var handle : ( IncomingMessage, ServerResponse ) -> Void { 83 | return { req, res in 84 | self.doRequest(req, res) 85 | } 86 | } 87 | public var middleware : Middleware { 88 | return { req, res, cb in 89 | self.doRequest(req, res) // THIS IS WRONG, need to call cb() only on last 90 | cb() 91 | } 92 | } 93 | 94 | 95 | // MARK: - run middleware 96 | 97 | func doRequest(_ request: IncomingMessage, _ response: ServerResponse) { 98 | final class State { 99 | var stack : ArraySlice 100 | let request : IncomingMessage 101 | let response : ServerResponse 102 | var next : Next? 103 | 104 | init(_ stack : ArraySlice, 105 | _ request : IncomingMessage, 106 | _ response : ServerResponse, 107 | _ next : @escaping Next) 108 | { 109 | self.stack = stack 110 | self.request = request 111 | self.response = response 112 | self.next = next 113 | } 114 | 115 | func step(_ args : Any...) { 116 | if let middleware = stack.popFirst() { 117 | do { 118 | try middleware(request, response, self.step) 119 | } 120 | catch { 121 | self.step(error) 122 | } 123 | } 124 | else { 125 | next?(); next = nil 126 | } 127 | } 128 | } 129 | 130 | func finalHandler(_ args: Any...) { 131 | response.writeHead(404) 132 | response.end() 133 | } 134 | 135 | // first lookup all middleware matching the request (i.e. the URL prefix 136 | // matches) 137 | // TODO: would be nice to have this as a lazy filter. 138 | 139 | let middleware = middlewarez.filter { $0.matches(request: request) } 140 | .map { $0.middleware } 141 | let state = State(middleware[middleware.indices], 142 | request, response, finalHandler) 143 | state.step() 144 | } 145 | 146 | } 147 | 148 | 149 | // MARK: - Wrap Server 150 | 151 | public extension Connect { 152 | 153 | /** 154 | * Create an HTTP server (using http.server) with the `Connect` instance 155 | * as the handler, and then start listening. 156 | * 157 | * - Parameters: 158 | * - port : The port the server should listen on. 159 | * - host : The host to bind the socket to, 160 | * defaults to wildcard IPv4 (0.0.0.0). 161 | * - backlog : The amount of socket backlog space (defaults to 512). 162 | * - onListening : An optional closure to run when the server started 163 | * listening. 164 | */ 165 | @inlinable 166 | @discardableResult 167 | func listen(_ port: Int?, _ host: String = "0.0.0.0", backlog: Int = 512, 168 | onListening execute: (( http.Server ) -> Void)? = nil) -> Self 169 | { 170 | let server = http.createServer(handler: self.handle) 171 | _ = server.listen(port, "0.0.0.0", backlog: backlog, onListening: execute) 172 | return self 173 | } 174 | 175 | /** 176 | * Create an HTTP server (using http.server) with the `Connect` instance 177 | * as the handler, and then start listening. 178 | * 179 | * - Parameters: 180 | * - port : The port the server should listen on. 181 | * - host : The host to bind the socket to, 182 | * defaults to wildcard IPv4 (0.0.0.0). 183 | * - backlog : The amount of socket backlog space (defaults to 512). 184 | * - onListening : An optional closure to run when the server started 185 | * listening. 186 | */ 187 | @inlinable 188 | @discardableResult 189 | func listen(_ port: Int?, _ host: String = "0.0.0.0", backlog: Int = 512, 190 | onListening execute: @escaping () -> Void) -> Self 191 | { 192 | return listen(port, host, backlog: backlog) { _ in execute() } 193 | } 194 | } 195 | 196 | import func MacroCore.__dirname 197 | 198 | #if swift(>=5.3) 199 | @inlinable 200 | public func __dirname(caller: String = #filePath) -> String { 201 | return MacroCore.__dirname(caller: caller) 202 | } 203 | #else 204 | @inlinable 205 | public func __dirname(caller: String = #file) -> String { 206 | return MacroCore.__dirname(caller: caller) 207 | } 208 | #endif 209 | -------------------------------------------------------------------------------- /Sources/connect/CookieParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CookieParser.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 6/16/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import enum MacroCore.process 10 | import protocol MacroCore.EnvironmentKey 11 | import class http.IncomingMessage 12 | 13 | /** 14 | * After running the `cookieParser` middleware you can access the cookies 15 | * via `request.cookies` (a [String:String]). 16 | * 17 | * Example: 18 | * 19 | * app.use(cookieParser()) 20 | * app.get("/cookies") { req, res, _ in 21 | * res.json(req.cookies) 22 | * } 23 | * 24 | */ 25 | public func cookieParser() -> Middleware { 26 | return { req, res, next in 27 | if req[CookieKey.self] == nil { 28 | let cookies = Cookies(req, res) 29 | req.environment[CookieKey.self] = cookies.cookies // grab all 30 | } 31 | next() 32 | } 33 | } 34 | 35 | // MARK: - IncomingMessage extension 36 | 37 | private enum CookieKey: EnvironmentKey { 38 | static let defaultValue : [ String : String ]? = nil 39 | static let loggingKey = "cookie" 40 | } 41 | 42 | public extension IncomingMessage { 43 | 44 | /// Returns the cookies embedded in the request. Note: Make sure to invoke 45 | /// the `cookieParser` middleware first, so that this property is actually 46 | /// filled. 47 | var cookies : [ String : String ] { 48 | get { 49 | // This concept is a little weird as so many thinks in Node. Why not just 50 | // parse the cookies on-demand? 51 | guard let cookies = self[CookieKey.self] else { 52 | process.emitWarning( 53 | "attempt to access `cookies` of request, " + 54 | "but cookieParser middleware wasn't invoked" 55 | ) 56 | 57 | // be smart 58 | let cookies = Cookies(self) 59 | self.environment[CookieKey.self] = cookies.cookies // grab all 60 | return cookies.cookies 61 | } 62 | 63 | return cookies 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/connect/Cookies.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cookies.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 10/06/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import let MacroCore.console 10 | import class http.IncomingMessage 11 | import class http.ServerResponse 12 | 13 | // "Set-Cookie:" Name "=" Value *( ";" Attribute) 14 | // "Cookie:" Name "=" Value *( ";" Name "=" Value) 15 | // 16 | // TODO: 17 | // - do proper RFC 2109, add quoting and such. 18 | // - add signing ala keygrip 19 | // - support for `secure` (only use with https) 20 | 21 | /// Module and object at the same time 22 | /// 23 | /// Usage: 24 | /// 25 | /// let cookies = Cookies(req, res) 26 | /// 27 | /// cookies.set("theAnswer", "42") // set a cookie 28 | /// if let answer = cookies.get("theAnswer") // get a cookie 29 | /// 30 | public final class Cookies { 31 | 32 | public let res : ServerResponse? 33 | 34 | public let cookies : [ String : String ] 35 | 36 | public init(_ req: IncomingMessage, _ res: ServerResponse? = nil) { 37 | self.res = res 38 | 39 | // request values we care about 40 | self.cookies = req.extractStringCookieDictionary() 41 | } 42 | 43 | 44 | // get/set funcs 45 | 46 | public func get(_ name: String) -> String? { 47 | return cookies[name] 48 | } 49 | 50 | public func set(cookie c: Cookie) { 51 | guard res != nil else { 52 | console.warn("attempt to set cookie, but got no response object!") 53 | return 54 | } 55 | res!.setHeader("Set-Cookie", c.description) 56 | } 57 | 58 | public func set(_ name: String, _ value: String, 59 | path : String? = "/", 60 | httpOnly : Bool = true, 61 | domain : String? = nil, 62 | comment : String? = nil, 63 | expires : Date? = nil, 64 | maxAge : Int? = nil) 65 | { 66 | // TODO: 67 | // - check `secure`. Node has `req.protocol` == https ? 68 | 69 | let cookie = Cookie(name: name, value: value, 70 | path: path, httpOnly: httpOnly, 71 | domain: domain, comment: comment, 72 | maxAge: maxAge, expires: expires) 73 | set(cookie: cookie) 74 | } 75 | 76 | public func reset(_ name: String) { 77 | set(cookie: Cookie(name: name, maxAge: 0)) 78 | } 79 | 80 | // subscript 81 | 82 | public subscript(name : String) -> String? { 83 | set { 84 | if let newValue = newValue { 85 | set(name, newValue) 86 | } 87 | else { 88 | console.error("attempt to set nil-value cookie: \(name), ignoring.") 89 | } 90 | } 91 | get { 92 | return get(name) 93 | } 94 | } 95 | } 96 | 97 | extension Cookies: CustomStringConvertible { 98 | public var description: String { 99 | var ms = " String { 178 | // TBD: %Z emits UTC 179 | let HTTPDateFormat = "%a, %d %b %Y %H:%M:%S GMT" 180 | return ts.componentsInUTC.format(HTTPDateFormat) 181 | } 182 | 183 | s += generateDateHeader(timestamp: time_t(v.timeIntervalSince1970)) 184 | } 185 | 186 | return s 187 | } 188 | } 189 | 190 | extension Cookie : CustomStringConvertible { 191 | public var description: String { 192 | return httpHeaderValue 193 | } 194 | } 195 | 196 | import let xsys.strchr 197 | import let xsys.strlen 198 | import let xsys.memcpy 199 | 200 | extension String { 201 | // Ah, this extension is crap. FIXME 202 | 203 | func trim(splitchar c: Int8 = 32) -> String { 204 | return withCString { start in 205 | if strchr(start, Int32(c)) == nil { return self } // contains no trimchar 206 | 207 | var p = start 208 | var didTrimLeft = false 209 | while p.pointee == c && p.pointee != 0 { p += 1; didTrimLeft = true } 210 | guard p.pointee != 0 else { return "" } 211 | 212 | var len = Int(strlen(p)) 213 | var didTrimRight = false 214 | while len > 0 && p[len - 1] == c { 215 | len -= 1 216 | didTrimRight = true 217 | } 218 | guard len != 0 else { return "" } 219 | 220 | if !didTrimLeft && !didTrimRight { return self } // as-is 221 | if !didTrimRight { return String(cString: p) } 222 | 223 | // lame and slow zero terminate 224 | let buflen = len + 1 225 | let buf = UnsafeMutablePointer.allocate(capacity: buflen) 226 | _ = memcpy(buf, p, len) 227 | buf[len] = 0 // zero terminate 228 | 229 | let s = String(cString: buf) 230 | buf.deallocate() 231 | return s 232 | } 233 | } 234 | 235 | func splitAndTrim(splitchar c: UInt8) -> [ String ] { 236 | guard !isEmpty else { return [] } 237 | 238 | let splitChar : UInt8 = 59 // semicolon 239 | let rawFields = utf8.split(separator: splitChar) 240 | 241 | // TODO: lame imp, too much copying 242 | var fields = Array() 243 | fields.reserveCapacity(rawFields.count) 244 | 245 | for field in rawFields { 246 | guard field.count > 0 else { continue } 247 | 248 | let s = String(field)! 249 | fields.append(s.trim()) 250 | } 251 | 252 | // TODO: split on ';', trim 253 | return fields 254 | } 255 | 256 | func splitPair(splitchar c: UInt8) -> ( String, String ) { 257 | let splits = utf8.split(separator: c, maxSplits: 1) 258 | guard splits.count > 1 else { return ( self, "" ) } 259 | assert(splits.count == 2, "max split was 1, but got more items?") 260 | let s0 : UTF8View.SubSequence = splits[0], s1 = splits[1] 261 | return ( String(decoding: s0, as: UTF8.self), 262 | String(decoding: s1, as: UTF8.self) ) 263 | } 264 | } 265 | 266 | private func splitCookieFields(headerValue v: String) -> [ String ] { 267 | return v.splitAndTrim(splitchar: 59) // semicolon 268 | } 269 | 270 | private extension IncomingMessage { 271 | 272 | func extractStringCookieDictionary() -> [ String : String ] { 273 | // Note: This just picks the first cookie! Newer clients send multiple 274 | // cookies, but in proper ordering. 275 | 276 | var result = Dictionary() 277 | 278 | for rawCookie in extractStringCookieHeaderArray() { 279 | let cEqual : UInt8 = 61 280 | let ( name, value ) = rawCookie.splitPair(splitchar: cEqual) 281 | 282 | guard result[name] == nil else { continue } // multiple cookies same name 283 | result[name] = value 284 | } 285 | 286 | return result 287 | } 288 | 289 | func extractStringCookieHeaderArray() -> [ String ] { 290 | let cookieHeader = headers["Cookie"] 291 | return cookieHeader.reduce([], { $0 + splitCookieFields(headerValue: $1) }) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Sources/connect/CrossCompile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrossCompile.swift 3 | // Macro 4 | // 5 | // Created by Helge Heß 6 | // Copyright © 2016-2021 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | // Dupes to support: 10 | // https://github.com/SPMDestinations/homebrew-tap/issues/2 11 | 12 | import xsys 13 | #if os(Windows) 14 | import WinSDK 15 | #elseif os(Linux) 16 | import Glibc 17 | #elseif canImport(Darwin) 18 | import Darwin 19 | #elseif canImport(WASIClib) 20 | import WASIClib 21 | #else 22 | #error("Unsupported C runtime.") 23 | #endif 24 | 25 | // MARK: - Macro/xsys/timeval_any.swift 26 | 27 | #if !os(Windows) 28 | internal extension timespec { 29 | var milliseconds : Int { 30 | return (tv_sec * 1000) + (tv_nsec / 1000000) 31 | } 32 | } 33 | #endif 34 | 35 | 36 | // MARK: - Macro/xsys/timespec.swift 37 | 38 | #if os(Windows) 39 | #elseif os(Linux) 40 | typealias timespec = Glibc.timespec 41 | 42 | extension timespec { 43 | 44 | static func monotonic() -> timespec { 45 | var ts = timespec() 46 | clock_gettime(CLOCK_MONOTONIC, &ts) 47 | return ts 48 | } 49 | 50 | } 51 | #else // Darwin 52 | typealias timespec = Darwin.timespec 53 | typealias timeval = Darwin.timeval 54 | 55 | internal extension timespec { 56 | 57 | init(_ mts: mach_timespec_t) { 58 | #if swift(>=4.1) 59 | self.init() 60 | #endif 61 | tv_sec = __darwin_time_t(mts.tv_sec) 62 | tv_nsec = Int(mts.tv_nsec) 63 | } 64 | 65 | static func monotonic() -> timespec { 66 | var cclock = clock_serv_t() 67 | var mts = mach_timespec_t() 68 | 69 | host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock); 70 | clock_get_time(cclock, &mts); 71 | mach_port_deallocate(mach_task_self_, cclock); 72 | 73 | return timespec(mts) 74 | } 75 | } 76 | #endif // Darwin 77 | 78 | 79 | // MARK: - Macro/xsys/time.swift 80 | 81 | #if !os(Windows) 82 | #if os(Windows) 83 | import WinSDK 84 | #elseif os(Linux) 85 | import Glibc 86 | 87 | typealias struct_tm = Glibc.tm 88 | #else 89 | import Darwin 90 | 91 | typealias struct_tm = Darwin.tm 92 | #endif 93 | 94 | /// The Unix `tm` struct is essentially NSDateComponents PLUS some timezone 95 | /// information (isDST, offset, tz abbrev name). 96 | internal extension xsys.struct_tm { 97 | 98 | /// Create a Unix date components structure from a timestamp. This variant 99 | /// creates components in the local timezone. 100 | init(_ tm: time_t) { 101 | self = tm.componentsInLocalTime 102 | } 103 | 104 | /// Create a Unix date components structure from a timestamp. This variant 105 | /// creates components in the UTC timezone. 106 | init(utc tm: time_t) { 107 | self = tm.componentsInUTC 108 | } 109 | 110 | var utcTime : time_t { 111 | var tm = self 112 | return timegm(&tm) 113 | } 114 | var localTime : time_t { 115 | var tm = self 116 | return mktime(&tm) 117 | } 118 | 119 | /// Example `strftime` format (`man strftime`): 120 | /// "%a, %d %b %Y %H:%M:%S GMT" 121 | /// 122 | func format(_ sf: String, defaultCapacity: Int = 100) -> String { 123 | var tm = self 124 | 125 | // Yes, yes, I know. 126 | let attempt1Capacity = defaultCapacity 127 | let attempt2Capacity = defaultCapacity > 1024 ? defaultCapacity * 2 : 1024 128 | var capacity = attempt1Capacity 129 | 130 | var buf = UnsafeMutablePointer.allocate(capacity: capacity) 131 | #if swift(>=4.1) 132 | defer { buf.deallocate() } 133 | #else 134 | defer { buf.deallocate(capacity: capacity) } 135 | #endif 136 | 137 | let rc = xsys.strftime(buf, capacity, sf, &tm) 138 | 139 | if rc == 0 { 140 | #if swift(>=4.1) 141 | buf.deallocate() 142 | #else 143 | buf.deallocate(capacity: capacity) 144 | #endif 145 | capacity = attempt2Capacity 146 | buf = UnsafeMutablePointer.allocate(capacity: capacity) 147 | 148 | let rc = xsys.strftime(buf, capacity, sf, &tm) 149 | assert(rc != 0) 150 | guard rc != 0 else { return "" } 151 | } 152 | 153 | return String(cString: buf); 154 | } 155 | } 156 | 157 | #endif // !os(Windows) 158 | 159 | 160 | // MARK: - Macro/fs/Utils/StatStruct 161 | 162 | #if !os(Windows) 163 | #if os(Linux) 164 | import let Glibc.S_IFMT 165 | import let Glibc.S_IFREG 166 | import let Glibc.S_IFDIR 167 | #else 168 | import let Darwin.S_IFMT 169 | import let Darwin.S_IFREG 170 | import let Darwin.S_IFDIR 171 | #endif 172 | 173 | internal extension xsys.stat_struct { 174 | func isFile() -> Bool { return (st_mode & S_IFMT) == S_IFREG } 175 | func isDirectory() -> Bool { return (st_mode & S_IFMT) == S_IFDIR } 176 | var size : Int { return Int(st_size) } 177 | } 178 | #endif // !os(Windows) 179 | -------------------------------------------------------------------------------- /Sources/connect/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 31/05/16. 6 | // Copyright © 2016-2024 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import enum MacroCore.process 10 | import let MacroCore.console 11 | import class http.IncomingMessage 12 | import class http.ServerResponse 13 | import xsys // timespec and extensions 14 | #if os(Linux) 15 | import func Glibc.isatty 16 | #else 17 | import func Darwin.isatty 18 | #endif 19 | 20 | // TODO: do some actual parsing of formats :-) 21 | 22 | /// Logging middleware. 23 | /// 24 | /// Currently accepts four formats: 25 | /// - default 26 | /// - short 27 | /// - tiny 28 | /// - dev (colorized status) 29 | /// 30 | public func logger(_ format: String = "default") -> Middleware { 31 | return { req, res, next in 32 | let startTS = timespec.monotonic() 33 | let fmt = formats[format] ?? format 34 | 35 | func printLog() { 36 | let endTS = timespec.monotonic() 37 | let diff = (endTS - startTS).milliseconds 38 | let info = LogInfoProvider(req: req, res: res, diff: diff) 39 | var msg = "" 40 | 41 | switch fmt { 42 | case formats["short"]!: 43 | msg += "\(info.remoteAddr) -" 44 | msg += " \"\(req.method) \(req.url) HTTP/\(req.httpVersion)\"" 45 | msg += " \(info.status) \(info.clen)" 46 | msg += " - \(info.responseTime) ms" 47 | 48 | case formats["dev"]!: 49 | msg += "\(req.method) \(info.paddedURL)" 50 | msg += " \(info.colorStatus) \(info.clen)" 51 | let rts = "\(info.responseTime)" 52 | let rt = rts.count < 3 53 | ? rts.padding(toLength: 3, withPad: " ", startingAt: 0) 54 | : rts 55 | msg += " - \(rt) ms" 56 | 57 | case formats["tiny"]!: 58 | msg += "\(req.method) \(req.url)" 59 | msg += " \(info.status) \(info.clen)" 60 | msg += " - \(info.responseTime) ms" 61 | 62 | case formats["default"]!: 63 | fallthrough 64 | default: 65 | msg += "\(info.remoteAddr) - - [\(info.date)]" 66 | msg += " \"\(req.method) \(req.url) HTTP/\(req.httpVersion)\"" 67 | msg += " \(info.status) \(info.clen)" 68 | msg += " \(info.qReferrer) \(info.qUA)" 69 | } 70 | 71 | // let msg = res.statusMessage ?? HTTPStatus.text(forStatus: res.statusCode!) 72 | console.log(msg) 73 | } 74 | 75 | _ = res.onceFinish { printLog() } 76 | next() 77 | } 78 | } 79 | 80 | 81 | private let formats = [ 82 | "default": 83 | ":remote-addr - - [:date] \":method :url HTTP/:http-version\"" + 84 | " :status :res[content-length] \":referrer\" \":user-agent\"", 85 | "short": 86 | ":remote-addr - :method :url HTTP/:http-version" + 87 | " :status :res[content-length] - :response-time ms", 88 | "tiny": ":method :url :status :res[content-length] - :response-time ms", 89 | "dev": 90 | ":method :paddedurl :colorstatus :res[content-length] - :response-time ms" 91 | ] 92 | 93 | 94 | private struct LogInfoProvider { 95 | 96 | let req : IncomingMessage 97 | let res : ServerResponse 98 | let diff : Int 99 | let noval = "-" 100 | 101 | var remoteAddr : String { 102 | guard let sock = req.socket else { return noval } 103 | guard let addr = sock.remoteAddress else { return noval } 104 | return addr.description 105 | } 106 | var responseTime : String { return "\(diff)" } 107 | 108 | var ua : String? { return req.headers["User-Agent"].first } 109 | var referrer : String? { return req.headers["Referrer"] .first } 110 | 111 | var qReferrer : String { 112 | guard let s = referrer else { return noval } 113 | return "\"\(s)\"" 114 | } 115 | var qUA : String { 116 | guard let s = ua else { return noval } 117 | return "\"\(s)\"" 118 | } 119 | 120 | var date : String { 121 | // 31/May/2016:07:53:29 +0200 122 | let logdatefmt = "%d/%b/%Y:%H:%M:%S %z" 123 | let time = xsys.time(nil).componentsInLocalTime 124 | return "\(time.format(logdatefmt))" 125 | } 126 | 127 | var clen : String { 128 | let clenI = Int((res.getHeader("Content-Length") as? String) ?? "") ?? -1 129 | return clenI >= 0 ? "\(clenI)" : noval 130 | } 131 | 132 | var status : String { 133 | return res.statusCode > 0 ? "\(res.statusCode)" : noval 134 | } 135 | var colorStatus : String { 136 | let colorStatus : String 137 | 138 | if !shouldDoColorLogging { 139 | colorStatus = self.status 140 | } 141 | else if res.statusCode > 0 { 142 | switch res.statusCode { 143 | case 200..<300: colorStatus = "\u{001B}[0;32m\(status)\u{001B}[0m" 144 | case 300..<400: colorStatus = "\u{001B}[0;34m\(status)\u{001B}[0m" 145 | case 400..<500: colorStatus = "\u{001B}[0;35m\(status)\u{001B}[0m" 146 | case 500..<600: colorStatus = "\u{001B}[0;31m\(status)\u{001B}[0m" 147 | default: colorStatus = "\(status)" 148 | } 149 | } 150 | else { 151 | colorStatus = noval 152 | } 153 | 154 | return colorStatus 155 | } 156 | 157 | static var urlPadLen = 28 158 | var paddedURL : String { 159 | let url = req.url 160 | let oldLength = url.count 161 | if oldLength > LogInfoProvider.urlPadLen { 162 | LogInfoProvider.urlPadLen = oldLength + ( oldLength % 2) 163 | } 164 | let padlen = LogInfoProvider.urlPadLen 165 | 166 | // right pad :-) 167 | let s = Array(repeating: " ", count: (padlen - oldLength)) 168 | return url + String(s) 169 | } 170 | } 171 | 172 | #if os(Windows) 173 | import func WinSDK.strcmp 174 | #elseif os(Linux) 175 | import func Glibc.strcmp 176 | #else 177 | import func Darwin.strcmp 178 | #endif 179 | 180 | fileprivate let shouldDoColorLogging : Bool = { 181 | // TODO: Add `isTTY` from Noze 182 | let isStdoutTTY = isatty(xsys.STDOUT_FILENO) != 0 183 | if !isStdoutTTY { return false } 184 | if let s = xsys.getenv("TERM"), strcmp(s, "dumb") == 0 { return false } 185 | if process.isRunningInXCode { return false } 186 | return true 187 | }() 188 | -------------------------------------------------------------------------------- /Sources/connect/MethodOverride.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MethodOverride.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 5/31/16. 6 | // Copyright © 2016-2024 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import let MacroCore.console 10 | 11 | /** 12 | * Enables support for the `X-HTTP-Method-Override` header. 13 | * 14 | * The `X-HTTP-Method-Override` allows the client to override the actual HTTP 15 | * method (e.g. `GET` or `PUT`) with a different one. 16 | * I.e. the `IncomingMessage/method` property will be set to the specified 17 | * method. 18 | * 19 | * This is sometimes used w/ the HTTP stack is only setup to process say `GET` 20 | * or `POST` requests, but not something more elaborate like `MKCALENDAR`. 21 | * 22 | * - Parameters: 23 | * - header: The header to check for the method override, defaults to 24 | * `X-HTTP-Method-Override`. This header will contain the method 25 | * name. 26 | * - methods: The whitelisted methods that allow the override, defaults to 27 | * just `POST`. 28 | * - Returns: A middleware functions that applies the methodOverride. 29 | */ 30 | public func methodOverride(header : String = "X-HTTP-Method-Override", 31 | methods : [ String ] = [ "POST" ]) 32 | -> Middleware 33 | { 34 | return { req, res, next in 35 | // TODO: support query values 36 | 37 | guard methods.contains(req.method) else { next(); return } 38 | guard let hvs = req.headers[header].first else { next(); return } 39 | 40 | // patch method and continue 41 | req.method = hvs 42 | next() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/connect/Middleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Middleware.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 5/3/16. 6 | // Copyright © 2016-2023 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import class http.IncomingMessage 10 | import class http.ServerResponse 11 | 12 | /** 13 | * The arguments to next() depend on the actual router, but may include: 14 | * 15 | * - `route` / `router` (skip the remaining middleware in the router) 16 | * - a `Swift.Error` object in case of error 17 | */ 18 | public typealias Next = ( Any... ) -> Void 19 | 20 | /** 21 | * Middleware are just functions that deal with HTTP transactions. 22 | * 23 | * They take a request (``IncomingMessage``) and response (``ServerResponse``) 24 | * object as well as a closure to signal whether they fully handled the request 25 | * or whether the respective "Router" (e.g. Connect) should run the next 26 | * middleware. 27 | * 28 | * Call ``Next`` when the request processing needs to continue, just return if 29 | * the request was fully processed. 30 | */ 31 | public typealias Middleware = 32 | ( IncomingMessage, ServerResponse, @escaping Next ) 33 | throws -> Void 34 | 35 | /** 36 | * Middleware are just functions that deal with HTTP transactions. 37 | * `FinalMiddleware` is middleware that always ends the response, i.e. would 38 | * never call the ``Next`` completion handler of regular ``Middleware``. 39 | * 40 | * They take a request (``IncomingMessage``) and response (``ServerResponse``). 41 | */ 42 | public typealias FinalMiddleware = 43 | ( IncomingMessage, ServerResponse ) throws -> Void 44 | -------------------------------------------------------------------------------- /Sources/connect/Pause.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pause.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 21/07/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import func MacroCore.setTimeout 10 | 11 | /// Middleware to simulate latency. 12 | /// 13 | /// Pause all requests: 14 | /// 15 | /// app.use(pause(1337)) // wait for 1337ms, then continue 16 | /// app.get("/") { req, res in 17 | /// res.send("Waited 1337 ms") 18 | /// } 19 | /// 20 | public func pause(_ timeout: Int, _ error: Error? = nil) -> Middleware { 21 | return { req, res, next in 22 | setTimeout(timeout) { 23 | if let error = error { 24 | next(error) 25 | } 26 | else { 27 | next() 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/connect/README.md: -------------------------------------------------------------------------------- 1 |

Macro Connect 2 | 4 |

5 | 6 | Connect is a Macro module modelled after the 7 | [Connect](https://github.com/senchalabs/connect#readme) 8 | JavaScript framework from SenchaLabs. 9 | As usual in Noze.io the module is supposed to be as similar as possible. 10 | 11 | Connect builds on top of the Macro http module. 12 | It adds the concept of *middleware*. Middleware are just functions that can 13 | deal with HTTP requests. 14 | They take a request (`IncomingMessage`) and response (`ServerResponse`) object 15 | as well as a callback to signal whether they fully handled the request 16 | or whether Connect should run the next middleware. 17 | That way you can stack middleware functions in a processing queue. 18 | Some middleware will just enhance the 19 | request object, like the cookie-parser, while some other middleware can deliver 20 | data, like the `serveStatic` middleware. 21 | 22 | **Express**: Connect adds the concept of middleware to the `http` module. Note 23 | that there is another module called 24 | [Express](../express) 25 | which builds on top of Connect. Express provides even more advanced routing 26 | capabilities and other extras. 27 | 28 | Show us some code! Example: 29 | 30 | ```swift 31 | import connect 32 | import process 33 | 34 | let __dirname = process.cwd() 35 | 36 | let app = connect() 37 | app.use(logger("dev")) // this middleware logs requests 38 | app.use(serveStatic(_dirname + "/public")) 39 | app.listen(1337) { print("server lisetning: \($0)") } 40 | ``` 41 | 42 | It is a very basic HTTP server that serves all files living in the 'public' 43 | directory and logs the requests on stdout. 44 | 45 | ## Included Middleware 46 | 47 | - [pause](Pause.swift) 48 | - [serveStatic](ServeStatic.swift) 49 | - [session](Session.swift) 50 | - [methodOverride](MethodOverride.swift) 51 | - [bodyParser](BodyParser.swift) 52 | - [bodyParser.urlencoded](BodyParser.swift) 53 | - [bodyParser.json](BodyParser.swift) 54 | - [bodyParser.raw](BodyParser.swift) 55 | - [bodyParser.tex](BodyParser.swift) 56 | - [cookieParser](CookieParser.swift) 57 | - [cors](CORS.swift) 58 | - [logger](Logger.swift) 59 | -------------------------------------------------------------------------------- /Sources/connect/ServeStatic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServeStatic.swift 3 | // Noze.io / MacroExpress 4 | // 5 | // Created by Helge Heß on 08/05/16. 6 | // Copyright © 2016-2021 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import enum MacroCore.process 10 | import struct Foundation.URL 11 | import fs 12 | import mime 13 | import xsys 14 | 15 | /** 16 | * Specify file permissions for `serveStatic` 17 | */ 18 | public enum ServeFilePermission { 19 | 20 | /// Allow access to the file. 21 | case allow 22 | 23 | /// Deny access to the file, will usually return a 404 (as a 403 exposes the 24 | /// existence) 25 | case deny 26 | 27 | /// Ignore a file, i.e. behave as if it didn't exist 28 | case ignore 29 | } 30 | 31 | /** 32 | * Specifies the supported processing of index-files. 33 | * 34 | * If the local path targetted by serveStatic is a directory, 35 | * serveStatic will check for index-files as specified by 36 | * this behaviour. 37 | * Possible modes are: no index file handling, a single index file or multiple 38 | * index files. 39 | */ 40 | public enum IndexBehaviour { 41 | 42 | /// Don't do any index-file processing. 43 | case none 44 | 45 | /// Support a single index-file with the specified name (e.g. index.html) 46 | case indexFile (String) 47 | 48 | /// Support a set of index files. 49 | case indexFiles([ String ]) 50 | 51 | @inlinable 52 | public init() { 53 | self = .indexFile("index.html") 54 | } 55 | } 56 | 57 | /** 58 | * Options supported by `serveStatic`. 59 | */ 60 | public struct ServeStaticOptions { 61 | 62 | public let dotfiles = ServeFilePermission.allow 63 | public let etag = false 64 | public let extensions : [ String ]? = nil 65 | public let index = IndexBehaviour() 66 | public let lastModified = true 67 | public let redirect = true 68 | public let `fallthrough` = true 69 | 70 | public init() {} // otherwise init is private 71 | } 72 | 73 | public enum ServeStaticError: Swift.Error { 74 | case couldNotConstructIndexPath 75 | case couldNotParseURL 76 | case fileMissing (URL) 77 | case indexFileIsNotAFile(URL) 78 | case pathIsNotAFile (URL) 79 | } 80 | 81 | /** 82 | * Serve static files, designed after: 83 | * 84 | * https://github.com/expressjs/serve-static 85 | * 86 | * Example: 87 | * 88 | * app.use(serveStatic(__dirname() + "/public")) 89 | * 90 | */ 91 | public func serveStatic(_ p : String = process.cwd(), 92 | options o : ServeStaticOptions = ServeStaticOptions()) 93 | -> Middleware 94 | { 95 | // Note: 'static' is a reserved word ... 96 | // TODO: wrapped request with originalUrl, baseUrl etc 97 | 98 | let baseFileURL = URL(fileURLWithPath: p.isEmpty ? process.cwd() : p) 99 | .standardized 100 | 101 | // options 102 | let options = ServeStaticOptions() 103 | 104 | // middleware 105 | return { req, res, next in 106 | // we only want HEAD + GET 107 | guard req.method == "HEAD" || req.method == "GET" else { 108 | if o.fallthrough { return next() } 109 | res.writeHead(.methodNotAllowed) 110 | res.end() 111 | return 112 | } 113 | 114 | // parse URL 115 | guard let url = URL(string: req.url)?.standardized, !url.path.isEmpty else { 116 | if o.fallthrough { return next() } 117 | return next(ServeStaticError.couldNotParseURL) 118 | } 119 | let rqPath = url.path 120 | 121 | // FIXME: sanitize URL, remove '..' etc!!! 122 | 123 | // naive implementation 124 | let fsURL : URL 125 | if rqPath.isEmpty || rqPath == "/" { 126 | fsURL = baseFileURL 127 | } 128 | else { 129 | let relPath = rqPath.hasPrefix("/") ? String(rqPath.dropFirst()) : rqPath 130 | guard let url = URL(string: relPath, relativeTo: baseFileURL) else { 131 | if o.fallthrough { return next() } 132 | return next(ServeStaticError.couldNotParseURL) 133 | } 134 | fsURL = url 135 | } 136 | 137 | 138 | // dotfiles 139 | 140 | if fsURL.lastPathComponent.hasPrefix(".") { 141 | switch options.dotfiles { 142 | case .allow: break 143 | case .ignore: next(); return 144 | case .deny: 145 | res.writeHead(404) 146 | res.end() 147 | return 148 | } 149 | } 150 | 151 | // FIXME: Use NIO sendfile 152 | 153 | // stat 154 | fs.stat(fsURL.path) { err, stat in 155 | guard let lStat = stat, err == nil else { 156 | if o.fallthrough { return next() } 157 | res.writeHead(.notFound) 158 | res.end() 159 | return 160 | } 161 | 162 | // directory 163 | 164 | if lStat.isDirectory() { 165 | if options.redirect && !rqPath.hasSuffix("/") { 166 | res.writeHead(.permanentRedirect, 167 | headers: [ "Location": rqPath + "/" ]) 168 | res.end() 169 | return 170 | } 171 | 172 | switch options.index { 173 | case .indexFile(let filename): 174 | guard let indexFileURL = 175 | URL(string: filename, relativeTo: fsURL) else { 176 | return next(ServeStaticError.couldNotConstructIndexPath) 177 | } 178 | 179 | fs.stat(indexFileURL.path) { err, stat in // TODO: reuse closure 180 | guard let lStat = stat, err == nil else { 181 | if o.fallthrough { return next() } 182 | res.writeHead(.notFound) 183 | res.end() 184 | return 185 | } 186 | guard lStat.isFile() else { 187 | return next(ServeStaticError.indexFileIsNotAFile(indexFileURL)) 188 | } 189 | 190 | if res.headers["Content-Type"].isEmpty, 191 | let type = mime.lookup(indexFileURL.path) 192 | { 193 | res.setHeader("Content-Type", type) 194 | } 195 | res.setHeader("Content-Length", lStat.size) 196 | 197 | // TODO: content-type? 198 | res.writeHead(200) 199 | if req.method == "HEAD" { res.end() } 200 | else { _ = fs.createReadStream(indexFileURL.path).pipe(res) } 201 | } 202 | return 203 | 204 | default: // TODO: implement multi-option 205 | res.writeHead(404) 206 | res.end() 207 | return 208 | } 209 | } 210 | 211 | 212 | // regular file 213 | 214 | guard lStat.isFile() else { 215 | return next(ServeStaticError.pathIsNotAFile(fsURL)) 216 | } 217 | 218 | if res.headers["Content-Type"].isEmpty, 219 | let type = mime.lookup(fsURL.path) 220 | { 221 | res.setHeader("Content-Type", type) 222 | } 223 | res.setHeader("Content-Length", lStat.size) 224 | 225 | res.writeHead(200) 226 | if req.method == "HEAD" { res.end() } 227 | else { 228 | _ = fs.createReadStream(fsURL.path).pipe(res) 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Sources/connect/Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Session.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 6/16/16. 6 | // Copyright © 2016-2024 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import let MacroCore.console 10 | import protocol MacroCore.EnvironmentKey 11 | import class http.IncomingMessage 12 | import class http.ServerResponse 13 | import struct Foundation.UUID 14 | import struct NIOConcurrencyHelpers.NIOLock 15 | 16 | fileprivate let sessionIdCookie = Cookie(name: "NzSID", maxAge: 3600) 17 | 18 | fileprivate var sessionIdCounter = 0 19 | 20 | public typealias SessionIdGenerator = ( IncomingMessage ) -> String 21 | 22 | func nextSessionID(msg: IncomingMessage) -> String { 23 | // https://github.com/SwiftWebUI/SwiftWebUI/issues/4 24 | return UUID().uuidString 25 | } 26 | 27 | public func session(store s : SessionStore = InMemorySessionStore(), 28 | cookie : Cookie? = nil, 29 | genid : SessionIdGenerator? = nil) 30 | -> Middleware 31 | { 32 | return { req, res, next in 33 | // This is just a workaround for recursive funcs crashing the compiler. 34 | let ctx = SessionContext(request : req, 35 | response : res, 36 | store : s, 37 | templateCookie : cookie ?? sessionIdCookie, 38 | genid : genid ?? nextSessionID) 39 | 40 | guard let sessionID = ctx.sessionID else { 41 | // no cookie with session-ID, register new 42 | ctx.configureNewSession() 43 | return next() 44 | } 45 | 46 | // TODO: This should do a session-checkout. 47 | // retrieve from store 48 | s.get(sessionID: sessionID) { err, session in 49 | if let rerr = err { 50 | req.log 51 | .notice("could not retrieve session with ID \(sessionID): \(rerr)") 52 | ctx.configureNewSession() 53 | return next() 54 | } 55 | 56 | guard let rsession = session else { 57 | req.log.notice( 58 | "No error, but could not retrieve session with ID \(sessionID)") 59 | ctx.configureNewSession() 60 | return next() 61 | } 62 | 63 | // found a session, store into request 64 | req.environment[SessionKey.self] = rsession 65 | ctx.pushSessionCookie() 66 | _ = res.onceFinish { 67 | // TODO: This should do the session-checkin. 68 | s.set(sessionID: sessionID, session: req.session) { err in 69 | if let err = err { 70 | req.log.error("could not save session \(sessionID): \(err)") 71 | } 72 | } 73 | } 74 | next() 75 | } 76 | } 77 | } 78 | 79 | class SessionContext { 80 | // FIXME: This is temporary until the swiftc crash with inner functions 81 | // is fixed. 82 | // A class because we pass it around in closures. 83 | 84 | let req : IncomingMessage 85 | let res : ServerResponse 86 | let store : SessionStore 87 | let template : Cookie 88 | let genid : SessionIdGenerator 89 | 90 | let cookies : Cookies 91 | var sessionID : String? = nil 92 | 93 | init(request: IncomingMessage, response: ServerResponse, 94 | store: SessionStore, templateCookie: Cookie, 95 | genid: @escaping SessionIdGenerator) 96 | { 97 | self.req = request 98 | self.res = response 99 | self.store = store 100 | self.template = templateCookie 101 | self.genid = genid 102 | 103 | // Derived 104 | self.cookies = Cookies(req, res) 105 | self.sessionID = self.cookies[templateCookie.name] 106 | } 107 | 108 | func pushSessionCookie() { 109 | guard let sessionID = self.sessionID else { return } 110 | var ourCookie = template 111 | ourCookie.value = sessionID 112 | cookies.set(cookie: ourCookie) 113 | } 114 | 115 | func configureNewSession() { 116 | let newSessionID = genid(req) 117 | req.environment[SessionKey.self] = Session() 118 | 119 | sessionID = newSessionID 120 | pushSessionCookie() 121 | 122 | // capture some stuff locally 123 | let req = self.req 124 | let store = self.store 125 | 126 | _ = res.onceFinish { 127 | store.set(sessionID: newSessionID, session: req.session) { err in 128 | if let err = err { 129 | req.log.error("could not save new session \(newSessionID): \(err)") 130 | } 131 | } 132 | } 133 | } 134 | 135 | @inlinable 136 | var hasSessionID : Bool { return self.sessionID != nil } 137 | } 138 | 139 | 140 | // MARK: - Session Class 141 | 142 | public class Session { 143 | // Reference type, so that we can do stuff like: 144 | // 145 | // req.session["a"] = 10 146 | // 147 | // kinda non-obvious. 148 | 149 | // This should not be concurrent but use a checkin/checkout system. 150 | fileprivate let lock = NIOLock() 151 | public var values = Dictionary() 152 | 153 | public subscript(key: String) -> Any? { 154 | set { 155 | lock.withLock { 156 | if let v = newValue { values[key] = v } 157 | else { _ = values.removeValue(forKey: key) } 158 | } 159 | } 160 | get { return lock.withLock { values[key] } } 161 | } 162 | 163 | public subscript(int key: String) -> Int { 164 | guard let v = lock.withLock({ values[key] }) else { return 0 } 165 | if let iv = v as? Int { return iv } 166 | #if swift(>=5.10) 167 | if let i = (v as? any BinaryInteger) { return Int(i) } 168 | #endif 169 | return Int("\(v)") ?? 0 170 | } 171 | } 172 | 173 | 174 | // MARK: - IncomingMessage extension 175 | 176 | private enum SessionKey: EnvironmentKey { 177 | static let defaultValue : Session? = nil 178 | static let loggingKey = "session" 179 | } 180 | 181 | public extension IncomingMessage { 182 | 183 | func registerNewSession() -> Session { 184 | let newSession = Session() 185 | environment[SessionKey.self] = newSession 186 | return newSession 187 | } 188 | 189 | var session : Session { 190 | return environment[SessionKey.self] ?? registerNewSession() 191 | } 192 | } 193 | 194 | 195 | // MARK: - Session Store 196 | 197 | public enum SessionStoreError : Error { 198 | case SessionNotFound 199 | case NotImplemented 200 | } 201 | 202 | public protocol SessionStore { 203 | // TODO: this needs the cookie timeout, so that the store can expire old 204 | // stuff 205 | // I don't particularily like the naming, but lets keep it close. 206 | 207 | /// Retrieve the session for the given ID 208 | func get(sessionID sid: String, _ cb: ( Error?, Session? ) -> Void) 209 | 210 | /// Store the session for the given ID 211 | func set(sessionID sid: String, session: Session, 212 | _ cb: ( Error? ) -> Void) 213 | 214 | /// Touch the session for the given ID 215 | func touch(sessionID sid: String, session: Session, 216 | _ cb: ( Error? ) -> Void) 217 | 218 | /// Destroy the session with the given session ID 219 | func destroy(sessionID sid: String, _ cb: ( String ) -> Void) 220 | 221 | /// Clear all sessions in the store 222 | func clear(cb: ( Error? ) -> Void ) 223 | 224 | /// Return the number of sessions in the store 225 | func length(cb: ( Error?, Int) -> Void) 226 | 227 | /// Return all sessions in the store, optional 228 | func all(cb: ( Error?, [ Session ] ) -> Void) 229 | } 230 | 231 | public extension SessionStore { 232 | func all(cb: ( Error?, [ Session ] ) -> Void) { 233 | cb(SessionStoreError.NotImplemented, []) 234 | } 235 | } 236 | 237 | public class InMemorySessionStore : SessionStore { 238 | 239 | // Well, there should be a checkout/checkin system?! 240 | fileprivate let lock = NIOLock() 241 | var store : [ String : Session ] 242 | 243 | public init() { 244 | store = [:] 245 | } 246 | 247 | public func get(sessionID sid: String, _ cb: ( Error?, Session? ) -> Void ) { 248 | guard let session = lock.withLock({ store[sid] }) else { 249 | cb(SessionStoreError.SessionNotFound, nil) 250 | return 251 | } 252 | cb(nil, session) 253 | } 254 | 255 | public func set(sessionID sid: String, session: Session, 256 | _ cb: ( Error? ) -> Void ) 257 | { 258 | lock.withLock { store[sid] = session } 259 | cb(nil) 260 | } 261 | 262 | public func touch(sessionID sid: String, session: Session, 263 | _ cb: ( Error? ) -> Void ) 264 | { 265 | cb(nil) 266 | } 267 | 268 | public func destroy(sessionID sid: String, _ cb: ( String ) -> Void) { 269 | lock.withLock { _ = store.removeValue(forKey: sid) } 270 | cb(sid) 271 | } 272 | 273 | public func clear(cb: ( Error? ) -> Void ) { 274 | lock.withLock { store.removeAll() } 275 | cb(nil) 276 | } 277 | 278 | public func length(cb: ( Error?, Int) -> Void) { 279 | cb(nil, lock.withLock { store.count }) 280 | } 281 | 282 | public func all(cb: ( Error?, [ Session ] ) -> Void) { 283 | let values = Array(lock.withLock { store.values }) 284 | cb(nil, values) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /Sources/connect/TypeIs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeIs.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 30/05/16. 6 | // Copyright © 2016-2023 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import class http.IncomingMessage 10 | import NIOHTTP1 11 | 12 | // TODO: the API is both crap nor really the same like Node 13 | 14 | @inlinable 15 | public func typeIs(_ message: IncomingMessage, _ types: [ String ]) -> String? { 16 | guard let ctype = message.headers["Content-Type"].first else { return nil } 17 | return typeIs(ctype, types) 18 | } 19 | 20 | @inlinable 21 | public func typeIs(_ type: String, _ types: [ String ]) -> String? { 22 | let lcType = type.lowercased() 23 | 24 | for matchType in types { 25 | if does(type: lcType, match: matchType) { 26 | return matchType 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | @usableFromInline 34 | internal func does(type lcType: String, match matchType: String) -> Bool { 35 | let lcMatch = matchType.lowercased() 36 | 37 | if lcType == lcMatch { return true } 38 | 39 | // FIXME: completely naive implementation :-> 40 | 41 | if lcMatch.hasSuffix("*") { 42 | let idx = lcMatch.index(before: lcMatch.endIndex) 43 | return lcType.hasPrefix(lcMatch[lcMatch.startIndex..=5.3) // oh this mess 37 | /** 38 | * Read the .env config file, apply it on the environment, and return the 39 | * parsed values. 40 | * 41 | * Important: Remember to call this as early as possible, otherwise Foundation 42 | * might not pick up the changed environment! (which also affects 43 | * `process.env`) 44 | * 45 | * Values which are already set in the environment are not overridden (unless 46 | * the `override` argument is set). 47 | * 48 | * Syntax: 49 | * - empty lines are skipped 50 | * - lines starting w/ `#` are skipped (comments) 51 | * - key & value are trimmed 52 | * - missing values become the empty string "" 53 | * 54 | * Note: This does none of the quoting stuff of the original yet. 55 | * 56 | * Original JS module: https://github.com/motdotla/dotenv 57 | */ 58 | @discardableResult 59 | static func config(path : String? = nil, 60 | override : Bool = false, 61 | caller : String = #filePath) -> [ String : String ]? 62 | { 63 | do { 64 | return try tryConfig(path: path, override: override, logError: true, 65 | caller: caller) 66 | } 67 | catch { 68 | return nil 69 | } 70 | } 71 | 72 | /// See `config` for details. This is a throwing variant 73 | static func tryConfig(path : String? = nil, 74 | override : Bool = false, 75 | logError : Bool = false, 76 | caller : String = #filePath) 77 | throws -> [ String : String ] 78 | { 79 | let path = path ?? (__dirname(caller: caller) + "/.env") 80 | 81 | do { _ = try fs.accessSync(path, mode: fs.R_OK) } 82 | catch { return [:] } // not an error 83 | 84 | do { 85 | let config = parse(try String(contentsOfFile: path)) 86 | 87 | for ( key, value ) in config { 88 | setenv(key, value, override ? 1 : 0) 89 | } 90 | 91 | return config 92 | } 93 | catch { 94 | if logError { 95 | console.error("dotenv failed to load .env file:", path, 96 | " error:", error) 97 | } 98 | throw error 99 | } 100 | } 101 | #else // Swift <5.3 102 | /** 103 | * Read the .env config file, apply it to the environment, and return the 104 | * parsed values. 105 | * 106 | * Important: Remember to call this as early as possible, otherwise Foundation 107 | * might not pick up the changed environment! (which also affects 108 | * `process.env`) 109 | * 110 | * Values which are already set in the environment are not overridden (unless 111 | * the `override` argument is set). 112 | * 113 | * Syntax: 114 | * - empty lines are skipped 115 | * - lines starting w/ `#` are skipped (comments) 116 | * - key & value are trimmed 117 | * - missing values become the empty string "" 118 | * 119 | * Note: This does none of the quoting stuff of the original yet. 120 | * 121 | * Original JS module: https://github.com/motdotla/dotenv 122 | */ 123 | @discardableResult 124 | static func config(path : String? = nil, 125 | override : Bool = false, 126 | caller : String = #file) -> [ String : String ]? 127 | { 128 | do { 129 | return try tryConfig(path: path, override: override, logError: true, 130 | caller: caller) 131 | } 132 | catch { 133 | return nil 134 | } 135 | } 136 | 137 | /// See `config` for details. This is a throwing variant 138 | static func tryConfig(path : String? = nil, 139 | override : Bool = false, 140 | logError : Bool = false, 141 | caller : String = #file) throws -> [String : String] 142 | { 143 | let path = path ?? (__dirname(caller: caller) + "/.env") 144 | 145 | do { _ = try fs.accessSync(path, mode: R_OK) } 146 | catch { return [:] } // not an error 147 | 148 | do { 149 | let config = parse(try String(contentsOfFile: path)) 150 | 151 | for ( key, value ) in config { 152 | setenv(key, value, override ? 1 : 0) 153 | } 154 | 155 | return config 156 | } 157 | catch { 158 | if logError { 159 | console.error("dotenv failed to load .env file:", path, 160 | " error:", error) 161 | } 162 | throw error 163 | } 164 | } 165 | #endif // <= Swift 5.2 166 | 167 | /** 168 | * Parse the string passed as a .env file. 169 | * 170 | * Syntax: 171 | * - empty lines are skipped 172 | * - lines starting w/ `#` are skipped (comments) 173 | * - key & value are trimmed 174 | * - missing values become the empty string "" 175 | * 176 | * Note: This does none of the quoting stuff of the original yet. 177 | */ 178 | static func parse(_ string: String) -> [ String : String ] { 179 | guard !string.isEmpty else { return [:] } 180 | 181 | var parsed = [ String : String ]() 182 | parsed.reserveCapacity(string.count / 20) // short lines assumed 183 | 184 | for line in string.components(separatedBy: .newlines) { 185 | guard !line.isEmpty else { continue } 186 | guard !line.hasPrefix("#") else { continue } 187 | 188 | let components = line.split(separator: "=", maxSplits: 1) 189 | guard !components.isEmpty else { continue } 190 | 191 | let key = components[0].trimmingCharacters(in: .whitespacesAndNewlines) 192 | let value = components.count < 2 193 | ? "" 194 | : components[1].trimmingCharacters(in: .whitespacesAndNewlines) 195 | 196 | if let existing = parsed[key] { parsed[key] = existing + value } // TBD 197 | else { parsed[key] = value } 198 | } 199 | 200 | return parsed 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/express/BasicAuth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicAuth.swift 3 | // Macro 4 | // 5 | // Created by Helge Heß on 6/3/16. 6 | // Copyright © 2020-2023 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | #if canImport(Foundation) 10 | import Foundation 11 | #endif 12 | import http 13 | import protocol MacroCore.EnvironmentKey 14 | 15 | public enum BasicAuthModule {} 16 | public typealias expressBasicAuth = BasicAuthModule 17 | 18 | public extension BasicAuthModule { 19 | // https://medium.com/javascript-in-plain-english/add-basic-authentication-to-an-express-app-9536f5095e88 20 | 21 | typealias Credentials = http.BasicAuthModule.Credentials 22 | 23 | #if canImport(Foundation) // String.Encoding.utf8, provide alternative 24 | @inlinable 25 | static func auth(_ req: IncomingMessage, encoding: String.Encoding = .utf8) 26 | throws -> Credentials 27 | { 28 | return try http.BasicAuthModule.auth(req, encoding: encoding) 29 | } 30 | #endif // canImport(Foundation) 31 | 32 | typealias SyncAuthorizer = (_ user: String, _ password: String ) -> Bool 33 | typealias AsyncAuthorizer = (_ user: String, _ password: String, 34 | @escaping ( Swift.Error?, Bool ) -> Void) -> Void 35 | 36 | struct Options { 37 | public enum Authorizer { 38 | case none 39 | case sync (SyncAuthorizer) 40 | case async(AsyncAuthorizer) 41 | } 42 | 43 | public var realm : String? 44 | public var users : [ String : String ] 45 | public var authorizer : Authorizer 46 | public var challenge = true 47 | 48 | public var unauthorizedResponse : (( IncomingMessage ) -> String)? 49 | 50 | public init(realm: String? = nil, users: [String : String] = [:], 51 | authorizer: Authorizer = .none, challenge: Bool = true, 52 | unauthorizedResponse: ((IncomingMessage) -> String)? = nil) 53 | { 54 | self.realm = realm 55 | self.users = users 56 | self.authorizer = authorizer 57 | self.challenge = challenge 58 | self.unauthorizedResponse = unauthorizedResponse 59 | } 60 | } 61 | 62 | /** 63 | * Perform HTTP Basic authentication. Only let the request continue if the 64 | * authentication is successful. 65 | * 66 | * Basic usage: 67 | * ``` 68 | * app.use(expressBasicAuth.basicAuth(users: [ 69 | * "admin": "supersecret" 70 | * ])) 71 | * 72 | * app.use { req, res, next in 73 | * console.log("user is authorized:", req.authenticatedBasicAuthUser) 74 | * } 75 | * ``` 76 | * 77 | * Using a custom authenticator: 78 | * ``` 79 | * app.use(expressBasicAuth.basicAuth { login, password in 80 | * return login == "admin" && password == "supersecret" 81 | * }) 82 | * ``` 83 | * 84 | * Asynchronous authentication: 85 | * ``` 86 | * app.use(expressBasicAuth.basicAuth { login, password, yield in 87 | * yield(nil, login == "admin" && password == "supersecret") 88 | * }) 89 | * ``` 90 | * 91 | * - Parameters: 92 | * - options: The configuration for the authentication, see ``Options``. 93 | * - users: An optional dictionary of users/passwords to run the auth 94 | * against (convenience argument, also available via ``Options``). 95 | * - authorizer: An optional, synchronous, authorization function 96 | * (convenience argument, also available via ``Options``). 97 | */ 98 | @inlinable 99 | static func basicAuth(_ options : Options = Options(), 100 | users : [ String: String ]? = nil, 101 | authorizer : SyncAuthorizer? = nil) 102 | -> Middleware 103 | { 104 | var options = options 105 | users.flatMap { $0.forEach { options.users[$0] = $1 } } 106 | if let authorizer = authorizer { options.authorizer = .sync(authorizer) } 107 | return basicAuth(options: options) 108 | } 109 | 110 | /** 111 | * Perform HTTP Basic authentication. Only let the request continue if the 112 | * authentication is successful. 113 | * 114 | * Basic usage: 115 | * 116 | * app.use(expressBasicAuth.basicAuth(users: [ 117 | * "admin": "supersecret" 118 | * ])) 119 | * 120 | * app.use { req, res, next in 121 | * console.log("user is authorized:", req.authenticatedBasicAuthUser) 122 | * } 123 | * 124 | * Using a custom authenticator: 125 | * 126 | * app.use(expressBasicAuth.basicAuth { login, password in 127 | * return login == "admin" && password == "supersecret" 128 | * }) 129 | * 130 | * Asynchronous authentication: 131 | * 132 | * app.use(expressBasicAuth.basicAuth { login, password, yield in 133 | * yield(nil, login == "admin" && password == "supersecret") 134 | * }) 135 | * 136 | */ 137 | @inlinable 138 | static func basicAuth(_ options : Options, 139 | users : [ String: String ]? = nil, 140 | authorizer : @escaping AsyncAuthorizer) 141 | -> Middleware 142 | { 143 | var options = options 144 | users.flatMap { $0.forEach { options.users[$0] = $1 } } 145 | options.authorizer = .async(authorizer) 146 | return basicAuth(options: options) 147 | } 148 | 149 | @inlinable 150 | static func safeCompare(_ lhs: String, _ rhs: String) -> Bool { 151 | return lhs == rhs // right, no difference in Swift? 152 | } 153 | 154 | @usableFromInline 155 | internal static func basicAuth(options: Options) -> Middleware { 156 | let unauthorizedResponse = options.unauthorizedResponse ?? { _ in 157 | if let realm = options.realm { 158 | return "Authentication failed, realm: \(realm)" 159 | } 160 | else { 161 | return "Authentication failed." 162 | } 163 | } 164 | 165 | return { req, res, next in 166 | 167 | func sendAuthenticationFailure() { 168 | res.statusCode = 401 169 | 170 | if options.challenge { // TBD 171 | if let realm = options.realm { // TODO: escaping 172 | res.setHeader("WWW-Authenticate", "Basic realm=\"\(realm)\"") 173 | } 174 | else { 175 | res.setHeader("WWW-Authenticate", "Basic") 176 | } 177 | } 178 | 179 | res.write(unauthorizedResponse(req)) 180 | return res.end() 181 | } 182 | 183 | /* Parse credentials using `http` module */ 184 | let credentials : Credentials 185 | do { 186 | credentials = try auth(req) 187 | } 188 | catch { 189 | req.log.error("Authentication failed w/ error:", error) 190 | return sendAuthenticationFailure() 191 | } 192 | 193 | /* Track (unauthenticated) user in Logger */ 194 | req.log[metadataKey: "basic.user"] = .string(credentials.name) 195 | 196 | 197 | /* Authenticate */ 198 | 199 | if let password = options.users[credentials.name] { 200 | guard safeCompare(credentials.pass, password) else { 201 | return sendAuthenticationFailure() 202 | } 203 | req.environment[BasicAuthUserKey.self] = credentials.name 204 | return next() 205 | } 206 | 207 | switch options.authorizer { 208 | 209 | case .async(let authorizer): 210 | authorizer(credentials.name, credentials.pass) { error, ok in 211 | if let error = error { 212 | req.log.error("Error during authentication of:", credentials.name, 213 | error) 214 | req.emit(error: error) 215 | return res.sendStatus(500) 216 | } 217 | 218 | guard ok else { return sendAuthenticationFailure() } 219 | 220 | req.environment[BasicAuthUserKey.self] = credentials.name 221 | return next() 222 | } 223 | 224 | case .sync(let authorizer): 225 | guard authorizer(credentials.name, credentials.pass) else { 226 | return sendAuthenticationFailure() 227 | } 228 | req.environment[BasicAuthUserKey.self] = credentials.name 229 | return next() 230 | 231 | case .none: 232 | if options.users.isEmpty { 233 | req.log.warn("No authentication setup in basicAuth middleware?") 234 | } 235 | return sendAuthenticationFailure() 236 | } 237 | } 238 | } 239 | } 240 | 241 | // MARK: - IncomingMessage extension 242 | 243 | private enum BasicAuthUserKey: EnvironmentKey { 244 | static let defaultValue = "" 245 | static let loggingKey = "user" 246 | } 247 | 248 | public extension IncomingMessage { 249 | 250 | /** 251 | * Returns the login of a user which got successfully authenticated using 252 | * the `basicAuth` middleware. 253 | */ 254 | var authenticatedBasicAuthUser : String { 255 | get { return self[BasicAuthUserKey.self] } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /Sources/express/ErrorMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Route.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 6/2/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import class http.IncomingMessage 10 | import class http.ServerResponse 11 | import typealias connect.Next 12 | 13 | /** 14 | * Middleware are just functions that deal with HTTP transactions. 15 | * The Express "ErrorMiddleware" enhances the concepts with special middleware 16 | * functions which also take a `Swift.Error` value as the first parameter. 17 | * 18 | * As soon as the middleware stack encounters and error (the regular middleware 19 | * either threw an error, or passed an error object to the `next` handler), 20 | * an Express router will switch to process the stack of error middleware. 21 | * 22 | * They take a request (`IncomingMessage`) and response (`ServerResponse`) 23 | * object as well as a closure to signal whether they fully handled the request 24 | * or whether the respective "Router" (e.g. Connect) should run the next 25 | * middleware. 26 | * 27 | * Call `Next` when the request processing needs to continue, just return if the 28 | * request was fully processed. 29 | */ 30 | public typealias ErrorMiddleware = 31 | ( Swift.Error, 32 | IncomingMessage, ServerResponse, @escaping Next ) 33 | throws -> Void 34 | -------------------------------------------------------------------------------- /Sources/express/ExpressWrappedDictionary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpressWrappedDictionary.swift 3 | // MacroExpress 4 | // 5 | // Created by Helge Heß on 14.07.24. 6 | // Copyright © 2024 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | /** 10 | * This is a wrapper for dictionaries, that behaves like a dictionary, 11 | * but also allows access to the keys using `dynamicMemberLookup`. 12 | * 13 | * For example: 14 | * ```swift 15 | * let service = request.query.service 16 | * ``` 17 | */ 18 | @dynamicMemberLookup 19 | public struct ExpressWrappedDictionary: Collection { 20 | 21 | public typealias WrappedType = [ String : V ] 22 | 23 | public typealias Key = WrappedType.Key 24 | public typealias Value = WrappedType.Value 25 | public typealias Keys = WrappedType.Keys 26 | public typealias Values = WrappedType.Values 27 | 28 | public typealias Element = WrappedType.Element 29 | public typealias Index = WrappedType.Index 30 | public typealias Indices = WrappedType.Indices 31 | public typealias SubSequence = WrappedType.SubSequence 32 | public typealias Iterator = WrappedType.Iterator 33 | 34 | public var dictionary : WrappedType 35 | 36 | @inlinable 37 | public init(_ dictionary: WrappedType) { self.dictionary = dictionary } 38 | } 39 | 40 | extension ExpressWrappedDictionary: Equatable where V: Equatable {} 41 | extension ExpressWrappedDictionary: Hashable where V: Hashable {} 42 | 43 | public extension ExpressWrappedDictionary { 44 | 45 | // MARK: - Dictionary 46 | 47 | @inlinable var keys : Keys { return dictionary.keys } 48 | @inlinable var values : Values { return dictionary.values } 49 | 50 | @inlinable subscript(_ key: Key) -> Value? { 51 | set { dictionary[key] = newValue } 52 | get { return dictionary[key] } 53 | } 54 | @inlinable subscript(key: Key, default defaultValue: @autoclosure () -> Value) 55 | -> Value 56 | { 57 | set { dictionary[key] = newValue } // TBD 58 | get { return self[key] ?? defaultValue() } 59 | } 60 | 61 | // MARK: - Sequence 62 | 63 | @inlinable 64 | func makeIterator() -> Iterator { return dictionary.makeIterator() } 65 | 66 | // MARK: - Collection 67 | 68 | @inlinable var indices : Indices { return dictionary.indices } 69 | @inlinable var startIndex : Index { return dictionary.startIndex } 70 | @inlinable var endIndex : Index { return dictionary.endIndex } 71 | @inlinable func index(after i: Index) -> Index { 72 | return dictionary.index(after: i) 73 | } 74 | @inlinable 75 | func formIndex(after i: inout Index) { dictionary.formIndex(after: &i) } 76 | 77 | @inlinable 78 | subscript(position: Index) -> Element { return dictionary[position] } 79 | 80 | @inlinable 81 | subscript(bounds: Range) -> Slice { 82 | return dictionary[bounds] 83 | } 84 | @inlinable func index(forKey key: Key) -> Index? { 85 | return dictionary.index(forKey: key) 86 | } 87 | 88 | @inlinable var isEmpty : Bool { return dictionary.isEmpty } 89 | @inlinable var first : (key: Key, value: Value)? { return dictionary.first } 90 | 91 | @inlinable 92 | var underestimatedCount: Int { return dictionary.underestimatedCount } 93 | 94 | @inlinable var count: Int { return dictionary.count } 95 | 96 | // MARK: - Dynamic Member Lookup 97 | 98 | @inlinable 99 | subscript(dynamicMember k: String) -> Value? { return dictionary[k] } 100 | } 101 | 102 | public extension ExpressWrappedDictionary { 103 | 104 | @inlinable 105 | subscript(int key: Key) -> Int? { 106 | guard let v = self[key] else { return nil } 107 | if let i = (v as? Int) { return i } 108 | #if swift(>=5.10) 109 | if let i = (v as? any BinaryInteger) { return Int(i) } 110 | #endif 111 | return Int("\(v)") 112 | } 113 | 114 | @inlinable 115 | subscript(string key: Key) -> String? { 116 | guard let v = self[key] else { return nil } 117 | if let s = (v as? String) { return s } 118 | return String(describing: v) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/express/IncomingMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IncomingMessage.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 6/2/16. 6 | // Copyright © 2016-2024 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | #if canImport(Foundation) 10 | import Foundation 11 | #endif 12 | import class http.IncomingMessage 13 | import NIOHTTP1 14 | 15 | 16 | public extension IncomingMessage { 17 | 18 | typealias Params = ExpressWrappedDictionary 19 | typealias Query = ExpressWrappedDictionary 20 | 21 | // TODO: baseUrl, originalUrl, path 22 | // TODO: hostname, ip, ips, protocol 23 | 24 | // TODO: originalUrl, path 25 | // TODO: hostname, ip, ips, protocol 26 | 27 | /// A reference to the active application. Updated when subapps are triggered. 28 | var app : Express? { return environment[ExpressExtKey.App.self] } 29 | 30 | /** 31 | * Contains the request parameters. 32 | * 33 | * Example: 34 | * ``` 35 | * app.use("/users/:id/view") { req, res, next in 36 | * guard let id = req.params[int: "id"] 37 | * else { return try res.sendStatus(400) } 38 | * } 39 | * ``` 40 | */ 41 | var params : Params { 42 | set { environment[ExpressExtKey.Params.self] = newValue } 43 | get { return environment[ExpressExtKey.Params.self] } 44 | } 45 | 46 | /** 47 | * Returns the query parameters as parsed by the `qs.parse` function. 48 | */ 49 | var query : Query { 50 | if let q = environment[ExpressExtKey.Query.self] { return q } 51 | 52 | // this should be filled by Express when the request arrives. It depends on 53 | // the 'query parser' setting: 54 | // - false => disable 55 | // - simple => querystring.parse 56 | // - extended => qs.parse 57 | // - custom - custom parser function 58 | 59 | // TODO: shoe[color]=blue gives shoe.color = blue 60 | // FIXME: cannot use url.parse due to overload 61 | // FIXME: improve parser (fragments?!) 62 | // TBD: just use Foundation?! 63 | guard let idx = url.firstIndex(of: "?") else { 64 | environment[ExpressExtKey.Query.self] = .init([:]) 65 | return Query([:]) 66 | } 67 | let q = url[url.index(after: idx)...] 68 | let qp = qs.parse(String(q)) 69 | environment[ExpressExtKey.Query.self] = .init(qp) 70 | return Query(qp) 71 | } 72 | 73 | /** 74 | * Contains the part of the URL which matched the current route. Example: 75 | * ``` 76 | * app.get("/admin/index") { ... } 77 | * ``` 78 | * 79 | * when this is invoked with "/admin/index/hello/world", the baseURL will 80 | * be "/admin/index". 81 | */ 82 | var baseURL : String? { 83 | set { environment[ExpressExtKey.BaseURL.self] = newValue } 84 | get { return environment[ExpressExtKey.BaseURL.self] } 85 | } 86 | 87 | /// The active route. 88 | var route : Route? { 89 | set { environment[ExpressExtKey.RouteKey.self] = newValue } 90 | get { return environment[ExpressExtKey.RouteKey.self] } 91 | } 92 | 93 | 94 | /** 95 | * Checks whether the Accept header of the client indicates that the client 96 | * can deal with the given type, and returns the Accept pattern which matched 97 | * the type. 98 | * 99 | * Example: 100 | * ``` 101 | * app.get("/index") { req, res, next in 102 | * if req.accepts("json") != nil { 103 | * try res.json(todos.getAll()) 104 | * } 105 | * else { try res.send("Hello World!") } 106 | * } 107 | * ``` 108 | * 109 | * - Parameters: 110 | * - contentType: The content-type look for in the `Accept` header 111 | * - Returns: The value of the matching content-type part. 112 | */ 113 | @inlinable 114 | func accepts(_ contentType: String) -> String? { 115 | // TODO: allow array values 116 | for acceptHeader in headers["Accept"] { 117 | // FIXME: naive and incorrect implementation :-) 118 | // TODO: parse quality, patterns, etc etc 119 | let acceptedTypes = acceptHeader.split(separator: ",") 120 | for mimeType in acceptedTypes { 121 | #if canImport(Foundation) 122 | if mimeType.contains(contentType) { return String(mimeType) } 123 | #else // prefix match if Foundation is missing 124 | let length = contentType.count 125 | if mimeType.count >= length && mimeType.prefix(length) == s { 126 | return String(mimeType) 127 | } 128 | #endif 129 | } 130 | } 131 | return nil 132 | } 133 | 134 | /** 135 | * Check whether the Content-Type of the request matches the given `pattern`. 136 | * 137 | * Refer to the connect `typeIs` function for the actual matching 138 | * implementation being used. 139 | * 140 | * Example: 141 | * ``` 142 | * app.use { req, res, next in 143 | * guard req.is("application/json") else { return next() } 144 | * // deal with JSON 145 | * } 146 | * ``` 147 | * 148 | * - Parameters: 149 | * - pattern: The type to check for, does a prefix or contains match on the 150 | * `Content-Type` header. Lowercased first. 151 | * - Returns: `true` if the header matched. 152 | */ 153 | @inlinable 154 | func `is`(_ pattern: String) -> Bool { 155 | return typeIs(self, [ pattern.lowercased() ]) != nil 156 | } 157 | 158 | /** 159 | * Returns true if the request originated from an `XMLHttpRequest` (aka 160 | * browser AJAX). 161 | * 162 | * This is checking whether the `X-Requested-With` header exists, 163 | * and whether that contains the `XMLHttpRequest` string. 164 | */ 165 | @inlinable 166 | var xhr : Bool { 167 | guard let h = headers["X-Requested-With"].first else { return false } 168 | return h.contains("XMLHttpRequest") 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Sources/express/JSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 6/3/16. 6 | // Copyright © 2016-2023 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import MacroCore 10 | import class http.ServerResponse 11 | 12 | public extension ServerResponse { 13 | // TODO: add jsonp 14 | // TODO: be a proper stream 15 | // TODO: Maybe we don't want to convert to a `JSON`, but rather stream real 16 | // object. 17 | 18 | #if canImport(Foundation) 19 | /** 20 | * Serializes the given object using Foundation's `JSONSerialization`, 21 | * writes it to the response, and ends the response. 22 | * 23 | * If the content-type can still be assigned (no data has been written yet), 24 | * the `Content-Type` header to `application/json; charset=utf-8` is set. 25 | * 26 | * Example: 27 | * ``` 28 | * app.get { req, res, next in 29 | * res.json([ "answer": 42 ] as Any) 30 | * } 31 | * ``` 32 | * 33 | * - Parameters: 34 | * - object: The object to JSON encode and send to the client. 35 | */ 36 | func json(_ object: Any?) { 37 | if canAssignContentType { 38 | setHeader("Content-Type", "application/json; charset=utf-8") 39 | } 40 | _ = writeJSON(object) 41 | end() 42 | } 43 | #endif // canImport(Foundation) 44 | 45 | 46 | /** 47 | * Serializes the given value using its `Encodable` implementation to JSON, 48 | * writes it to the response, and ends the response. 49 | * 50 | * If the content-type can still be assigned (no data has been written yet), 51 | * the `Content-Type` header to `application/json; charset=utf-8` is set. 52 | * 53 | * Example: 54 | * ``` 55 | * app.get { req, res, next in 56 | * res.json([ "answer": 42 ]) 57 | * } 58 | * ``` 59 | * 60 | * - Parameters: 61 | * - object: The value to JSON encode and send to the client. 62 | */ 63 | func json(_ object: E) { 64 | if canAssignContentType { 65 | setHeader("Content-Type", "application/json; charset=utf-8") 66 | } 67 | _ = write(object) 68 | end() 69 | } 70 | 71 | /** 72 | * Serializes the given value using its `Encodable` implementation to JSON, 73 | * writes it to the response, and ends the response. 74 | * If the object is `nil`, and empty string is sent (and the response ends). 75 | * 76 | * If the content-type can still be assigned (no data has been written yet), 77 | * the `Content-Type` header to `application/json; charset=utf-8` is set. 78 | * 79 | * Example: 80 | * ``` 81 | * app.get { req, res, next in 82 | * res.json(nil) 83 | * } 84 | * ``` 85 | * 86 | * - Parameters: 87 | * - object: The value to JSON encode and send to the client. 88 | */ 89 | func json(_ object: E?) { 90 | guard let object = object else { 91 | _ = write("") 92 | return end() 93 | } 94 | return json(object) 95 | } 96 | 97 | /** 98 | * Serializes the given String array to JSON, 99 | * writes it to the response, and ends the response. 100 | * 101 | * If the content-type can still be assigned (no data has been written yet), 102 | * the `Content-Type` header to `application/json; charset=utf-8` is set. 103 | * 104 | * Example: 105 | * ``` 106 | * app.get { req, res, next in 107 | * res.json([]]) 108 | * } 109 | * ``` 110 | * 111 | * - Parameters: 112 | * - object: The string array to JSON encode and send to the client. 113 | */ 114 | func json(_ stringArray: [ String ]) { 115 | // Note: This is a workaround to make `json([])` work w/o warnings. 116 | _ = write(stringArray) 117 | end() 118 | } 119 | 120 | /** 121 | * Serializes the given String/String dictionary to JSON, 122 | * writes it to the response, and ends the response. 123 | * 124 | * If the content-type can still be assigned (no data has been written yet), 125 | * the `Content-Type` header to `application/json; charset=utf-8` is set. 126 | * 127 | * Example: 128 | * ``` 129 | * app.get { req, res, next in 130 | * res.json([:]]) 131 | * } 132 | * ``` 133 | * 134 | * - Parameters: 135 | * - object: The string array to JSON encode and send to the client. 136 | */ 137 | func json(_ stringDictionary: [ String : String ]) { 138 | // Note: This is a workaround to make `json([:])` work w/o warnings. 139 | _ = write(stringDictionary) 140 | end() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/express/MiddlewareObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiddlewareObject.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 02/06/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import let MacroCore.console 10 | 11 | /** 12 | * MiddlewareObject is the 'object variant' of a Middleware callback. 13 | * 14 | * All MiddlewareObject's provide a `handle(request:response:next:)` method. 15 | * 16 | * And you can generate a Middleware function for a MiddlewareObject by using 17 | * the `.middleware` property. Like so: 18 | * 19 | * app.use(otherApp.middleware) 20 | * 21 | * Finally, you can also use the as a http-module request handler. Same thing: 22 | * 23 | * http.createServer(onRequest: app.requestHandler) 24 | * 25 | */ 26 | public protocol MiddlewareObject { 27 | 28 | func handle(request : IncomingMessage, 29 | response : ServerResponse, 30 | next : @escaping Next) throws 31 | } 32 | 33 | /** 34 | * ErrorMiddlewareObject is the 'object variant' of an ErrorMiddleware callback. 35 | * 36 | * All ErrorMiddlewareObject's provide a 37 | * `handle(error:request:response:next:)` method. 38 | * 39 | * And you can generate a ErrorMiddleware function for a ErrorMiddlewareObject 40 | * by using the `.errorMiddleware` property. Like so: 41 | * 42 | * app.use(otherApp.errorMiddleware) 43 | * 44 | */ 45 | public protocol ErrorMiddlewareObject { 46 | 47 | func handle(error : Swift.Error, 48 | request : IncomingMessage, 49 | response : ServerResponse, 50 | next : @escaping Next) throws 51 | } 52 | 53 | public protocol MountableMiddlewareObject : MiddlewareObject { 54 | 55 | func mount(at: String, parent: Express) 56 | } 57 | 58 | 59 | // MARK: - Default Implementations 60 | 61 | public extension MiddlewareObject { 62 | 63 | /** 64 | * Returns a `Middleware` closure which targets this `MiddlewareObject`. 65 | */ 66 | @inlinable 67 | var middleware: Middleware { 68 | return { req, res, next in 69 | try self.handle(request: req, response: res, next: next) 70 | } 71 | } 72 | 73 | /** 74 | * Returns a request handler closure which targets this `MiddlewareObject`. 75 | */ 76 | var requestHandler: ( IncomingMessage, ServerResponse ) -> Void { 77 | return { req, res in 78 | do { 79 | try self.handle(request: req, response: res) { ( args: Any... ) in 80 | if let error = args.first as? Error { 81 | console.error("No middleware catched the error:", 82 | "\(self) \(req.method) \(req.url):", 83 | error) 84 | res.writeHead(500) 85 | res.end() 86 | } 87 | else if req.method == "OPTIONS" { 88 | // This assumes option headers have been set via cors middleware or 89 | // sth similar. 90 | // Just respond with OK and we are done, right? 91 | 92 | if res.getHeader("Allow") == nil { 93 | res.setHeader("Allow", 94 | allowedDefaultMethods.joined(separator: ", ")) 95 | } 96 | if res.getHeader("Server") == nil { 97 | res.setHeader("Server", "Macro/1.33.7") 98 | } 99 | 100 | res.writeHead(200) 101 | res.end() 102 | } 103 | else { 104 | // essentially the final handler 105 | console.warn("No middleware called end:", 106 | "\(self) \(req.method) \(req.url)") 107 | res.writeHead(404) 108 | res.end() 109 | } 110 | } 111 | } 112 | catch { 113 | console.error("No middleware catched the error:", 114 | "\(self) \(req.method) \(req.url):", 115 | error) 116 | res.writeHead(500) 117 | res.end() 118 | } 119 | } 120 | } 121 | } 122 | 123 | public extension ErrorMiddlewareObject { 124 | 125 | /** 126 | * Returns an `ErrorMiddleware` closure which targets this 127 | * `ErrorMiddlewareObject`. 128 | */ 129 | @inlinable 130 | var errorMiddleware: ErrorMiddleware { 131 | return { error, req, res, next in 132 | try self.handle(error: error, request: req, response: res, next: next) 133 | } 134 | } 135 | } 136 | 137 | fileprivate let allowedDefaultMethods = [ 138 | "GET", "HEAD", "POST", "OPTIONS", "DELETE", "PUT", "PATCH" 139 | ] 140 | -------------------------------------------------------------------------------- /Sources/express/Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Module.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 4/3/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import MacroCore 10 | @_exported import connect 11 | @_exported import class http.IncomingMessage 12 | @_exported import class http.ServerResponse 13 | 14 | public enum ExpressModule {} 15 | 16 | public extension ExpressModule { 17 | 18 | @inlinable 19 | static func express(invokingSourceFilePath: StaticString = #file, 20 | middleware: Middleware...) -> Express 21 | { 22 | let app = Express(invokingSourceFilePath: invokingSourceFilePath) 23 | middleware.forEach { app.use($0) } 24 | return app 25 | } 26 | } 27 | 28 | @inlinable 29 | public func express(invokingSourceFilePath: StaticString = #file, 30 | middleware: Middleware...) -> Express 31 | { 32 | let app = Express(invokingSourceFilePath: invokingSourceFilePath) 33 | middleware.forEach { app.use($0) } 34 | return app 35 | } 36 | -------------------------------------------------------------------------------- /Sources/express/Mustache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mustache.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 02/06/16. 6 | // Copyright © 2016-2024 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import func fs.readFile 10 | import func fs.readFileSync 11 | import enum fs.path 12 | import let MacroCore.console 13 | import Mustache 14 | 15 | let mustacheExpress : ExpressEngine = { path, options, done in 16 | fs.readFile(path, "utf8") { err, str in 17 | guard err == nil else { 18 | done(err!) 19 | return 20 | } 21 | 22 | guard let template = str else { 23 | console.error("read file return no error but no string either: \(path)") 24 | done("Got no string?") 25 | return 26 | } 27 | 28 | var parser = MustacheParser() 29 | let tree = parser.parse(string: template) 30 | 31 | let ctx = ExpressMustacheContext(path: path, object: options) 32 | tree.render(inContext: ctx) { result in 33 | done(nil, result) 34 | } 35 | } 36 | } 37 | 38 | class ExpressMustacheContext : MustacheDefaultRenderingContext { 39 | 40 | let viewPath : String 41 | 42 | init(path p: String, object root: Any?) { 43 | self.viewPath = path.dirname(p) 44 | super.init(root) 45 | } 46 | 47 | override func retrievePartial(name n: String) -> MustacheNode? { 48 | let ext = ".mustache" 49 | let partialPath = viewPath + "/" + (n.hasSuffix(ext) ? n : (n + ext)) 50 | 51 | guard let template = fs.readFileSync(partialPath, "utf8") else { 52 | console.error("could not load partial: \(n): \(partialPath)") 53 | return nil 54 | } 55 | 56 | var parser = MustacheParser() 57 | let tree = parser.parse(string: template) 58 | return tree 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/express/README.md: -------------------------------------------------------------------------------- 1 | # Macro Express 2 | 3 | Express is a Macro module modelled after the 4 | [ExpressJS](http://expressjs.com) 5 | framework. 6 | As usual in Macro the module is supposed to be as similar as possible. 7 | 8 | Express is a module based on top of 9 | [Connect](../connect). 10 | It is also based on middleware, but adds 11 | additional routing features, 12 | support for templates (defaults to Mustache), 13 | and more fancy conveniences for JSON, etc ;-) 14 | 15 | Show us some code! Example: 16 | 17 | ```swift 18 | import express 19 | 20 | let __dirname = process.cwd() // our modules have no __dirname 21 | 22 | let app = express() 23 | 24 | // Hook up middleware: 25 | // - logging 26 | // - POST form value decoder 27 | // - cookie parsing (looks for cookies, adds them to the request) 28 | // - session handling 29 | // - serve files in 'public' directory 30 | app.use(logger("dev")) 31 | app.use(bodyParser.urlencoded()) 32 | app.use(cookieParser()) 33 | app.use(session()) 34 | app.use(serveStatic(__dirname + "/public")) 35 | 36 | // Setup dynamic templates: Mustache templates ending in .html in the 37 | // 'views' directory. 38 | app.set("view engine", "html") // really mustache, but we want to use .html 39 | app.set("views", __dirname + "/views") 40 | 41 | // Simple session based counter middleware: 42 | app.use { req, _, next in 43 | req.session["viewCount"] = req.session[int: "viewCount"] + 1 44 | next() // just counting, no HTTP processing 45 | } 46 | 47 | // Basic form handling: 48 | // - If the browser sends 'GET /form', render the 49 | // Mustache template in views/form.html. 50 | // - If the browser sends 'POST /form', grab the values and render the 51 | // form with them 52 | app.get("/form") { _, res in 53 | res.render("form") 54 | } 55 | app.post("/form") { req, res in 56 | let user = req.body[string: "user"] // form value 'user' 57 | 58 | let options : [ String : Any ] = [ 59 | "user" : user, 60 | "nouser" : user.isEmpty 61 | ] 62 | res.render("form", options) 63 | } 64 | 65 | app.listen(1337) { print("Server listening: \($0)") } 66 | ``` 67 | 68 | A sample Mustache template: 69 | 70 | ```mustache 71 | {{> header}} 72 |
73 | 77 | 80 | 81 | 82 |
83 | {{> footer}} 84 | ``` 85 | 86 | This includes header/footer templates via `{{> header}}` and it renders values 87 | using `{{user}}`. You get the idea. 88 | -------------------------------------------------------------------------------- /Sources/express/Render.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Render.swift 3 | // Noze.io / Macro / ExExpress 4 | // 5 | // Created by Helge Heß on 6/2/16. 6 | // Copyright © 2016-2023 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import enum MacroCore.process 10 | import class http.ServerResponse 11 | import let MacroCore.console 12 | import fs 13 | 14 | public enum ExpressRenderingError: Swift.Error { 15 | case responseHasNoAppObject 16 | case unsupportedViewEngine(String) 17 | case didNotFindTemplate(String) 18 | case templateError(Swift.Error?) 19 | } 20 | 21 | public extension ServerResponse { 22 | 23 | // TODO: How do we get access to the application?? Need to attach to the 24 | // request? We need to retrieve values. 25 | 26 | /** 27 | * Lookup a template with the given name, locate the rendering engine for it, 28 | * and render it with the options that are passed in. 29 | * 30 | * Example: 31 | * 32 | * app.get { _, res in 33 | * res.render('index', { "title": "Hello World!" }) 34 | * } 35 | * 36 | * Assuming your 'views' directory contains an `index.mustache` file, this 37 | * would trigger the Mustache engine to render the template with the given 38 | * dictionary as input. 39 | * 40 | * When no options are passed in, render will fallback to the `view options` 41 | * setting in the application (TODO: merge the two contexts). 42 | */ 43 | func render(_ template: String, _ options : Any? = nil) { 44 | guard let app = self.app else { 45 | console.error("No app object assigned to response: \(self)") 46 | emit(error: ExpressRenderingError.responseHasNoAppObject) 47 | finishRender500IfNecessary() 48 | return 49 | } 50 | 51 | app.render(template: template, options: options, to: self) 52 | } 53 | 54 | fileprivate func finishRender500IfNecessary() { 55 | guard !writableEnded else { return } 56 | writeHead(500) 57 | end() 58 | } 59 | } 60 | 61 | public extension Express { 62 | 63 | /** 64 | * Lookup a template with the given name, locate the rendering engine for it, 65 | * and render it with the options that are passed in. 66 | * 67 | * Refer to the `ServerResponse.render` method for details. 68 | */ 69 | func render(template: String, options: Any?, to res: ServerResponse) { 70 | let viewEngine = (get("view engine") as? String) ?? "mustache" 71 | guard let engine = engines[viewEngine] else { 72 | console.error("Did not find view engine: \(viewEngine)") 73 | res.emit(error: ExpressRenderingError.unsupportedViewEngine(viewEngine)) 74 | res.finishRender500IfNecessary() 75 | return 76 | } 77 | 78 | let viewsPath = viewDirectory(for: viewEngine, response: res) 79 | let emptyOpts : [ String : Any ] = [:] 80 | let appViewOptions = get("view options") ?? emptyOpts 81 | let viewOptions = options ?? appViewOptions // TODO: merge if possible 82 | 83 | lookupTemplatePath(template, in: viewsPath, preferredEngine: viewEngine) { 84 | pathOrNot in 85 | guard let path = pathOrNot else { 86 | res.emit(error: ExpressRenderingError.didNotFindTemplate(template)) 87 | res.finishRender500IfNecessary() 88 | return 89 | } 90 | 91 | engine(path, viewOptions) { ( results: Any?... ) in 92 | let rc = results.count 93 | let v0 = rc > 0 ? results[0] : nil 94 | let v1 = rc > 1 ? results[1] : nil 95 | 96 | if let error = v0 { 97 | res.emit(error: ExpressRenderingError 98 | .templateError(error as? Swift.Error)) 99 | console.error("template error:", error) 100 | res.writeHead(500) 101 | res.end() 102 | return 103 | } 104 | 105 | guard let result = v1 else { // Hm? 106 | console.warn("template returned no content: \(template) \(results)") 107 | res.writeHead(204) 108 | res.end() 109 | return 110 | } 111 | 112 | // TBD: maybe support a stream as a result? (result.pipe(res)) 113 | // Or generators, there are many more options. 114 | if !(result is String) { 115 | console.warn("template rendering result is not a String:", result) 116 | } 117 | 118 | let s = (result as? String) ?? "\(result)" 119 | 120 | // Wow, this is harder than it looks when we want to consider a MIMEType 121 | // object as a value :-) 122 | var setContentType = true 123 | if let oldType = res.getHeader("Content-Type") { 124 | let s = (oldType as? String) ?? String(describing: oldType) // FIXME 125 | setContentType = (s == "httpd/unix-directory") // a hack for Apache 126 | } 127 | 128 | if setContentType { 129 | // FIXME: also consider extension of template (.html, .vcf etc) 130 | res.setHeader("Content-Type", detectTypeForContent(string: s)) 131 | } 132 | 133 | res.writeHead(200) 134 | res.write(s) 135 | res.end() 136 | } 137 | } 138 | } 139 | 140 | // ExExpress variant - TODO: make it async 141 | func lookupTemplatePath(_ template: String, in dir: String, 142 | preferredEngine: String? = nil, 143 | yield: @escaping ( String? ) -> Void) 144 | { 145 | // Hm, Swift only has pathComponents on URL? 146 | // FIXME 147 | 148 | let pathesToCheck : [ String ] = { () -> [ String ] in 149 | if let ext = preferredEngine { return [ ext ] + engines.keys } 150 | else { return Array(engines.keys) } 151 | }() 152 | .map { "\(dir)/\(template).\($0)" } 153 | 154 | guard !pathesToCheck.isEmpty else { return yield(nil) } 155 | 156 | final class State { 157 | var pending : ArraySlice 158 | let yield : ( String? ) -> Void 159 | 160 | init(_ pathesToCheck: [ String ], yield: @escaping ( String? ) -> Void) { 161 | pending = pathesToCheck[...] 162 | self.yield = yield 163 | } 164 | 165 | func step() { 166 | guard let pathToCheck = pending.popFirst() else { return yield(nil) } 167 | fs.stat(pathToCheck) { error, stat in 168 | guard let stat = stat, stat.isFile() else { 169 | return self.step() 170 | } 171 | self.yield(pathToCheck) 172 | } 173 | } 174 | } 175 | 176 | let state = State(pathesToCheck, yield: yield) 177 | state.step() 178 | } 179 | } 180 | 181 | 182 | import enum mime.mime 183 | 184 | fileprivate 185 | func detectTypeForContent(string: String, 186 | default: String = "text/html; charset=utf-8") 187 | -> String 188 | { 189 | // TODO: more clever detection? ;-) 190 | for ( prefix, type ) in mime.typePrefixMap { 191 | if string.hasPrefix(prefix) { return type } 192 | } 193 | return `default` 194 | } 195 | 196 | 197 | // MARK: - X Compile Support - Macro/fs/Utils/StatStruct 198 | // Dupe to support: 199 | // https://github.com/SPMDestinations/homebrew-tap/issues/2 200 | 201 | #if !os(Windows) 202 | import struct xsys.stat_struct 203 | #if os(Linux) 204 | import let Glibc.S_IFMT 205 | import let Glibc.S_IFREG 206 | #else 207 | import let Darwin.S_IFMT 208 | import let Darwin.S_IFREG 209 | #endif 210 | 211 | fileprivate extension xsys.stat_struct { 212 | func isFile() -> Bool { return (st_mode & S_IFMT) == S_IFREG } 213 | } 214 | #endif // !os(Windows) 215 | -------------------------------------------------------------------------------- /Sources/express/RouteKeeper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteKeeper.swift 3 | // Noze.io / ExExpress / Macro 4 | // 5 | // Created by Helge Heß on 6/2/16. 6 | // Copyright © 2016-2023 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import typealias connect.Middleware 10 | import NIOHTTP1 11 | 12 | /** 13 | * An object which keeps routes. 14 | * 15 | * The `Express` object itself is a route keeper, and so are the `Router` 16 | * object, and even a `Route` itself. 17 | * 18 | * The primary purpose of this protocol is to decouple all the convenience 19 | * `use`, `get` etc functions from the actual functionality: `add(route:)`. 20 | */ 21 | public protocol RouteKeeper: AnyObject { 22 | 23 | /** 24 | * Add the specified ``Route`` to the ``RouteKeeper``. 25 | * 26 | * A ``RouteKeeper`` is an object owning routes, even ``Route`` itself is 27 | * a ``RouteKeeper`` (and can contain "subroutes"). 28 | * 29 | * This is the designated way to add routes to a keep, methods like 30 | * ``get(id:_:)`` or ``put(id:_:)`` all call into this primary method. 31 | * 32 | * - Parameters: 33 | * - route: The ``Route`` to add. 34 | */ 35 | func add(route: Route) 36 | } 37 | 38 | 39 | // MARK: - Route Method 40 | 41 | public extension RouteKeeper { 42 | 43 | /** 44 | * Returns a route to gate on a path. Since a ``Route`` itself is a 45 | * ``RouteKeeper``, additional routes can be added. 46 | * 47 | * Attached routes are mounted, i.e. their path is relative to the parent 48 | * route. 49 | * 50 | * Examples: 51 | * ``` 52 | * app.route("/cows") 53 | * .get { req, res, next ... } 54 | * .post { req, res, next ... } 55 | * 56 | * app.route("/admin") 57 | * .get("/view") { .. } // does match `/admin/view`, not `/view` 58 | * ``` 59 | * 60 | * One can also mount using a separate ``Express`` instance. 61 | */ 62 | @inlinable 63 | func route(id: String? = nil, _ path: String) -> Route { 64 | let route = Route(id: id, pattern: path) 65 | add(route: route) 66 | return route 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/express/RouteMounts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMounts.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 2023-04-16. 6 | // Copyright © 2016-2023 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import NIOHTTP1 10 | 11 | @usableFromInline 12 | internal func mountIfPossible(pattern : String, 13 | parent : RouteKeeper, 14 | children : [ MiddlewareObject ]) 15 | { 16 | guard let parent = parent as? Express else { return } 17 | 18 | for child in children { 19 | guard let child = child as? MountableMiddlewareObject else { continue } 20 | child.mount(at: pattern, parent: parent) 21 | } 22 | } 23 | 24 | public extension RouteKeeper { 25 | // Directly attach MiddlewareObject's as Middleware. That is: 26 | // let app = express() 27 | // let admin = express() 28 | // app.use("/admin", admin) 29 | // TBD: should we have a Route which keeps the object? Has various advantages, 30 | // particularily during debugging. 31 | 32 | @discardableResult 33 | @inlinable 34 | func use(id: String? = nil, _ mw: MiddlewareObject...) -> Self { 35 | add(route: Route(id: id, pattern: nil, method: nil, middleware: mw)) 36 | return self 37 | } 38 | 39 | @discardableResult 40 | @inlinable 41 | func use(id: String? = nil, _ pathPattern: String, _ mw: MiddlewareObject...) 42 | -> Self 43 | { 44 | mountIfPossible(pattern: pathPattern, parent: self, children: mw) 45 | add(route: Route(id: id, pattern: pathPattern, method: nil, middleware: mw)) 46 | return self 47 | } 48 | 49 | @discardableResult 50 | @inlinable 51 | func all(id: String? = nil, _ pathPattern: String, _ mw: MiddlewareObject...) 52 | -> Self 53 | { 54 | mountIfPossible(pattern: pathPattern, parent: self, children: mw) 55 | add(route: Route(id: id, pattern: pathPattern, method: nil, middleware: mw)) 56 | return self 57 | } 58 | 59 | @discardableResult 60 | @inlinable 61 | func get(id: String? = nil, _ pathPattern: String, _ mw: MiddlewareObject...) 62 | -> Self 63 | { 64 | mountIfPossible(pattern: pathPattern, parent: self, children: mw) 65 | add(route: Route(id: id, pattern: pathPattern, method: .GET, 66 | middleware: mw)) 67 | return self 68 | } 69 | 70 | @discardableResult 71 | @inlinable 72 | func post(id: String? = nil, _ pathPattern: String, _ mw: MiddlewareObject...) 73 | -> Self 74 | { 75 | mountIfPossible(pattern: pathPattern, parent: self, children: mw) 76 | add(route: Route(id: id, pattern: pathPattern, method: .POST, 77 | middleware: mw)) 78 | return self 79 | } 80 | 81 | @discardableResult 82 | @inlinable 83 | func head(id: String? = nil, _ pathPattern: String, _ mw: MiddlewareObject...) 84 | -> Self 85 | { 86 | mountIfPossible(pattern: pathPattern, parent: self, children: mw) 87 | add(route: Route(id: id, pattern: pathPattern, method: .HEAD, 88 | middleware: mw)) 89 | return self 90 | } 91 | 92 | @discardableResult 93 | @inlinable 94 | func put(id: String? = nil, _ pathPattern: String, _ mw: MiddlewareObject...) 95 | -> Self 96 | { 97 | mountIfPossible(pattern: pathPattern, parent: self, children: mw) 98 | add(route: Route(id: id, pattern: pathPattern, method: .PUT, 99 | middleware: mw)) 100 | return self 101 | } 102 | 103 | @discardableResult 104 | @inlinable 105 | func del(id: String? = nil, _ pathPattern: String, _ mw: MiddlewareObject...) 106 | -> Self 107 | { 108 | mountIfPossible(pattern: pathPattern, parent: self, children: mw) 109 | add(route: Route(id: id, pattern: pathPattern, method: .DELETE, 110 | middleware: mw)) 111 | return self 112 | } 113 | 114 | @discardableResult 115 | @inlinable 116 | func patch(id: String? = nil, _ pathPattern: String, 117 | _ mw: MiddlewareObject...) -> Self 118 | { 119 | mountIfPossible(pattern: pathPattern, parent: self, children: mw) 120 | add(route: Route(id: id, pattern: pathPattern, method: .PATCH, 121 | middleware: mw)) 122 | return self 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/express/RoutePattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoutePattern.swift 3 | // Noze.io / Macro / ExExpress 4 | // 5 | // Created by Helge Heß on 6/2/16. 6 | // Copyright © 2016-2021 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import enum MacroCore.process 10 | 11 | private let debugMatcher = process.getenvflag("macro.router.matcher.debug") 12 | 13 | public enum RoutePattern: Hashable { 14 | 15 | case root 16 | case text (String) 17 | case variable(String) 18 | case wildcard 19 | case prefix (String) 20 | case suffix (String) 21 | case contains(String) 22 | case eol 23 | 24 | func match(string s: String) -> Bool { 25 | switch self { 26 | case .root: return s == "" 27 | case .text(let v): return s == v 28 | case .wildcard: return true 29 | case .variable: return true // allow anything, like .Wildcard 30 | case .prefix(let v): return s.hasPrefix(v) 31 | case .suffix(let v): return s.hasSuffix(v) 32 | case .contains(let v): return s.contains(v) 33 | case .eol: return false // nothing should come anymore 34 | } 35 | } 36 | 37 | /** 38 | * Creates a pattern for a given 'url' string. 39 | * 40 | * - the "*" string is considered a match-all. 41 | * - otherwise the string is split into path components (on '/') 42 | * - if it starts with a "/", the pattern will start with a Root symbol 43 | * - "*" (like in `/users/ * / view`) matches any component (spaces added) 44 | * - if the component starts with `:`, it is considered a variable. 45 | * Example: `/users/:id/view` 46 | * - "text*", "*text*", "*text" creates hasPrefix/hasSuffix/contains patterns 47 | * - otherwise the text is matched AS IS 48 | */ 49 | static func parse(_ s: String) -> [ RoutePattern ]? { 50 | if s == "*" { return nil } // match-all 51 | 52 | let comps = extractEscapedURLPathComponents(for: s) 53 | 54 | var isFirst = true 55 | 56 | var pattern : [ RoutePattern ] = [] 57 | for c in comps { 58 | if isFirst { 59 | isFirst = false 60 | if c == "" { // root 61 | pattern.append(.root) 62 | continue 63 | } 64 | } 65 | 66 | if c == "*" { 67 | pattern.append(.wildcard) 68 | continue 69 | } 70 | 71 | if c.hasPrefix(":") { 72 | let vIdx = c.index(after: c.startIndex) 73 | pattern.append(.variable(String(c[vIdx.. 1 { 84 | let eIdx = c.index(before: c.endIndex) 85 | pattern.append(.contains(String(c[vIdx.. String? 116 | { 117 | // Note: Express does a prefix match, which is important for mounting. 118 | // TODO: Would be good to support a "$" pattern which guarantees an exact 119 | // match. 120 | var pattern = p 121 | var matched = "" 122 | 123 | if debugMatcher { 124 | print("match: components: \(escapedPathComponents)\n" + 125 | " against: \(pattern)") 126 | } 127 | 128 | // this is to support matching "/" against the "/*" ("", "*") pattern 129 | // That is: 130 | // /hello/abc [pc = 2] 131 | // will match 132 | // /hello* [pc = 1] 133 | if escapedPathComponents.count + 1 == pattern.count { 134 | if case .wildcard = pattern.last! { 135 | let endIdx = pattern.count - 1 136 | pattern = Array(pattern[0..= pattern.count else { return nil } 143 | 144 | // If the pattern ends in $ 145 | if let lastComponent = pattern.last { 146 | if case .eol = lastComponent { 147 | // is this correct? 148 | guard escapedPathComponents.count < pattern.count else { return nil } 149 | } 150 | } 151 | 152 | 153 | var lastWasWildcard = false 154 | var lastWasEOL = false 155 | for i in pattern.indices { 156 | let patternComponent = pattern[i] 157 | let matchComponent = escapedPathComponents[i] // TODO: unescape? 158 | 159 | guard patternComponent.match(string: matchComponent) else { 160 | if debugMatcher { 161 | print(" no match on: '\(matchComponent)' (\(patternComponent))") 162 | } 163 | return nil 164 | } 165 | 166 | if i == 0 && matchComponent.isEmpty { 167 | matched += "/" 168 | } 169 | else { 170 | if matched != "/" { matched += "/" } 171 | matched += matchComponent 172 | } 173 | 174 | if debugMatcher { 175 | print(" comp matched[\(i)]: \(patternComponent) " + 176 | "against '\(matchComponent)'") 177 | } 178 | 179 | if case .variable(let s) = patternComponent { 180 | variables[s] = matchComponent // TODO: unescape 181 | } 182 | 183 | 184 | // Special case, last component is a wildcard. Like /* or /todos/*. In 185 | // this case we ignore extra URL path stuff. 186 | let isLast = i + 1 == pattern.count 187 | if isLast { 188 | if case .wildcard = patternComponent { 189 | lastWasWildcard = true 190 | } 191 | if case .eol = patternComponent { 192 | lastWasEOL = true 193 | } 194 | } 195 | } 196 | 197 | if debugMatcher { 198 | if lastWasWildcard || lastWasEOL { 199 | print("MATCH: last was WC \(lastWasWildcard) EOL \(lastWasEOL)") 200 | } 201 | } 202 | 203 | if escapedPathComponents.count > pattern.count { 204 | //if !lastWasWildcard { return nil } 205 | if lastWasEOL { return nil } // all should have been consumed 206 | } 207 | 208 | if debugMatcher { print(" match: '\(matched)'") } 209 | return matched 210 | } 211 | } 212 | 213 | extension RoutePattern: CustomStringConvertible { 214 | 215 | @inlinable 216 | public var description : String { 217 | switch self { 218 | case .root: return "/" 219 | case .text(let v): return v 220 | case .wildcard: return "*" 221 | case .eol: return "$" 222 | case .variable (let n): return ":\(n)" 223 | case .prefix(let v): return "\(v)*" 224 | case .suffix(let v): return "*\(v)" 225 | case .contains(let v): return "*\(v)*" 226 | } 227 | } 228 | } 229 | 230 | func extractEscapedURLPathComponents(for urlPath: String) -> [ String ] { 231 | guard !urlPath.isEmpty else { return [] } 232 | 233 | let isAbsolute = urlPath.hasPrefix("/") 234 | let pathComps = urlPath.split(separator: "/", 235 | omittingEmptySubsequences: false) 236 | .map(String.init) 237 | /* Note: we cannot just return a leading slash for absolute pathes as we 238 | * wouldn't be able to distinguish between an absolute path and a 239 | * relative path starting with an escaped slash. 240 | * So: Absolute pathes instead start with an empty string. 241 | */ 242 | var gotAbsolute = isAbsolute ? false : true 243 | return pathComps.filter { 244 | if $0 != "" || !gotAbsolute { 245 | if !gotAbsolute { gotAbsolute = true } 246 | return true 247 | } 248 | else { 249 | return false 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Sources/express/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 6/2/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import typealias connect.Next 10 | import typealias connect.Middleware 11 | 12 | // A Route itself now can do everything a Router could do before (most 13 | // importantly it can hold an array of middleware) 14 | public typealias Router = Route 15 | 16 | public extension Router { 17 | 18 | convenience init(id: String? = nil, _ pattern: String?, 19 | _ middleware: Middleware...) 20 | { 21 | self.init(id: id, pattern: pattern, method: nil, 22 | middleware: middleware) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/express/ServerResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerResponse.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 6/2/16. 6 | // Copyright © 2016-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import enum NIOHTTP1.HTTPResponseStatus 10 | import struct MacroCore.Buffer 11 | import class http.IncomingMessage 12 | import class http.ServerResponse 13 | import struct Foundation.Data 14 | 15 | public extension ServerResponse { 16 | 17 | /// A reference to the active application. Updated when subapps are triggered. 18 | var app : Express? { return environment[ExpressExtKey.App.self] } 19 | 20 | 21 | /// A reference to the request associated with this response. 22 | var request : IncomingMessage? { 23 | set { environment[ExpressExtKey.RequestKey.self] = newValue } 24 | get { return environment[ExpressExtKey.RequestKey.self] } 25 | } 26 | 27 | typealias Locals = ExpressWrappedDictionary 28 | 29 | /** 30 | * This is legacy, an app can also just use `EnvironmentKey`s with either 31 | * `IncomingMessage` or `ServerResponse`. 32 | * `EnvironmentKey`s are guaranteed to be unique. 33 | * 34 | * Traditionally `locals` was used to store Stringly-typed keys & values. 35 | */ 36 | var locals : Locals { 37 | set { environment[ExpressExtKey.Locals.self] = newValue } 38 | get { return environment[ExpressExtKey.Locals.self] } 39 | } 40 | } 41 | 42 | public extension ServerResponse { 43 | // TODO: Would be cool: send(stream: GReadableStream), then stream.pipe(self) 44 | 45 | 46 | // MARK: - Status Handling 47 | 48 | /// Set the HTTP status, returns self 49 | /// 50 | /// Example: 51 | /// 52 | /// res.status(404).send("didn't find it") 53 | /// 54 | @discardableResult 55 | @inlinable 56 | func status(_ code: Int) -> Self { 57 | statusCode = code 58 | return self 59 | } 60 | 61 | /// Set the HTTP status code and send the status description as the body. 62 | /// 63 | @inlinable 64 | func sendStatus(_ code: Int) { 65 | let status = HTTPResponseStatus(statusCode: code) 66 | statusCode = code 67 | send(status.reasonPhrase) 68 | } 69 | 70 | 71 | // MARK: - Redirects 72 | 73 | /** 74 | * Sets the HTTP `Location` header. The location must be URL encoded already. 75 | * 76 | * If the given string is `back`, it will be replaced with the value of the 77 | * `Referer` header, or `/` if there is none. 78 | * 79 | * If the string is empty or nil, the header will be removed. 80 | */ 81 | func location(_ location: String?) { 82 | guard let location = location, !location.isEmpty else { 83 | removeHeader("Location") 84 | return 85 | } 86 | 87 | if location == "back" { 88 | if let referer = getHeader("Referer") as? String, !referer.isEmpty { 89 | setHeader("Location", referer) 90 | } 91 | else { 92 | setHeader("Location", "/") 93 | } 94 | } 95 | else { 96 | // TBD: make absolute based on Host? 97 | // Also: make paths absolute (e.g. when given 'admin/new') 98 | setHeader("Location", location) 99 | } 100 | } 101 | 102 | func redirect(_ statusCode: Int, _ location: String) { 103 | self.status(statusCode) 104 | self.location(location) 105 | self.end() 106 | } 107 | func redirect(_ location: String) { 108 | redirect(302, location) // work around swiftc confusion 109 | } 110 | 111 | 112 | // MARK: - Sending Content 113 | 114 | @inlinable 115 | func send(_ string: String) { 116 | if canAssignContentType { 117 | var ctype = string.hasPrefix("(_ object: T) { 147 | json(object) 148 | } 149 | @inlinable 150 | func send(_ object: T?) { 151 | guard let object = object else { 152 | log.warn("sending empty string for nil Encodable object?!") 153 | return send("") 154 | } 155 | json(object) 156 | } 157 | 158 | @inlinable 159 | var canAssignContentType : Bool { 160 | return !headersSent && getHeader("Content-Type") == nil 161 | } 162 | 163 | @inlinable 164 | func format(handlers: [ String : () -> () ]) { 165 | var defaultHandler : (() -> ())? = nil 166 | 167 | guard let rq = request else { 168 | handlers["default"]?() 169 | return 170 | } 171 | 172 | for ( key, handler ) in handlers { 173 | guard key != "default" else { defaultHandler = handler; continue } 174 | 175 | if let mimeType = rq.accepts(key) { 176 | if canAssignContentType { 177 | setHeader("Content-Type", mimeType) 178 | } 179 | handler() 180 | return 181 | } 182 | } 183 | if let cb = defaultHandler { cb() } 184 | } 185 | 186 | 187 | // MARK: - Header Accessor Renames 188 | 189 | @inlinable 190 | func get(_ header: String) -> Any? { 191 | return getHeader(header) 192 | } 193 | @inlinable 194 | func set(_ header: String, _ value: Any?) { 195 | if let v = value { setHeader(header, v) } 196 | else { removeHeader(header) } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Sources/express/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // Noze.io / Macro 4 | // 5 | // Created by Helge Heß on 02/06/16. 6 | // Copyright © 2016-2024 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | /** 10 | * Just a special kind of dictionary. The ``Express`` application class is 11 | * currently the sole example. 12 | * 13 | * Examples: 14 | * ```swift 15 | * app.set("env", "production") 16 | * app.enable("x-powered-by") 17 | * 18 | * let env = app.settings.env 19 | * ``` 20 | */ 21 | public protocol SettingsHolder { 22 | 23 | /** 24 | * Sets or removes a configuration key in the settings store. 25 | * 26 | * Example: 27 | * ```swift 28 | * app.set("view engine", "html") 29 | * .set("views", __dirname() + "/views") 30 | * .enable("x-powered-by") 31 | * ``` 32 | * 33 | * - Parameters: 34 | * - key: The name of the key, e.g. "view engine" 35 | * - value: The associated value, if `nil` is passed in, the value is 36 | * removed from the store. 37 | * - Returns: `self` for chaining. 38 | */ 39 | @discardableResult 40 | func set(_ key: String, _ value: Any?) -> Self 41 | 42 | /** 43 | * Returns the value of a configuration key from the settings store. 44 | * 45 | * Example: 46 | * ```swift 47 | * let engine = app.get("view engine") 48 | * ``` 49 | * 50 | * - Parameters: 51 | * - key: The name of the key, e.g. "view engine" 52 | * - Returns: The value in the store, or `nil` if missing. 53 | */ 54 | func get(_ key: String) -> Any? 55 | } 56 | 57 | public extension SettingsHolder { 58 | 59 | /** 60 | * Set configuration key in the settings store to `true`. 61 | * 62 | * Example: 63 | * ```swift 64 | * app.enable("x-powered-by") 65 | * ``` 66 | * 67 | * - Parameters: 68 | * - key: The name of the bool key, e.g. "view engine" 69 | * - Returns: `self` for chaining. 70 | */ 71 | @inlinable 72 | @discardableResult 73 | func enable(_ key: String) -> Self { 74 | return set(key, true) 75 | } 76 | 77 | /** 78 | * Set configuration key in the settings store to `false`. 79 | * 80 | * Example: 81 | * ```swift 82 | * app.disable("x-powered-by") 83 | * ``` 84 | * 85 | * - Parameters: 86 | * - key: The name of the bool key, e.g. "view engine" 87 | * - Returns: `self` for chaining. 88 | */ 89 | @inlinable 90 | @discardableResult 91 | func disable(_ key: String) -> Self { 92 | return set(key, false) 93 | } 94 | 95 | @inlinable 96 | subscript(setting key : String) -> Any? { 97 | get { return get(key) } 98 | set { set(key, newValue) } 99 | } 100 | } 101 | 102 | /** 103 | * An object representing the settings in the associated holder (i.e. in the 104 | * MacroExpress app). 105 | * 106 | * Modules can extend this structure to add more, predefined and typed 107 | * settings. 108 | */ 109 | @dynamicMemberLookup 110 | public struct ExpressSettings { 111 | 112 | public let holder : SettingsHolder 113 | 114 | @inlinable 115 | public init(_ holder: SettingsHolder) { self.holder = holder } 116 | 117 | @inlinable 118 | subscript(dynamicMember key: String) -> Any? { 119 | return holder[setting: key] 120 | } 121 | } 122 | 123 | public extension SettingsHolder { 124 | 125 | /** 126 | * Returns an object representing the settings in the holder (i.e. in the 127 | * MacroExpress app). 128 | * 129 | * Example: 130 | * 131 | * if app.settings.env == "production" { ... } 132 | */ 133 | @inlinable 134 | var settings : ExpressSettings { return ExpressSettings(self) } 135 | } 136 | 137 | 138 | // MARK: - Predefined Settings 139 | 140 | import enum MacroCore.process 141 | 142 | public extension ExpressSettings { 143 | 144 | /** 145 | * Returns the runtime environment we are in, e.g. `production` or 146 | * `development`. 147 | * 148 | * This first checks for an explicit `env` setting in the SettingsHolder 149 | * (i.e. Express application object). 150 | * If that's missing, it checks the `MACRO_ENV` environment variable. 151 | */ 152 | @inlinable 153 | var env : String { 154 | if let v = holder .get("env") as? String { return v } 155 | if let v = process.env["MACRO_ENV"] { return v } 156 | 157 | #if DEBUG 158 | return "development" 159 | #else 160 | return "production" 161 | #endif 162 | } 163 | 164 | /** 165 | * Returns true if the Macro should add the x-powered-by header to the 166 | * response. 167 | */ 168 | @inlinable 169 | var xPoweredBy : Bool { 170 | guard let v = holder.get("x-powered-by") else { return true } 171 | return boolValue(v) 172 | } 173 | } 174 | 175 | 176 | // MARK: - Helpers 177 | 178 | @usableFromInline 179 | func boolValue(_ v : Any) -> Bool { 180 | // TODO: this should be some Foundation like thing 181 | if let b = v as? Bool { return b } 182 | if let b = v as? Int { return b != 0 } 183 | #if swift(>=5.10) 184 | if let i = (v as? any BinaryInteger) { return Int(i) != 0 } 185 | #endif 186 | if let s = v as? String { 187 | switch s.lowercased() { 188 | case "no", "false", "0", "disable": return false 189 | default: return true 190 | } 191 | } 192 | return true 193 | } 194 | -------------------------------------------------------------------------------- /Sources/mime/MIME.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MIME.swift 3 | // ExExpress / Macro 4 | // 5 | // Created by Helge Hess on 11/06/17. 6 | // Copyright © 2017-2020 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | // desired API: 10 | // https://www.npmjs.com/package/mime-types 11 | 12 | import struct Foundation.URL 13 | 14 | public enum MIMEModule {} 15 | public typealias mime = MIMEModule 16 | 17 | public extension MIMEModule { 18 | 19 | /** 20 | * Returns a MIME type for the given extension or filesystem path. 21 | * 22 | * Examples: 23 | * 24 | * mime.lookup("json") => application/json; charset=UTF-8 25 | * mime.lookup("index.html") => text/html; charset=UTF-8 26 | * 27 | */ 28 | @inlinable 29 | static func lookup(_ path: String) -> String? { 30 | if !path.contains(".") && !path.contains("/") { 31 | if let ctype = types[path] { 32 | if let cs = defaultCharsets[ctype] { 33 | return ctype + "; charset=" + cs 34 | } 35 | return ctype 36 | } 37 | } 38 | 39 | let url = Foundation.URL(fileURLWithPath: path) 40 | guard let ctype = types[url.pathExtension] else { return nil } 41 | if let cs = defaultCharsets[ctype] { return ctype + "; charset=" + cs } 42 | return ctype 43 | } 44 | 45 | @inlinable 46 | static func charset(_ ctype: String) -> String? { 47 | return defaultCharsets[ctype] 48 | } 49 | 50 | static let types : [ String : String ] = [ 51 | "html" : "text/html", 52 | "js" : "application/javascript", 53 | "ico" : "image/x-icon", 54 | "svg" : "image/svg+xml", 55 | "eot" : "application/vnd.ms-fontobject", 56 | "woff" : "application/x-font-woff", 57 | "woff2" : "application/x-font-woff", 58 | "ttf" : "application/x-font-ttf", 59 | "markdown" : "text/x-markdown", 60 | "md" : "text/x-markdown", 61 | "json" : "application/json", 62 | "vcf" : "text/vcard", 63 | "ics" : "text/calendar", 64 | "xml" : "text/xml" 65 | ] 66 | 67 | static let extensions : [ String : [ String ] ] = [ 68 | "text/html" : [ "html" ], 69 | "application/javascript" : [ "js" ], 70 | "image/x-icon" : [ "ico" ], 71 | "image/svg+xml" : [ "svg" ], 72 | "application/vnd.ms-fontobject" : [ "eot" ], 73 | "application/x-font-woff" : [ "woff", "woff2" ], 74 | "application/x-font-ttf" : [ "ttf" ], 75 | "text/x-markdown" : [ "markdown", "md" ], 76 | "application/json" : [ "json" ], 77 | "text/vcard" : [ "vcf" ], 78 | "text/calendar" : [ "ics" ], 79 | "text/xml" : [ "xml" ] 80 | ] 81 | 82 | static let defaultCharsets : [ String : String ] = [ 83 | "text/html" : "UTF-8", 84 | "application/javascript" : "UTF-8", 85 | "image/svg+xml" : "UTF-8", 86 | "text/x-markdown" : "UTF-8", 87 | "application/json" : "UTF-8", 88 | "text/vcard" : "UTF-8", 89 | "text/calendar" : "UTF-8", 90 | "text/xml" : "UTF-8" 91 | ] 92 | 93 | static let typePrefixMap = [ 94 | ( " Void ) 26 | -> Void 27 | 28 | public typealias FilenameSelector = 29 | ( IncomingMessage, File, @escaping ( Swift.Error?, String ) -> Void ) 30 | -> Void 31 | 32 | public let destination : DestinationSelector 33 | public let filename : FilenameSelector? 34 | 35 | public init(destination : @escaping DestinationSelector, 36 | filename : FilenameSelector? = nil) 37 | { 38 | self.destination = destination 39 | self.filename = filename 40 | } 41 | 42 | public convenience init(dest: String) { 43 | self.init(destination: { req, file, yield in 44 | yield(nil, dest) 45 | }) 46 | } 47 | 48 | 49 | // MARK: - Storage API 50 | 51 | public func startFile(_ file: multer.File, in ctx: MulterStorageContext) { 52 | } 53 | public func endFile (_ file: multer.File, in ctx: MulterStorageContext) { 54 | } 55 | 56 | public func write(_ data: Buffer, to file: multer.File, 57 | in ctx: MulterStorageContext) throws 58 | { 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/multer/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // MacroExpress / multer 4 | // 5 | // Created by Helge Heß on 30/05/16. 6 | // Copyright © 2021-2025 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import struct MacroCore.Buffer 10 | 11 | public extension multer { 12 | 13 | /** 14 | * Represents a file that got uploaded by ``multer``. It carries the field 15 | * name, the filename, the mime type, and if the ``MemoryStorage`` was used, 16 | * the file contents. 17 | */ 18 | final class File: Equatable { 19 | 20 | /// Name in form field 21 | public var fieldName : String 22 | 23 | /// Name of file (filename in content-disposition) 24 | public var originalName : String 25 | 26 | // TBD: encoding? 27 | 28 | /** 29 | * MIME type of the file - as declared by the browser/user-agent, e.g. 30 | * `image/png` or `application/octet-stream`. 31 | * Defaults to the latter if not found. 32 | */ 33 | public var mimeType : String 34 | 35 | /** 36 | * The path of the file on the local filesystem, if available. I.e. when 37 | * used together with the disk storage. 38 | */ 39 | public var path : String? 40 | 41 | /** 42 | * The Buffer of the file, if loaded into memory. I.e. when used together 43 | * with the memory storage. 44 | */ 45 | public var buffer : Buffer? 46 | 47 | /** 48 | * Create a new multer file object. 49 | * 50 | * - Parameters: 51 | * - fieldName: Name of the form field that contains the file 52 | * - originalName: Name of file (filename in content-disposition) 53 | * - mimeType: MIME type of the file, defaults to octet-stream if n/a. 54 | * - path: Path to stored file on the server, if used w/ disk 55 | * storage. 56 | * - buffer: Contents of file, if loaded into memory. 57 | */ 58 | @inlinable 59 | public init(fieldName : String, 60 | originalName : String, 61 | mimeType : String, 62 | path : String? = nil, 63 | buffer : Buffer? = nil) 64 | { 65 | self.fieldName = fieldName 66 | self.originalName = originalName 67 | self.mimeType = mimeType 68 | self.path = path 69 | self.buffer = buffer 70 | } 71 | 72 | /** 73 | * Returns true if this has a nil ``path`` or ``buffer`` and no 74 | * ``originalName`` set. 75 | * 76 | * It may still have a ``mimeType`` set to `application/octet-stream`. 77 | */ 78 | @inlinable 79 | public var isEmpty: Bool { 80 | originalName.isEmpty && path == nil && buffer == nil 81 | } 82 | 83 | @inlinable 84 | public static func ==(lhs: File, rhs: File) -> Bool { 85 | return lhs.fieldName == rhs.fieldName 86 | && lhs.originalName == rhs.originalName 87 | && lhs.mimeType == rhs.mimeType 88 | && lhs.path == rhs.path 89 | && lhs.buffer == rhs.buffer 90 | } 91 | } 92 | } 93 | 94 | extension multer.File: CustomStringConvertible { 95 | 96 | @inlinable 97 | public var description: String { 98 | var ms = " Middleware 40 | { 41 | let restrictions = fields.flatMap { Dictionary(tolerantPairs: $0) } 42 | 43 | return { req, res, next in 44 | guard typeIs(req, [ "multipart/form-data" ]) != nil else { return next() } 45 | 46 | guard let ctype = req.headers["Content-Type"].first, 47 | let boundary = extractHeaderArgument("boundary", from: ctype) 48 | else { 49 | req.log.warn("missing boundary in multipart/form-data", 50 | req.getHeader("Content-Type") ?? "-") 51 | return next() 52 | } 53 | 54 | // Interact properly w/ bodyParser 55 | switch req.body { 56 | case .json, .urlEncoded, .text: 57 | return next() // already parsed as another type 58 | 59 | case .noBody, .error: // already parsed as nothing or error 60 | return next() 61 | 62 | case .notParsed: 63 | let ctx = Context(request: req, response: res, boundary: boundary, 64 | multer: self, restrictions: restrictions, 65 | next: next) 66 | req.onReadable { 67 | let data = req.read() 68 | ctx.write(data) 69 | } 70 | req.onError(execute: ctx.handleError) 71 | req.onEnd (execute: ctx.finish) 72 | 73 | case .raw(let bytes): 74 | let ctx = Context(request: req, response: res, boundary: boundary, 75 | multer: self, restrictions: restrictions, 76 | next: next) 77 | ctx.write(bytes) 78 | ctx.finish() 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/multer/Multer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Multer.swift 3 | // MacroExpress / multer 4 | // 5 | // Created by Helge Heß on 30/05/16. 6 | // Copyright © 2021-2025 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import MacroCore 10 | import http 11 | import connect 12 | 13 | 14 | /** 15 | * Middleware to parse `multipart/form-data` payloads. 16 | * 17 | * E.g. files submitted using an HTML form like: 18 | * ```html 19 | *
20 | * 21 | * 22 | *
23 | * ``` 24 | * 25 | * Roughly designed after the Node 26 | * [multer](https://github.com/expressjs/multer#readme) 27 | * package. 28 | */ 29 | public struct multer { 30 | 31 | /** 32 | * A function that controls what files should be uploaded or skipped. 33 | * 34 | * This can be passed to a multer-init. The function gets the 35 | * `IncomingMessage`, the ``File`` and a callback that it **must** invoke to 36 | * continue multer processing. 37 | * 38 | * The filter can issue an error by passing on into the error parameter of the 39 | * callback. And it can pass in `true` or `false` to accept or reject a file. 40 | */ 41 | public typealias FileFilter = 42 | ( IncomingMessage, File, @escaping ( Swift.Error?, Bool ) -> Bool ) -> Void 43 | 44 | public var storage : MulterStorage 45 | public var fileFilter : FileFilter? 46 | public var limits : Limits 47 | public var dest : String? 48 | 49 | 50 | // MARK: - Init 51 | 52 | #if false // Swift 5.0 is not clever enough to consider the nested unavail 53 | @available(*, unavailable, message: "DiskStorage is not working yet.") 54 | @inlinable 55 | public init(storage : MulterStorage? = nil, 56 | dest : String, 57 | limits : Limits = Limits(), 58 | fileFilter : FileFilter? = nil) 59 | { 60 | self.dest = dest 61 | self.fileFilter = fileFilter 62 | self.limits = limits 63 | self.storage = storage ?? DiskStorage(dest: dest) 64 | } 65 | #endif 66 | 67 | /** 68 | * Create a new multer configuration. 69 | * 70 | * This accepts a ``MulterStorage``, ``Limits-swift.struct`` and 71 | * a ``FileFilter-swift.typealias``. 72 | * All of which are optional. The default storage is the ``MemoryStorage``. 73 | * 74 | * - Parameters: 75 | * - storage: Where to put uploaded files, defaults to ``MemoryStorage`` 76 | * - limits: What ``Limits-swift.struct`` to apply, checks the 77 | * documentation for the defaults. 78 | * - fileFilter: A function that can filter what files should be uploaded, 79 | * defaults to "no filter", i.e. accept all files. 80 | */ 81 | @inlinable 82 | public init(storage : MulterStorage? = nil, 83 | limits : Limits = Limits(), 84 | fileFilter : FileFilter? = nil) 85 | { 86 | self.dest = nil 87 | self.fileFilter = fileFilter 88 | self.limits = limits 89 | self.storage = storage ?? multer.memoryStorage() 90 | } 91 | } 92 | 93 | public extension multer { // MARK: - Storage Factory 94 | 95 | /** 96 | * Returns a multer storage that writes the file contents to the 97 | * ``File/buffer`` property of the ``File`` object, i.e. stores it in-memory. 98 | * 99 | * - Returns: A new ``MemoryStorage`` object. 100 | */ 101 | @inlinable 102 | static func memoryStorage() -> MemoryStorage { 103 | return MemoryStorage() 104 | } 105 | 106 | #if false // Swift 5.0 is not clever enough to consider the nested unavail 107 | @available(*, unavailable, message: "DiskStorage is not working yet.") 108 | @inlinable 109 | static 110 | func diskStorage(destination : @escaping DiskStorage.DestinationSelector, 111 | filename : DiskStorage.FilenameSelector? = nil) 112 | -> DiskStorage 113 | { 114 | return DiskStorage(destination: destination, filename: filename) 115 | } 116 | #endif 117 | } 118 | 119 | 120 | public extension multer { // MARK: - Middleware Convenience 121 | 122 | /** 123 | * Accept a single file for the specific `fieldName`. 124 | * 125 | * - Parameter fieldName: The name of the form field associated with a file. 126 | * - Returns: The middleware to parse the form data. 127 | */ 128 | @inlinable 129 | func single(_ fieldName: String) -> Middleware { 130 | return fields([ ( fieldName, 1 )]) 131 | } 132 | 133 | /** 134 | * Accept a set of files for the specific `fieldName`. 135 | * 136 | * - Parameter fieldName: The name of the form field associated with the files. 137 | * - Returns: The middleware to parse the form data. 138 | */ 139 | @inlinable 140 | func array(_ fieldName: String, _ maxCount: Int? = nil) -> Middleware { 141 | return fields([ ( fieldName, maxCount )]) 142 | } 143 | 144 | /** 145 | * Accept text fields only. 146 | * 147 | * Emits a `limitUnexpectedFile` error if a file is encountered. 148 | * 149 | * - Returns: The middleware to parse the form data. 150 | */ 151 | @inlinable 152 | func none() -> Middleware { 153 | return fields([]) 154 | } 155 | /** 156 | * Accept all incoming files. The files will be available in the 157 | * `request.files` property. 158 | * 159 | * - Returns: The middleware to parse the form data. 160 | */ 161 | @inlinable 162 | func any() -> Middleware { 163 | return fields(nil) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Sources/multer/MulterError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MulterError.swift 3 | // MacroExpress / multer 4 | // 5 | // Created by Helge Heß on 30/05/16. 6 | // Copyright © 2021 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | public extension multer { 10 | 11 | enum MulterError: Swift.Error { 12 | case limitUnexpectedFile(fieldName: String) 13 | case tooManyFiles 14 | case tooManyFields 15 | case fileTooLarge 16 | case fieldNameTooLong 17 | case fieldValueTooLong 18 | 19 | case invalidPartHeader(MultiPartParser.Header) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/multer/MulterStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MulterStorage.swift 3 | // MacroExpress / multer 4 | // 5 | // Created by Helge Heß on 30/05/16. 6 | // Copyright © 2021 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import struct MacroCore.Buffer 10 | 11 | public protocol MulterStorageContext { 12 | 13 | var config : multer { get } 14 | 15 | func handleError(_ error: Swift.Error) 16 | } 17 | 18 | public protocol MulterStorage { 19 | 20 | func startFile(_ file: multer.File, in context: MulterStorageContext) 21 | func endFile (_ file: multer.File, in context: MulterStorageContext) 22 | 23 | func write(_ data: Buffer, to file: multer.File, 24 | in context: MulterStorageContext) throws 25 | } 26 | -------------------------------------------------------------------------------- /Sources/multer/PartType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PartType.swift 3 | // MacroExpress / multer 4 | // 5 | // Created by Helge Heß 6 | // Copyright © 2021 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal extension multer { 12 | 13 | // Whether to create a file or form value. 14 | // We get: 15 | // Content-Disposition: form-data; name="file"; filename="abc.csv" 16 | // Content-Type: application/octet-stream 17 | // 18 | // Note: As per RFC 7578: 19 | // - each part _must_ have a form-data Content-Disposition. 20 | // - "filename" is optional 21 | // - Content-Type defaults to 'text/plain', can have charset 22 | // - can have Content-Transfer-Encoding, e.g. quoted-printable 23 | // - not used in practice 24 | // - there can be a special `_charset_` form value carrying the 25 | // default charset (e.g. 'iso-8859-1') 26 | 27 | /** 28 | * The type of a specific multipart/form-data part. 29 | * 30 | * It is either a file, a field, or something unknown. 31 | */ 32 | enum PartType: Equatable { 33 | 34 | case file (File) 35 | case field(String) 36 | case invalid 37 | 38 | var name : String { 39 | switch self { 40 | case .file (let file) : return file.fieldName 41 | case .field(let name) : return name 42 | case .invalid : return "" 43 | } 44 | } 45 | 46 | /** 47 | * Returns `.none` if there is not `Content-Disposition`, or it doesn't 48 | * contain a `name` parameter. 49 | * 50 | * Returns `.file` if the `Content-Disposition` contains either a `filename` 51 | * parameter, of it the `Content-Type` isn't "text/plain". 52 | * 53 | * Otherwise returns `.field`. 54 | */ 55 | init(with header: MultiPartParser.Header) { 56 | // Content-Disposition: form-data; name="file"; filename="abc.csv" 57 | // Content-Type: application/octet-stream 58 | guard let cd = header.valueForHeader("Content-Disposition") else { 59 | self = .invalid 60 | return 61 | } 62 | guard let name = extractHeaderArgument("name", from: cd) else { 63 | self = .invalid 64 | return 65 | } 66 | 67 | let ctype = header.valueForHeader("Content-Type") ?? "text/plain" 68 | let filename = extractHeaderArgument("filename", from: cd) 69 | 70 | if filename != nil || !ctype.hasPrefix("text/plain") { 71 | self = .file(File(fieldName: name, originalName: filename ?? "", 72 | mimeType: ctype, path: nil, buffer: nil)) 73 | } 74 | else { 75 | self = .field(name) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/multer/ProcessingContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessingContext.swift 3 | // MacroExpress / multer 4 | // 5 | // Created by Helge Heß on 30/05/16. 6 | // Copyright © 2021-2025 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import struct MacroCore.Buffer 10 | import http 11 | import connect 12 | 13 | internal extension multer { 14 | 15 | /** 16 | * This is the main driver for processing the multipart/form-data content. 17 | * 18 | * It collects the plain field values, 19 | * pushes file data into the storage 20 | * and validates limits. 21 | */ 22 | final class Context: MulterStorageContext { 23 | 24 | let multer : multer 25 | let restrictions : [ String : Int? ]? // fieldname to maxCount 26 | let request : IncomingMessage 27 | let response : ServerResponse 28 | var next : Next? 29 | var parser : MultiPartParser 30 | 31 | var config : multer { return multer } 32 | 33 | init(request : IncomingMessage, 34 | response : ServerResponse, 35 | boundary : String, 36 | multer : multer, 37 | restrictions : [ String : Int? ]?, 38 | next : @escaping Next) 39 | { 40 | self.request = request 41 | self.response = response 42 | self.next = next 43 | self.multer = multer 44 | self.restrictions = restrictions 45 | self.parser = MultiPartParser(boundary: boundary) 46 | } 47 | 48 | private let defaultFieldValueEncoding : String.Encoding = .utf8 49 | 50 | private var fieldValueEncoding : String.Encoding? { 51 | guard let charsetBuffer = self.values["_charset_"]?.first else { 52 | return nil 53 | } 54 | guard let charset = try? charsetBuffer.toString(.utf8) else { 55 | request.log 56 | .error("Could not decode formdata encoding buffer:", charsetBuffer) 57 | return nil 58 | } 59 | return String.Encoding 60 | .encodingWithName(charset, fallbackEncoding: defaultFieldValueEncoding) 61 | } 62 | 63 | private func buildFormValues() -> [ String : Any ] { 64 | guard !values.isEmpty else { return [:] } 65 | 66 | let encoding = fieldValueEncoding ?? defaultFieldValueEncoding 67 | var formValues = [ String : Any ]() 68 | formValues.reserveCapacity(values.count) 69 | for ( name, value ) in values { 70 | switch value { 71 | case .single(let buffer): 72 | if let s = try? buffer.toString(encoding) { formValues[name] = s } 73 | else { 74 | formValues[name] = buffer 75 | } 76 | case .multiple(let buffers): 77 | assert(buffers.count > 1) 78 | let strings = buffers.compactMap { try? $0.toString(encoding) } 79 | if strings.count == buffers.count { formValues[name] = strings } 80 | else { formValues[name] = buffers } 81 | } 82 | } 83 | return formValues 84 | } 85 | 86 | func finish() { 87 | parser.end(handler: handleEvent) 88 | guard let next = next else { return } // nothing will handle the result? 89 | 90 | switch request.body { 91 | 92 | case .notParsed: 93 | if values.isEmpty { 94 | request.body = .urlEncoded([:]) 95 | } 96 | else { 97 | request.body = .urlEncoded(buildFormValues()) 98 | } 99 | 100 | case .urlEncoded(var values): 101 | if !values.isEmpty { 102 | values.merge(buildFormValues(), uniquingKeysWith: { $1 }) 103 | request.body = .urlEncoded(values) 104 | } 105 | 106 | default: 107 | if !values.isEmpty { 108 | request.log.warn( 109 | "Not storing multipart/form-data values, body already set!") 110 | } 111 | } 112 | 113 | // Filter out empty files. 114 | for ( field, fieldFiles ) in files where fieldFiles.count == 1 { 115 | guard let fieldFile = fieldFiles.first, fieldFile.isEmpty else { 116 | continue 117 | } 118 | files[field] = [] // this is an empty file 119 | } 120 | 121 | request.files = files 122 | 123 | next() 124 | self.next = nil 125 | } 126 | func write(_ bytes: Buffer) { parser.write(bytes, handler: handleEvent) } 127 | 128 | func handleError(_ error: Swift.Error) { 129 | if case .notParsed = request.body { request.body = .error(error) } 130 | guard let next = next else { return } 131 | next(error) 132 | self.next = nil 133 | 134 | // TBD: Is this sensible? I think so. 135 | request.destroy(error) 136 | } 137 | 138 | 139 | // MARK: - Parser 140 | 141 | private enum FieldValue { 142 | case single (Buffer) 143 | case multiple([ Buffer ]) 144 | 145 | var first: Buffer? { 146 | switch self { 147 | case .single (let buffer) : return buffer 148 | case .multiple(let buffers) : return buffers.first 149 | } 150 | } 151 | } 152 | 153 | private var values = [ String : FieldValue ]() 154 | private var files = [ String : [ File ]]() 155 | private var header : [ ( name: String, value: String ) ]? 156 | private var bodyData : Buffer? 157 | private var activePart = PartType.invalid 158 | 159 | private var fileCount : Int { return files.nestedCount } 160 | 161 | private func finishPart() { 162 | defer { header = nil; bodyData = nil; activePart = .invalid } 163 | guard let header = header else { return } 164 | request.log.trace("end part:", header, bodyData, values) 165 | } 166 | 167 | /** 168 | * Check whether the given part would exceed a restriction in either the 169 | * multer config, or the name/max-count array. 170 | */ 171 | private func exceedsPartRestriction(_ partType: PartType) -> MulterError? { 172 | if let v = multer.limits.fieldNameSize { 173 | guard partType.name.count <= v else { return .fieldNameTooLong } 174 | } 175 | 176 | switch partType { 177 | case .invalid: break 178 | 179 | case .field: 180 | if let v = multer.limits.fields { 181 | guard v < self.values.count else { return .tooManyFields } 182 | } 183 | 184 | case .file(let file): 185 | let name = file.fieldName 186 | if let restrictions = restrictions { 187 | guard let restriction = restrictions[name] else { 188 | return .limitUnexpectedFile(fieldName: name) 189 | } 190 | if let v = restriction { 191 | let existingCount = files[name]?.count ?? 0 192 | guard existingCount < v else { return .tooManyFiles } 193 | } 194 | // else: unrestricted count 195 | } 196 | if let v = multer.limits.files { 197 | guard fileCount < v else { return .tooManyFiles } 198 | } 199 | } 200 | 201 | return nil 202 | } 203 | 204 | /** 205 | * This is our MultiPartParser handler function. 206 | * We feed data into the parser, and the parser calls us back with parsed 207 | * events. 208 | */ 209 | private func handleEvent(_ event: MultiPartParser.Event) { 210 | guard next != nil else { 211 | return request.log 212 | .warn("multipart parse event, but next has been called:", event) 213 | } 214 | 215 | // TODO: this needs to write into the storage 216 | switch event { 217 | 218 | case .parseError(let error): 219 | request.log.error("multipart/form-data parse error:", error) 220 | handleError(error) 221 | 222 | case .preambleData(let buffer): 223 | if !buffer.isEmpty { 224 | request.log.log("ignoring multipart preamble:", buffer) 225 | } 226 | case .postambleData(let buffer): 227 | if !buffer.isEmpty { 228 | request.log.log("ignoring multipart postamble:", buffer) 229 | } 230 | 231 | case .startPart(let header): 232 | finishPart() // close existing 233 | 234 | let partType = PartType(with: header) 235 | 236 | if case .invalid = partType { 237 | request.log.error("Invalid multipart/form-data part:", header) 238 | return handleError(MulterError.invalidPartHeader(header)) 239 | } 240 | if let error = exceedsPartRestriction(partType) { 241 | request.log.error("Limit hit for multipart part:", partType.name, 242 | error) 243 | return handleError(error) 244 | } 245 | 246 | self.header = header 247 | self.activePart = partType 248 | request.log.trace("start part:", partType, header) 249 | 250 | if case .file(let file) = partType { 251 | files[file.fieldName, default: []].append(file) 252 | multer.storage.startFile(file, in: self) 253 | } 254 | 255 | case .endPart: 256 | switch activePart { 257 | case .invalid : break 258 | case .file (let file) : multer.storage.endFile(file, in: self) 259 | case .field(let name) : endField(name) 260 | } 261 | finishPart() 262 | 263 | case .bodyData(let data): 264 | switch activePart { 265 | case .invalid : break 266 | case .file(let file): 267 | do { try multer.storage.write(data, to: file, in: self) } 268 | catch { handleError(error) } 269 | case .field : addFieldData(data) 270 | } 271 | } 272 | } 273 | 274 | 275 | // MARK: - Deal with field data 276 | 277 | private func addFieldData(_ data: Buffer) { 278 | if let v = multer.limits.fileSize { 279 | let newSize = (bodyData?.count ?? 0) + data.count 280 | guard newSize <= v else { 281 | return handleError(MulterError.fieldValueTooLong) 282 | } 283 | } 284 | if nil == bodyData?.append(data) { bodyData = data } 285 | } 286 | 287 | private func endField(_ name: String) { 288 | let fieldBuffer = bodyData ?? Buffer(capacity: 0) 289 | bodyData = nil 290 | 291 | if var value = values.removeValue(forKey: name) { 292 | switch value { 293 | case .single(let buffer): 294 | value = .multiple([ buffer, fieldBuffer ]) 295 | case .multiple(var buffers): 296 | buffers.append(fieldBuffer) 297 | value = .multiple(buffers) 298 | } 299 | } 300 | else { 301 | values[name] = .single(fieldBuffer) 302 | } 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /Sources/multer/README.md: -------------------------------------------------------------------------------- 1 |

Macro Multer 2 | 4 |

5 | 6 | A package for parsing `multipart/form-data` payloads. 7 | 8 | E.g. files submitted using an HTML form like: 9 | ```html 10 |
11 | 12 | 13 |
14 | ``` 15 | 16 | Roughly designed after the Node [multer](https://github.com/expressjs/multer#readme) 17 | package. 18 | 19 | 20 | **Note**: DiskStorage is prepared, but not working yet. 21 | 22 | ### Example 23 | 24 | [Examples](https://github.com/Macro-swift/Examples/blob/main/Sources/express-simple/main.swift#L48) 25 | ```swift 26 | app.post("/multer", multer().array("file", 10)) { req, res, _ in 27 | req.log.info("Got files:", req.files["file"]) 28 | res.render("multer", [ 29 | "files": req.files["file"]?.map { 30 | [ "name": $0.originalName, 31 | "size": $0.buffer?.length ?? 0, 32 | "mimeType": $0.mimeType ] 33 | } ?? [], 34 | "hasFiles": !(req.files["file"]?.isEmpty ?? true) 35 | ]) 36 | } 37 | ``` 38 | 39 | 40 | ### Links 41 | 42 | - [Multer](https://github.com/expressjs/multer#readme) 43 | - [Macro](https://github.com/Macro-swift/Macro/) 44 | - [SwiftNIO](https://github.com/apple/swift-nio) 45 | 46 | ### Who 47 | 48 | **MacroExpress** is brought to you by 49 | the 50 | [Always Right Institute](http://www.alwaysrightinstitute.com) 51 | and 52 | [ZeeZide](http://zeezide.de). 53 | We like 54 | [feedback](https://twitter.com/ar_institute), 55 | GitHub stars, 56 | cool [contract work](http://zeezide.com/en/services/services.html), 57 | presumably any form of praise you can think of. 58 | 59 | There is a `#microexpress` channel on the 60 | [Noze.io Slack](http://slack.noze.io/). Feel free to join! 61 | -------------------------------------------------------------------------------- /Sources/multer/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // MacroExpress / multer 4 | // 5 | // Created by Helge Heß on 30/05/16. 6 | // Copyright © 2021 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | 10 | internal func extractHeaderArgument(_ arg: String, from value: String) 11 | -> String? 12 | { 13 | // Naive version, wish we had structured headers ;-) 14 | // multipart/form-data; boundary="abc" 15 | // multipart/form-data; boundary=abc 16 | let parts = value 17 | .split(separator: ";", maxSplits: 20, omittingEmptySubsequences: true) 18 | .map { $0.trimmingCharacters(in: .whitespaces) } 19 | 20 | guard let value = parts.first(where: { $0.hasPrefix("\(arg)=")})? 21 | .dropFirst(arg.count + 1) 22 | .trimmingCharacters(in: .whitespaces) 23 | else { 24 | return nil 25 | } 26 | 27 | if value.first == "\"" { 28 | guard value.count > 1 && value.last == "\"" else { 29 | assertionFailure("Unexpected \(arg) value quoting in: \(value)") 30 | return nil 31 | } 32 | return String(value.dropFirst().dropLast()) 33 | } 34 | 35 | return value 36 | } 37 | 38 | internal extension Collection where Element == MultiPartParser.HeaderField { 39 | 40 | func valueForHeader(_ name: String) -> String? { 41 | if let pair = first(where: { $0.name == name }) { return pair.value } 42 | let lcName = name.lowercased() 43 | if let pair = first(where: { $0.name.lowercased() == lcName }) { 44 | return pair.value 45 | } 46 | return nil 47 | } 48 | } 49 | 50 | internal extension Dictionary { 51 | 52 | /// Same like `init(uniquingKeysWith:)`, but doesn't crash if abused. 53 | init(tolerantPairs: [ ( Key, Value ) ]) { 54 | self.init() 55 | reserveCapacity(tolerantPairs.count) 56 | for ( key, value ) in tolerantPairs { 57 | assert(self[key] == nil, "Detected duplicate key: \(key)") 58 | self[key] = value 59 | } 60 | } 61 | 62 | } 63 | 64 | internal extension Dictionary where Value: Collection { 65 | 66 | var nestedCount : Int { 67 | var count = 0 68 | for values in self.values { count += values.count } 69 | return count 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #error("Swift 5.5 requires --enable-test-discovery") 4 | -------------------------------------------------------------------------------- /Tests/RouteTests/ErrorMiddlewareTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MacroTestUtilities 3 | import class http.IncomingMessage 4 | @testable import express 5 | 6 | final class ErrorMiddlewareTests: XCTestCase { 7 | 8 | func testSimpleThrowErrorHandler() throws { 9 | enum SomeError: Swift.Error { 10 | case thisWentWrong 11 | } 12 | 13 | let route = Route(id: "root") 14 | 15 | // install error handler 16 | 17 | var errorCatched : Swift.Error? = nil 18 | route.use(id: "handler") { error, req, res, next in 19 | XCTAssertNil(errorCatched) 20 | errorCatched = error 21 | } 22 | 23 | // install throwing route 24 | 25 | route.use(id: "thrower") { req, res, next in 26 | throw SomeError.thisWentWrong 27 | } 28 | 29 | // test 30 | 31 | let req = IncomingMessage( 32 | .init(version: .init(major: 1, minor: 1), method: .POST, uri: "/hello")) 33 | let res = TestServerResponse() 34 | 35 | var didCallNext = false 36 | try route.handle(request: req, response: res) { ( args : Any... ) in 37 | XCTAssertTrue(args.isEmpty, "toplevel next called w/ arguments \(args)") 38 | didCallNext = true 39 | } 40 | XCTAssertFalse (didCallNext, "error not handled by error middleware") 41 | XCTAssertNotNil(errorCatched, "no error captured by error middleware") 42 | XCTAssert (errorCatched is SomeError, "different error type captured") 43 | if let error = errorCatched as? SomeError { 44 | XCTAssertEqual(error, SomeError.thisWentWrong, 45 | "different error captured") 46 | } 47 | } 48 | 49 | func testSimpleNextErrorHandler() throws { 50 | enum SomeError: Swift.Error { 51 | case thisWentWrong 52 | } 53 | 54 | let route = Route(id: "root") 55 | 56 | // install error handler 57 | 58 | var errorCatched : Swift.Error? = nil 59 | route.use { error, req, res, next in 60 | XCTAssertNil(errorCatched) 61 | errorCatched = error 62 | } 63 | 64 | // install throwing route 65 | 66 | route.use { req, res, next in 67 | next(SomeError.thisWentWrong) 68 | } 69 | 70 | // test 71 | 72 | let req = IncomingMessage( 73 | .init(version: .init(major: 1, minor: 1), method: .POST, uri: "/hello")) 74 | let res = TestServerResponse() 75 | 76 | var didCallNext = false 77 | try route.handle(request: req, response: res) { ( args : Any... ) in 78 | XCTAssertTrue(args.isEmpty, "toplevel next called w/ arguments \(args)") 79 | didCallNext = true 80 | } 81 | XCTAssertFalse (didCallNext, "error not handled by error middleware") 82 | XCTAssertNotNil(errorCatched, "no error captured by error middleware") 83 | XCTAssert (errorCatched is SomeError, "different error type captured") 84 | if let error = errorCatched as? SomeError { 85 | XCTAssertEqual(error, SomeError.thisWentWrong, 86 | "different error captured") 87 | } 88 | } 89 | 90 | static var allTests = [ 91 | ( "testSimpleThrowErrorHandler" , testSimpleThrowErrorHandler ), 92 | ( "testSimpleNextErrorHandler" , testSimpleNextErrorHandler ), 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /Tests/RouteTests/RouteMountingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MacroTestUtilities 3 | import class http.IncomingMessage 4 | @testable import express 5 | 6 | final class RouteMountingTests: XCTestCase { 7 | 8 | func testSimpleMountMatch() throws { 9 | let outerRoute = Route(id: "outer") 10 | 11 | // match /admin/view 12 | var didCallRoute = false 13 | outerRoute.route("/admin") 14 | .get("/view") { req, res, next in 15 | XCTAssertEqual(req.baseURL, "/admin/view", "baseURL does not match") 16 | XCTAssertEqual(req.url, "/admin/view", "HTTP URL does not match") 17 | didCallRoute = true 18 | } 19 | 20 | // test 21 | 22 | let req = IncomingMessage( 23 | .init(version: .init(major: 1, minor: 1), method: .GET, 24 | uri: "/admin/view")) 25 | let res = TestServerResponse() 26 | 27 | var didCallNext = false 28 | try outerRoute.handle(request: req, response: res) { ( args : Any... ) in 29 | XCTAssertTrue(args.isEmpty, "toplevel next called w/ arguments \(args)") 30 | didCallNext = true 31 | } 32 | XCTAssertTrue (didCallRoute, "not handled by middleware") 33 | XCTAssertFalse(didCallNext, "not handled by middleware (did call next)") 34 | } 35 | 36 | func testSimpleErrorMountMatch() throws { 37 | enum SomeError: Swift.Error { 38 | case thisWentWrong 39 | } 40 | 41 | let outerRoute = Route(id: "outer") 42 | 43 | // match /admin/view 44 | var didCallErrorMiddleware = false 45 | var didCallThrowingMiddleware = false 46 | outerRoute.route(id: "outer", "/admin") 47 | .get(id: "error", "/view") { error, req, res, next in 48 | XCTAssertEqual(req.baseURL, "/admin/view", "baseURL does not match") 49 | XCTAssertEqual(req.url, "/admin/view", "HTTP URL does not match") 50 | didCallErrorMiddleware = true 51 | } 52 | .use(id: "thrower") { req, res, next in 53 | didCallThrowingMiddleware = true 54 | throw SomeError.thisWentWrong 55 | } 56 | 57 | // test 58 | 59 | let req = IncomingMessage( 60 | .init(version: .init(major: 1, minor: 1), method: .GET, 61 | uri: "/admin/view")) 62 | let res = TestServerResponse() 63 | 64 | var didCallNext = false 65 | try outerRoute.handle(request: req, response: res) { ( args : Any... ) in 66 | XCTAssertTrue(args.isEmpty, "toplevel next called w/ arguments \(args)") 67 | didCallNext = true 68 | } 69 | XCTAssertTrue (didCallThrowingMiddleware, "not handled by middleware") 70 | XCTAssertTrue (didCallErrorMiddleware, "not handled by error middleware") 71 | XCTAssertFalse(didCallNext, "not handled by middleware (did call next)") 72 | } 73 | 74 | static var allTests = [ 75 | ( "testSimpleMountMatch" , testSimpleMountMatch ), 76 | ( "testSimpleErrorMountMatch" , testSimpleErrorMountMatch ) 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /Tests/RouteTests/SimpleRouteTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MacroTestUtilities 3 | import class http.IncomingMessage 4 | @testable import express 5 | 6 | final class SimpleRouteTests: XCTestCase { 7 | 8 | func testSimpleEndingRoute() throws { 9 | let route = Route(id: "root") 10 | 11 | var didCallRoute = false 12 | route.use { req, res, next in 13 | didCallRoute = true 14 | } 15 | 16 | // test 17 | 18 | let req = IncomingMessage( 19 | .init(version: .init(major: 1, minor: 1), method: .POST, uri: "/hello")) 20 | let res = TestServerResponse() 21 | 22 | var didCallNext = false 23 | try route.handle(request: req, response: res) { ( args : Any... ) in 24 | XCTAssertTrue(args.isEmpty, "toplevel next called w/ arguments \(args)") 25 | didCallNext = true 26 | } 27 | XCTAssertTrue (didCallRoute, "not handled by middleware") 28 | XCTAssertFalse(didCallNext, "not handled by middleware (did call next)") 29 | } 30 | 31 | func testSimpleNonEndingRoute() throws { 32 | let route = Route() 33 | 34 | var didCallRoute = false 35 | route.use { req, res, next in 36 | didCallRoute = true 37 | next() 38 | } 39 | 40 | // test 41 | 42 | let req = IncomingMessage( 43 | .init(version: .init(major: 1, minor: 1), method: .POST, uri: "/hello")) 44 | let res = TestServerResponse() 45 | 46 | var didCallNext = false 47 | try route.handle(request: req, response: res) { ( args : Any... ) in 48 | XCTAssertTrue(args.isEmpty, "toplevel next called w/ arguments \(args)") 49 | didCallNext = true 50 | } 51 | XCTAssertTrue(didCallRoute, "middleware not called") 52 | XCTAssertTrue(didCallNext, "next not called as expected") 53 | } 54 | 55 | static var allTests = [ 56 | ( "testSimpleEndingRoute" , testSimpleEndingRoute ), 57 | ( "testSimpleNonEndingRoute" , testSimpleNonEndingRoute ) 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /Tests/RouteTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [ XCTestCaseEntry ] { 5 | return [ 6 | testCase(ErrorMiddlewareTests.allTests), 7 | testCase(SimpleRouteTests .allTests), 8 | testCase(RouteMountingTests .allTests) 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /Tests/bodyParserTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [ XCTestCaseEntry ] { 5 | return [ 6 | testCase(bodyParserTests.allTests) 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/bodyParserTests/bodyParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Macro 3 | @testable import http 4 | @testable import connect 5 | 6 | final class bodyParserTests: XCTestCase { 7 | 8 | override class func setUp() { 9 | super.setUp() 10 | disableAtExitHandler() 11 | } 12 | 13 | func testStringParser() throws { 14 | let req = IncomingMessage( 15 | .init(version: .init(major: 1, minor: 1), 16 | method: .POST, uri: "/post", headers: [ 17 | "Content-Type": "text/html" 18 | ]) 19 | ) 20 | 21 | req.push(Buffer("Hello World")) 22 | req.push(nil) // EOF 23 | 24 | let sem = expectation(description: "parsing body ...") 25 | 26 | // This is using pipes, and pipes need to (currently) happen on the 27 | // same eventloop. 28 | MacroCore.shared.fallbackEventLoop().execute { 29 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 30 | 31 | // this is using concat ... so we need an expectations 32 | let mw = bodyParser.text() 33 | do { 34 | try mw(req, res) { ( args : Any...) in 35 | console.log("done parsing ...", args) 36 | sem.fulfill() 37 | } 38 | } 39 | catch { 40 | sem.fulfill() 41 | } 42 | } 43 | 44 | waitForExpectations(timeout: 3) { error in 45 | if let error = error { 46 | console.log("Error:", error.localizedDescription) 47 | XCTFail("expection returned in error") 48 | } 49 | 50 | guard case .text(let value) = req.body else { 51 | XCTFail("returned value is not a text") 52 | return 53 | } 54 | 55 | XCTAssertEqual(value, "Hello World") 56 | } 57 | } 58 | 59 | func testArrayFormValueParser() throws { 60 | let req = IncomingMessage( 61 | .init(version: .init(major: 1, minor: 1), 62 | method: .POST, uri: "/post", headers: [ 63 | "Content-Type": "application/x-www-form-urlencoded" 64 | ]) 65 | ) 66 | 67 | req.push(Buffer("a[]=1&a[]=2")) 68 | req.push(nil) // EOF 69 | 70 | let sem = expectation(description: "parsing body ...") 71 | 72 | // This is using pipes, and pipes need to (currently) happen on the 73 | // same eventloop. 74 | MacroCore.shared.fallbackEventLoop().execute { 75 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 76 | 77 | // this is using concat ... so we need an expectations 78 | let mw = bodyParser.urlencoded() 79 | do { 80 | try mw(req, res) { ( args : Any...) in 81 | console.log("done parsing ...", args) 82 | sem.fulfill() 83 | } 84 | } 85 | catch { 86 | sem.fulfill() 87 | } 88 | } 89 | 90 | waitForExpectations(timeout: 3) { error in 91 | if let error = error { 92 | console.log("Error:", error.localizedDescription) 93 | XCTFail("expection returned in error") 94 | } 95 | 96 | do { 97 | guard let value = req.body.a else { 98 | XCTFail("body has no 'a' value") 99 | return 100 | } 101 | guard let values = value as? [ String ] else { 102 | XCTFail("'a' body does not have the expected [String] value") 103 | return 104 | } 105 | XCTAssertEqual(values, [ "1", "2" ]) 106 | } 107 | do { 108 | guard case .urlEncoded(let dict) = req.body else { 109 | XCTFail("returned value is not url encoded") 110 | return 111 | } 112 | 113 | guard let typedDict = dict as? [ String : [ String ] ] else { 114 | XCTFail("returned value is not the expected dict type") 115 | return 116 | } 117 | XCTAssertEqual(typedDict, [ "a": [ "1", "2" ]]) 118 | } 119 | } 120 | } 121 | 122 | static var allTests = [ 123 | ( "testStringParser" , testStringParser ), 124 | ( "testArrayFormValueParser" , testArrayFormValueParser ) 125 | ] 126 | } 127 | -------------------------------------------------------------------------------- /Tests/dotenvTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [ XCTestCaseEntry ] { 5 | return [ testCase(dotenvTests.allTests) ] 6 | } 7 | #endif 8 | -------------------------------------------------------------------------------- /Tests/dotenvTests/dotenvTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import dotenv 3 | 4 | final class dotenvTests: XCTestCase { 5 | 6 | func testConfig() { 7 | let expected = [String: String]() 8 | XCTAssertEqual(try dotenv.tryConfig(), expected) 9 | XCTAssertEqual(dotenv.config(), expected) 10 | } 11 | 12 | func testParse() { 13 | let emptyString = "" 14 | XCTAssertEqual(dotenv.parse(emptyString), 15 | [String: String](), 16 | "Empty strings return an empty dictionary") 17 | 18 | let testString = 19 | """ 20 | empty 21 | #comment - the next line is deliberately empty 22 | 23 | deja=vue 24 | bool=true 25 | int=42 26 | string=hello 27 | = 28 | deja=hello again! 29 | """ 30 | let parsed = dotenv.parse(testString) 31 | XCTAssertEqual(parsed["empty"], 32 | "", 33 | "Value of an empty key is an empty String") 34 | XCTAssertEqual(parsed["bool"], 35 | "true") 36 | XCTAssertEqual(parsed["deja"], 37 | "vuehello again!", 38 | "When the same key appears more than once its value is appended") 39 | } 40 | 41 | static var allTests = [ 42 | ( "testConfig" , testConfig ), 43 | ( "testParse" , testParse ) 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /Tests/mimeTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [ XCTestCaseEntry ] { 5 | return [ testCase(mimeTests.allTests) ] 6 | } 7 | #endif 8 | -------------------------------------------------------------------------------- /Tests/mimeTests/mimeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import mime 3 | 4 | final class mimeTests: XCTestCase { 5 | 6 | func testLookup() throws { 7 | XCTAssert(mime.lookup("json") == "application/json; charset=UTF-8") 8 | XCTAssert(mime.lookup("index.html") == "text/html; charset=UTF-8") 9 | } 10 | 11 | static var allTests = [ 12 | ( "testLookup", testLookup ), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Tests/multerTests/MultiPartParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import struct MacroCore.Buffer 3 | @testable import multer 4 | 5 | final class MultiPartParserTests: XCTestCase { 6 | 7 | func testSimpleFormData() throws { 8 | typealias fixture = Fixtures.SimpleFormData 9 | XCTAssertEqual(fixture.data[-2], 13) 10 | XCTAssertEqual(fixture.data[-1], 10) 11 | 12 | let parser = MultiPartParser(boundary: fixture.boundary) 13 | 14 | var events = [ MultiPartParser.Event ]() 15 | var expectedIdx = 0 16 | parser.write(fixture.data) { 17 | print("EVENT[\(expectedIdx)]:", $0) 18 | events.append($0) 19 | if expectedIdx < fixture.expectedEvents.count { 20 | XCTAssertEqual($0, fixture.expectedEvents[expectedIdx]) 21 | expectedIdx += 1 22 | } 23 | } 24 | parser.end { 25 | print("END EVENT:", $0) 26 | events.append($0) 27 | XCTAssert(false, "unexpected event in end") // should be empty 28 | } 29 | XCTAssert(parser.buffer?.isEmpty ?? true) 30 | 31 | print("EVENTS:") 32 | XCTAssertFalse(events.isEmpty) 33 | for event in events { 34 | print(" -", event) 35 | } 36 | 37 | XCTAssertEqual(events.count, fixture.expectedEvents.count) 38 | for i in 0..<(min(events.count, fixture.expectedEvents.count)) { 39 | XCTAssertEqual(events[i], fixture.expectedEvents[i]) 40 | } 41 | } 42 | 43 | func testSimpleFormDataFragmented() throws { 44 | typealias fixture = Fixtures.SimpleFormData 45 | XCTAssertEqual(fixture.data[-2], 13) 46 | XCTAssertEqual(fixture.data[-1], 10) 47 | 48 | let parser = MultiPartParser(boundary: fixture.boundary) 49 | 50 | var events = [ MultiPartParser.Event ]() 51 | var expectedIdx = 0 52 | 53 | func checkNextEvent(_ event: MultiPartParser.Event, isEnd: Bool = false) { 54 | print("\(isEnd ? "END-" : "")EVENT[\(expectedIdx)]:", event) 55 | events.append(event) 56 | if expectedIdx < fixture.expectedEvents.count { 57 | XCTAssertEqual(event, fixture.expectedEvents[expectedIdx]) 58 | expectedIdx += 1 59 | } 60 | } 61 | 62 | let slices = [ 63 | fixture.data.slice( 0, 1), 64 | fixture.data.slice( 1, 30), 65 | fixture.data.slice( 30, -10), 66 | fixture.data.slice(-10, -1), 67 | fixture.data.slice( -1) 68 | ] 69 | 70 | for slice in slices { 71 | parser.write(slice) { checkNextEvent($0) } 72 | } 73 | 74 | parser.end { checkNextEvent($0, isEnd: true) } 75 | XCTAssert(parser.buffer?.isEmpty ?? true) 76 | 77 | print("EVENTS:") 78 | XCTAssertFalse(events.isEmpty) 79 | for event in events { 80 | print(" -", event) 81 | } 82 | 83 | XCTAssertEqual(events.count, fixture.expectedEvents.count) 84 | for i in 0..<(min(events.count, fixture.expectedEvents.count)) { 85 | XCTAssertEqual(events[i], fixture.expectedEvents[i]) 86 | } 87 | } 88 | 89 | func testImageSubmitData() throws { 90 | typealias fixture = Fixtures.ImageSubmitData 91 | XCTAssertEqual(fixture.data[-2], 13) 92 | XCTAssertEqual(fixture.data[-1], 10) 93 | 94 | let parser = MultiPartParser(boundary: fixture.boundary) 95 | 96 | var events = [ MultiPartParser.Event ]() 97 | var expectedIdx = 0 98 | 99 | func checkNextEvent(_ event: MultiPartParser.Event, isEnd: Bool = false) { 100 | print("\(isEnd ? "END-" : "")EVENT[\(expectedIdx)]:", event) 101 | events.append(event) 102 | if expectedIdx < fixture.expectedEvents.count { 103 | XCTAssertEqual(event, fixture.expectedEvents[expectedIdx]) 104 | expectedIdx += 1 105 | } 106 | } 107 | 108 | let slices = [ 109 | fixture.data.slice( 0, 1), 110 | fixture.data.slice( 1, 30), 111 | fixture.data.slice( 30, -10), 112 | fixture.data.slice(-10, -1), 113 | fixture.data.slice( -1) 114 | ] 115 | 116 | for slice in slices { 117 | parser.write(slice) { checkNextEvent($0) } 118 | } 119 | 120 | parser.end { checkNextEvent($0, isEnd: true) } 121 | XCTAssert(parser.buffer?.isEmpty ?? true) 122 | 123 | print("EVENTS:") 124 | XCTAssertFalse(events.isEmpty) 125 | for event in events { 126 | print(" -", event) 127 | } 128 | 129 | XCTAssertEqual(events.count, fixture.expectedEvents.count) 130 | for i in 0..<(min(events.count, fixture.expectedEvents.count)) { 131 | XCTAssertEqual(events[i], fixture.expectedEvents[i]) 132 | } 133 | } 134 | 135 | func testTwoFileSubmit() throws { 136 | typealias fixture = Fixtures.TwoFilesSubmit 137 | XCTAssertEqual(fixture.data[-2], 13) 138 | XCTAssertEqual(fixture.data[-1], 10) 139 | 140 | let parser = MultiPartParser(boundary: fixture.boundary) 141 | 142 | var events = [ MultiPartParser.Event ]() 143 | var expectedIdx = 0 144 | 145 | func checkNextEvent(_ event: MultiPartParser.Event, isEnd: Bool = false) { 146 | print("\(isEnd ? "END-" : "")EVENT[\(expectedIdx)]:", event) 147 | events.append(event) 148 | if expectedIdx < fixture.expectedEvents.count { 149 | XCTAssertEqual(event, fixture.expectedEvents[expectedIdx]) 150 | expectedIdx += 1 151 | } 152 | } 153 | 154 | let slices = [ 155 | fixture.data.slice( 0, 1), 156 | fixture.data.slice( 1, 30), 157 | fixture.data.slice( 30, -10), 158 | fixture.data.slice(-10, -1), 159 | fixture.data.slice( -1) 160 | ] 161 | 162 | for slice in slices { 163 | parser.write(slice) { checkNextEvent($0) } 164 | } 165 | 166 | parser.end { checkNextEvent($0, isEnd: true) } 167 | XCTAssert(parser.buffer?.isEmpty ?? true) 168 | 169 | print("EVENTS:") 170 | XCTAssertFalse(events.isEmpty) 171 | for event in events { 172 | print(" -", event) 173 | } 174 | 175 | XCTAssertEqual(events.count, fixture.expectedEvents.count) 176 | for i in 0..<(min(events.count, fixture.expectedEvents.count)) { 177 | XCTAssertEqual(events[i], fixture.expectedEvents[i]) 178 | } 179 | } 180 | 181 | func testBiggerPayload() throws { 182 | typealias fixture = Fixtures.LargeEmptyFile 183 | XCTAssertEqual(fixture.data[-2], 13) 184 | XCTAssertEqual(fixture.data[-1], 10) 185 | 186 | let parser = MultiPartParser(boundary: fixture.boundary) 187 | 188 | var events = [ MultiPartParser.Event ]() 189 | var expectedIdx = 0 190 | 191 | func checkNextEvent(_ event: MultiPartParser.Event, isEnd: Bool = false) { 192 | events.append(event) 193 | if expectedIdx < fixture.expectedPrefixEvents.count { 194 | print("\(isEnd ? "END-" : "")EVENT[\(expectedIdx)]:", event) 195 | XCTAssertEqual(event, fixture.expectedPrefixEvents[expectedIdx]) 196 | expectedIdx += 1 197 | } 198 | } 199 | 200 | // split into 64K segments 201 | for i in stride(from: 0, to: fixture.data.count, by: 64_000) { 202 | let slice = fixture.data.slice(i, min(i + 64_000, fixture.data.count)) 203 | parser.write(slice) { checkNextEvent($0) } 204 | } 205 | XCTAssert(parser.buffer?.isEmpty ?? true) 206 | 207 | #if false 208 | print("EVENTS:") 209 | XCTAssertFalse(events.isEmpty) 210 | for event in events { 211 | print(" -", event) 212 | } 213 | #endif 214 | 215 | XCTAssert(events.count > fixture.expectedPrefixEvents.count) 216 | for i in 0..<(min(events.count, fixture.expectedPrefixEvents.count)) { 217 | XCTAssertEqual(events[i], fixture.expectedPrefixEvents[i]) 218 | } 219 | 220 | XCTAssertEqual(events.last, .endPart) 221 | 222 | print("DONE:") 223 | } 224 | 225 | static var allTests = [ 226 | ( "testSimpleFormData" , testSimpleFormData ), 227 | ( "testSimpleFormDataFragmented" , testSimpleFormDataFragmented ), 228 | ( "testImageSubmitData" , testImageSubmitData ), 229 | ( "testTwoFileSubmit" , testTwoFileSubmit ), 230 | ( "testBiggerPayload" , testBiggerPayload ) 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /Tests/multerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [ XCTestCaseEntry ] { 5 | return [ 6 | testCase(MultiPartParserTests.allTests), 7 | testCase(multerTests .allTests) 8 | ] 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Tests/multerTests/multerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MacroCore 3 | import struct NIO.ByteBuffer 4 | import http 5 | import connect 6 | @testable import multer 7 | 8 | final class multerTests: XCTestCase { 9 | 10 | override class func setUp() { 11 | super.setUp() 12 | disableAtExitHandler() 13 | } 14 | 15 | func testSimpleAny() throws { 16 | typealias fixture = Fixtures.SimpleFormData 17 | 18 | let loop = MacroCore.shared.fallbackEventLoop() 19 | let sem = expectation(description: "parsing body ...") 20 | let req = fixture.request 21 | 22 | // TBD: What is the pipe situation here? Do we really use them? 23 | loop.execute { 24 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 25 | 26 | let mw = multer().any() 27 | do { 28 | try mw(req, res) { ( args : Any...) in 29 | XCTAssertEqual(args.count, 0) 30 | sem.fulfill() 31 | } 32 | } 33 | catch { 34 | sem.fulfill() 35 | } 36 | } 37 | 38 | waitForExpectations(timeout: 3) { error in 39 | if let error = error { 40 | console.log("Error:", error.localizedDescription) 41 | XCTFail("expection returned an error") 42 | } 43 | 44 | XCTAssertEqual(req.body.count , 1) 45 | XCTAssertEqual(req.files.count , 1) 46 | 47 | do { 48 | guard let value = req.body.title else { 49 | XCTFail("body has no 'title' value") 50 | return 51 | } 52 | guard let title = value as? String else { 53 | XCTFail("'title' body does not have the expected String value") 54 | return 55 | } 56 | XCTAssertEqual(title, "file.csv") 57 | } 58 | 59 | do { 60 | guard case .urlEncoded(let dict) = req.body else { 61 | XCTFail("returned value is not url encoded") 62 | return 63 | } 64 | 65 | guard let typedDict = dict as? [ String : String ] else { 66 | XCTFail("returned value is not the expected dict type") 67 | return 68 | } 69 | XCTAssertEqual(typedDict, [ "title": "file.csv" ]) 70 | } 71 | 72 | if let file = req.files.first?.value.first { 73 | XCTAssertEqual(file.fieldName , "file") 74 | XCTAssertNil (file.filename) 75 | XCTAssertEqual(file.buffer?.count ?? 0, 0) // empty! 76 | } 77 | } 78 | } 79 | 80 | func testSimpleNoneFail() throws { 81 | typealias fixture = Fixtures.SimpleFormData 82 | 83 | let loop = MacroCore.shared.fallbackEventLoop() 84 | let sem = expectation(description: "parsing body ...") 85 | let req = fixture.request 86 | 87 | loop.execute { 88 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 89 | 90 | let mw = multer().none() 91 | do { 92 | try mw(req, res) { ( args : Any...) in 93 | if let error = args.first as? multer.MulterError { 94 | switch error { 95 | case .limitUnexpectedFile(let fieldName): 96 | XCTAssertEqual(fieldName, "file") 97 | default: 98 | XCTAssert(false, "Got a different multer error: \(error)") 99 | } 100 | } 101 | else { 102 | XCTAssert(false, "Expected a multer error, got: \(args)") 103 | } 104 | sem.fulfill() 105 | } 106 | } 107 | catch { 108 | sem.fulfill() 109 | } 110 | } 111 | 112 | waitForExpectations(timeout: 3) 113 | } 114 | 115 | func testSimpleSingleOK() throws { 116 | typealias fixture = Fixtures.SimpleFormData 117 | 118 | let loop = MacroCore.shared.fallbackEventLoop() 119 | let sem = expectation(description: "parsing body ...") 120 | let req = fixture.request 121 | 122 | loop.execute { 123 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 124 | 125 | let mw = multer().single("file") 126 | do { 127 | try mw(req, res) { ( args : Any...) in 128 | XCTAssertEqual(args.count, 0) 129 | sem.fulfill() 130 | } 131 | } 132 | catch { 133 | sem.fulfill() 134 | } 135 | } 136 | 137 | waitForExpectations(timeout: 3) { error in 138 | if let error = error { XCTFail("expection returned \(error)") } 139 | 140 | XCTAssertEqual(req.files.count , 1) 141 | if let file = req.files.first?.value.first { 142 | XCTAssertEqual(file.fieldName , "file") 143 | XCTAssertNil (file.filename) 144 | XCTAssertEqual(file.buffer?.count ?? 0, 0) // empty! 145 | } 146 | } 147 | } 148 | 149 | func testSingleFail() throws { 150 | typealias fixture = Fixtures.TwoFilesSubmit 151 | 152 | let loop = MacroCore.shared.fallbackEventLoop() 153 | let sem = expectation(description: "parsing body ...") 154 | let req = fixture.request 155 | 156 | loop.execute { 157 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 158 | 159 | let mw = multer().single("file") 160 | do { 161 | try mw(req, res) { ( args : Any...) in 162 | if let error = args.first as? multer.MulterError { 163 | switch error { 164 | case .tooManyFiles: 165 | break 166 | default: 167 | XCTAssert(false, "Got a different multer error: \(error)") 168 | } 169 | } 170 | else { 171 | XCTAssert(false, "Expected a multer error, got: \(args)") 172 | } 173 | sem.fulfill() 174 | } 175 | } 176 | catch { 177 | sem.fulfill() 178 | } 179 | } 180 | 181 | waitForExpectations(timeout: 3) 182 | } 183 | 184 | func testEmpty() throws { 185 | typealias fixture = Fixtures.EmptyFile 186 | 187 | let loop = MacroCore.shared.fallbackEventLoop() 188 | let sem = expectation(description: "parsing body ...") 189 | let req = fixture.request 190 | 191 | loop.execute { 192 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 193 | 194 | let mw = multer().array("file", 2) 195 | do { 196 | try mw(req, res) { ( args : Any...) in 197 | XCTAssertEqual(args.count, 0) 198 | sem.fulfill() 199 | } 200 | } 201 | catch { 202 | sem.fulfill() 203 | } 204 | } 205 | 206 | waitForExpectations(timeout: 3) { error in 207 | if let error = error { XCTFail("expection returned \(error)") } 208 | 209 | XCTAssertEqual(req.files.count, 1) // this still has an entry! 210 | XCTAssertEqual(req.files["file"]?.count ?? 0, 0) // but no files 211 | XCTAssertNil(req.files["file"]?.first) 212 | } 213 | } 214 | 215 | func testMultiOK() throws { 216 | typealias fixture = Fixtures.TwoFilesSubmit 217 | 218 | let loop = MacroCore.shared.fallbackEventLoop() 219 | let sem = expectation(description: "parsing body ...") 220 | let req = fixture.request 221 | 222 | loop.execute { 223 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 224 | 225 | let mw = multer().array("file", 2) 226 | do { 227 | try mw(req, res) { ( args : Any...) in 228 | XCTAssertEqual(args.count, 0) 229 | sem.fulfill() 230 | } 231 | } 232 | catch { 233 | sem.fulfill() 234 | } 235 | } 236 | 237 | waitForExpectations(timeout: 3) { error in 238 | if let error = error { XCTFail("expection returned \(error)") } 239 | 240 | XCTAssertEqual(req.files.count, 1) 241 | XCTAssertEqual(req.files["file"]?.count ?? 0, 2) 242 | 243 | if let file = req.files.first?.value.first { 244 | XCTAssertEqual(file.fieldName , "file") 245 | XCTAssertEqual(file.originalName , fixture.filenames.first) 246 | XCTAssertEqual(file.buffer?.count ?? 0, fixture.cFile.count) 247 | } 248 | 249 | if let file = req.files.first?.value.dropFirst().first { 250 | XCTAssertEqual(file.fieldName , "file") 251 | XCTAssertEqual(file.originalName , fixture.filenames.dropFirst().first) 252 | XCTAssertEqual(file.buffer?.count ?? 0, fixture.icon.count) 253 | } 254 | } 255 | } 256 | 257 | func testSizeLimit() throws { 258 | typealias fixture = Fixtures.ImageSubmitData 259 | 260 | let loop = MacroCore.shared.fallbackEventLoop() 261 | let sem = expectation(description: "parsing body ...") 262 | let req = fixture.request 263 | 264 | loop.execute { 265 | let res = ServerResponse(unsafeChannel: nil, log: req.log) 266 | 267 | var limits = multer.Limits() 268 | limits.fileSize = 1024 // 1KB 269 | 270 | let mw = multer(limits: limits).single("file") 271 | do { 272 | try mw(req, res) { ( args : Any...) in 273 | if let error = args.first as? multer.MulterError { 274 | switch error { 275 | case .fileTooLarge: 276 | break 277 | default: 278 | XCTAssert(false, "Got a different multer error: \(error)") 279 | } 280 | } 281 | else { 282 | XCTAssert(false, "Expected a multer error, got: \(args)") 283 | } 284 | sem.fulfill() 285 | } 286 | } 287 | catch { 288 | sem.fulfill() 289 | } 290 | } 291 | 292 | waitForExpectations(timeout: 3) 293 | } 294 | 295 | func testBufferRemainingMatchPerformance() { 296 | // Just make sure we don't hit cross-module surprises. 297 | let bb = Buffer(ByteBuffer(repeating: 0, count: 64 * 1024)) 298 | let needle : [ UInt8 ] = [ 30, 50, 60, 30, 40, 25, 21, 22, 23, 24, 88, 18 ] 299 | 300 | let start = Date() 301 | measure { 302 | for _ in 0..<100 { 303 | let idxMaybe = bb 304 | .indexOf(needle, options: .partialSuffixMatch) 305 | XCTAssertNotNil(idxMaybe) 306 | } 307 | } 308 | let duration = -start.timeIntervalSinceNow 309 | print("TOOK:", duration) 310 | } 311 | 312 | static var allTests = [ 313 | ( "testSimpleAny" , testSimpleAny ), 314 | ( "testSimpleNoneFail" , testSimpleNoneFail ), 315 | ( "testSimpleSingleOK" , testSimpleSingleOK ), 316 | ( "testSingleFail" , testSingleFail ), 317 | ( "testMultiOK" , testMultiOK ), 318 | ( "testSizeLimit" , testSizeLimit ), 319 | ( "testBufferRemainingMatchPerformance", 320 | testBufferRemainingMatchPerformance ) 321 | ] 322 | } 323 | --------------------------------------------------------------------------------