├── .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 = ""
121 | return ms
122 | }
123 | }
124 |
125 | public let cookies = Cookies.self
126 |
127 | // MARK: - Internals
128 |
129 | import struct Foundation.Date
130 |
131 | public struct Cookie {
132 | public let name : String
133 | public var value : String
134 | public var path : String?
135 | public var httpOnly : Bool
136 | public var domain : String?
137 | public var comment : String?
138 | public var maxAge : Int? // in seconds
139 | public var expires : Date?
140 | // let secure : Bool
141 |
142 | public init(name: String, value: String = "",
143 | path : String? = "/",
144 | httpOnly : Bool = true,
145 | domain : String? = nil,
146 | comment : String? = nil,
147 | maxAge : Int? = nil,
148 | expires : Date? = nil)
149 | {
150 | self.name = name
151 | self.value = value
152 | self.path = path
153 | self.httpOnly = httpOnly
154 | self.domain = domain
155 | self.comment = comment
156 | self.maxAge = maxAge
157 | self.expires = expires
158 | }
159 | }
160 |
161 | import struct xsys.time_t
162 |
163 | public extension Cookie {
164 |
165 | var httpHeaderValue: String {
166 | // TODO: quoting
167 | var s = "\(name)=\(value)"
168 |
169 | if let v = path { s += "; Path=\(v)" }
170 | if let v = domain { s += "; Domain=\(v)" }
171 | if let v = comment { s += "; Comment=\(v)" }
172 | if let v = maxAge { s += "; Max-Age=\(v)" }
173 |
174 | if let v = expires {
175 | s += "; expires="
176 |
177 | func generateDateHeader(timestamp ts: time_t) -> 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 |
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 = "" }
100 |
101 | if !originalName.isEmpty { ms += " filename=\(originalName)" }
102 | if !mimeType .isEmpty { ms += " type=\(mimeType)" }
103 | if let path = path { ms += " local=\(path)" }
104 | if let buffer = buffer { ms += " contents=\(buffer)" }
105 | return ms
106 | }
107 | }
108 |
109 | #if canImport(Foundation)
110 |
111 | import struct Foundation.URL
112 |
113 | public extension multer.File {
114 |
115 | /**
116 | * The name of the file on the local filesystem, if available. I.e. when
117 | * used together with the disk storage.
118 | */
119 | @inlinable
120 | var filename : String? {
121 | guard let path = path else { return nil }
122 | return URL(fileURLWithPath: path).lastPathComponent
123 | }
124 |
125 | /**
126 | * The folder of the file in the local filesystem, if available. I.e. when
127 | * used together with the disk storage.
128 | */
129 | @inlinable
130 | var destination : String? {
131 | guard let path = path else { return nil }
132 | return URL(fileURLWithPath: path).deletingLastPathComponent().path
133 | }
134 | }
135 | #endif // canImport(Foundation)
136 |
--------------------------------------------------------------------------------
/Sources/multer/IncomingMessageMulter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IncomingMessageMulter.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 protocol MacroCore.EnvironmentKey
10 | import class http.IncomingMessage
11 |
12 | extension multer {
13 |
14 | enum FilesKey: EnvironmentKey {
15 | static let defaultValue : [ String : [ multer.File ] ]? = nil
16 | static let loggingKey = "files"
17 | }
18 | }
19 |
20 | public extension IncomingMessage {
21 |
22 | /**
23 | * The parsed files keyed by form field name. Each field can contain multiple
24 | * files!
25 | */
26 | var files : [ String : [ multer.File ] ] {
27 | set { environment[multer.FilesKey.self] = newValue }
28 | get { return environment[multer.FilesKey.self] ?? [:] }
29 | }
30 |
31 | /**
32 | * Returns the first parsed file.
33 | */
34 | var file : multer.File? {
35 | // TBD: Own key or not?
36 | get { return environment[multer.FilesKey.self]?.first?.value.first }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/multer/Limits.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Limits.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 | public extension multer {
10 |
11 | /**
12 | * Upload limits configuration for multer.
13 | *
14 | * This allows to configure file size or count limits. And various other
15 | * limits.
16 | */
17 | struct Limits {
18 |
19 | /// Maximum size of field names (100 bytes)
20 | public var fieldNameSize : Int? = 100
21 |
22 | /// Maximum size of field value (1MB)
23 | public var fieldSize : Int? = 1024 * 1024
24 |
25 | /// Maximum number of non-file fields (unlimited)
26 | public var fields : Int? = nil
27 |
28 | /// Max file size (unlimited)
29 | public var fileSize : Int? = nil
30 |
31 | /// Maximum number of file fields (unlimited)
32 | public var files : Int? = nil
33 |
34 | /// Maximum number of header fields (2000)
35 | /// Note: This is not checked yet.
36 | public var headerPairs : Int? = 2000
37 |
38 | /**
39 | * Create a new multer limits configuration.
40 | *
41 | * All parameters are optional, defaults:
42 | * - field name size: 100
43 | * - field size: 1MB
44 | * - header pairs: 2000
45 | *
46 | * - Parameters:
47 | * - fieldNameSize: Maximum size of field names (100 bytes)
48 | * - fieldSize: Maximum size of field value (1MB)
49 | * - fields: Maximum number of non-file fields (unlimited)
50 | * - fileSize: Max file size (unlimited)
51 | * - files: Maximum number of file fields (unlimited)
52 | * - headerPairs: Maximum number of header fields (2000)
53 | */
54 | public init(fieldNameSize : Int? = 100,
55 | fieldSize : Int? = 1024 * 1024,
56 | fields : Int? = nil,
57 | fileSize : Int? = nil,
58 | files : Int? = nil,
59 | headerPairs : Int? = 2000)
60 | {
61 | self.fieldNameSize = fieldNameSize
62 | self.fieldSize = fieldSize
63 | self.fields = fields
64 | self.fileSize = fileSize
65 | self.files = files
66 | self.headerPairs = headerPairs
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/multer/MemoryStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MemoryStorage.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 | extension multer {
12 |
13 | /**
14 | * A ``MulterStorage`` that writes the file contents to the ``File/buffer``
15 | * property of the ``File`` object, i.e. stores it in-memory.
16 | */
17 | open class MemoryStorage: MulterStorage {
18 | // TBD: does this have to be a class?
19 |
20 | public init() {}
21 |
22 | // MARK: - Storage API
23 |
24 | @inlinable
25 | public func startFile(_ file: multer.File, in ctx: MulterStorageContext) {}
26 | @inlinable
27 | public func endFile (_ file: multer.File, in ctx: MulterStorageContext) {}
28 |
29 | @inlinable
30 | public func write(_ data: Buffer, to file: multer.File,
31 | in ctx: MulterStorageContext) throws
32 | {
33 | if let v = ctx.config.limits.fileSize {
34 | let newSize = (file.buffer?.count ?? 0) + data.count
35 | guard newSize <= v else {
36 | return ctx.handleError(MulterError.fileTooLarge)
37 | }
38 | }
39 |
40 | if nil == file.buffer?.append(data) { file.buffer = data }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/multer/Middleware.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Middleware.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 | import http
11 | import connect
12 |
13 | public extension multer {
14 |
15 | /**
16 | * Parse `multipart/form-data` form fields with a set of restrictions.
17 | *
18 | * There are multiple convenience methods to restrict the set of fields to
19 | * accept:
20 | * - ``single(_:)``: accept just one file for the specified name
21 | * - ``array(_:_:)``: accept just multiple files for the specified name
22 | * - ``none()``: accept no file, just form regular fields
23 | * - ``any()``: accept all files, careful!
24 | *
25 | * All convenience methods call into this middleware.
26 | *
27 | * ### Restrictions
28 | *
29 | * If not restrictions are set, only the limits in the `multer` config apply.
30 | * If restrictions are set, an incoming file part MUST be listed, otherwise
31 | * the middleware will fail with an error.
32 | *
33 | *
34 | * - Parameter fields: An optional set of restrictions on fields containing
35 | * files.
36 | * - Returns: The middleware to parse the form data.
37 | */
38 | func fields(_ fields: [ ( fieldName: String, maxCount: Int? ) ]?)
39 | -> 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 | *
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 |
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 |
--------------------------------------------------------------------------------