├── .editorconfig
├── .github
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── main.yml
│ └── pull_request.yml
├── .gitignore
├── .licenseignore
├── .swift-format
├── .swiftformatignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── NIOSMTP
├── .gitignore
├── NIOSMTP.xcodeproj
│ └── project.pbxproj
├── NIOSMTP
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Configuration.swift
│ ├── DataModel.swift
│ ├── Info.plist
│ ├── PrintEverythingHandler.swift
│ ├── SMTPRequestEncoder.swift
│ ├── SMTPResponseDecoder.swift
│ ├── SendEmailHandler.swift
│ └── ViewController.swift
├── README.md
└── build.sh
├── README.md
├── TLSify
├── .gitignore
├── Package.swift
├── README.md
└── Sources
│ ├── TLSify
│ └── main.swift
│ └── TLSifyLib
│ ├── CloseOnErrorHandler.swift
│ ├── GlueHandler.swift
│ └── TLSProxy.swift
├── UniversalBootstrapDemo
├── .gitignore
├── Package.swift
├── README.md
└── Sources
│ └── UniversalBootstrapDemo
│ ├── EventLoopGroupManager.swift
│ ├── ExampleHTTPLibrary.swift
│ └── main.swift
├── backpressure-file-io-channel
├── .gitignore
├── Package.swift
├── README.md
├── Sources
│ ├── BackpressureChannelToFileIO
│ │ ├── FileIOChannelWriteCoordinator.swift
│ │ └── SaveEverythingHTTPServer.swift
│ └── BackpressureChannelToFileIODemo
│ │ └── main.swift
└── Tests
│ └── BackpressureChannelToFileIOTests
│ ├── IntegrationTest.swift
│ └── StateMachineTest.swift
├── connect-proxy
├── .gitignore
├── Package.swift
└── Sources
│ └── ConnectProxy
│ ├── ConnectHandler.swift
│ ├── ConnectProxyError.swift
│ ├── GlueHandler.swift
│ └── main.swift
├── dev
├── build_all.sh
└── git.commit.template
├── http-responsiveness-server
├── Package.swift
└── Sources
│ └── HTTPResponsivenessServer
│ └── main.swift
├── http2-client
├── .gitignore
├── Package.swift
├── Sources
│ └── http2-client
│ │ ├── Types.swift
│ │ └── main.swift
└── scripts
│ └── test_top_sites.sh
├── http2-server
├── .gitignore
├── Package.swift
├── README.md
└── Sources
│ └── http2-server
│ ├── HardcodedPrivateKeyAndCerts.swift
│ └── main.swift
├── json-rpc
├── .gitignore
├── Package.swift
├── README.md
├── Sources
│ ├── ClientExample
│ │ └── main.swift
│ ├── JsonRpc
│ │ ├── Client.swift
│ │ ├── Codec.swift
│ │ ├── Model.swift
│ │ ├── Server.swift
│ │ └── Utils.swift
│ ├── LightsdDemo
│ │ └── main.swift
│ └── ServerExample
│ │ └── main.swift
└── Tests
│ ├── JsonRpcTests
│ ├── JsonRpcTests.swift
│ └── XCTestManifests.swift
│ └── LinuxMain.swift
└── nio-launchd
├── .gitignore
├── Package.swift
├── README.md
└── Sources
└── nio-launchd
├── Client.swift
├── Server.swift
└── main.swift
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Expected behavior
2 | _[what you expected to happen]_
3 |
4 | ### Actual behavior
5 | _[what actually happened]_
6 |
7 | ### Steps to reproduce
8 |
9 | 1. ...
10 | 2. ...
11 |
12 | ### If possible, minimal yet complete reproducer code (or URL to code)
13 |
14 | _[anything to help us reproducing the issue]_
15 |
16 | ### version/commit hashes from all involved dependencies
17 |
18 | _[tag/commit hash]_
19 |
20 | ### Swift & OS version (output of `swift --version && uname -a`)
21 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | _[One line description of your change]_
2 |
3 | ### Motivation:
4 |
5 | _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_
6 |
7 | ### Modifications:
8 |
9 | _[Describe the modifications you've done.]_
10 |
11 | ### Result:
12 |
13 | _[After your change, what will change.]_
14 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | schedule:
7 | - cron: "0 8,20 * * *"
8 |
9 | jobs:
10 | soundness:
11 | name: Soundness
12 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
13 | with:
14 | license_header_check_project_name: "SwiftNIO"
15 | docs_check_enabled: false
16 | api_breakage_check_enabled: false
17 |
18 | construct-build-test-matrix:
19 | name: Construct build matrix
20 | runs-on: ubuntu-latest
21 | outputs:
22 | build-test-matrix: '${{ steps.generate-matrix.outputs.build-test-matrix }}'
23 | steps:
24 | - name: Checkout repository
25 | uses: actions/checkout@v4
26 | with:
27 | persist-credentials: false
28 | - id: generate-matrix
29 | run: echo "build-test-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT"
30 | env:
31 | MATRIX_LINUX_COMMAND: STRICT_CONCURRENCY=true SWIFT_PACKAGE_DIRECTORIES='TLSify UniversalBootstrapDemo http-responsiveness-server connect-proxy http2-client http2-server json-rpc nio-launchd' dev/build_all.sh && SWIFT_PACKAGE_DIRECTORIES='backpressure-file-io-channel' dev/build_all.sh
32 |
33 | build-tests:
34 | name: Builds
35 | needs: construct-build-test-matrix
36 | uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main
37 | with:
38 | name: "Builds"
39 | matrix_string: '${{ needs.construct-build-test-matrix.outputs.build-test-matrix }}'
40 |
--------------------------------------------------------------------------------
/.github/workflows/pull_request.yml:
--------------------------------------------------------------------------------
1 | name: PR
2 |
3 | on:
4 | pull_request:
5 | types: [opened, reopened, synchronize]
6 |
7 | jobs:
8 | soundness:
9 | name: Soundness
10 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
11 | with:
12 | license_header_check_project_name: "SwiftNIO"
13 | docs_check_enabled: false
14 | api_breakage_check_enabled: false
15 |
16 | construct-build-test-matrix:
17 | name: Construct build matrix
18 | runs-on: ubuntu-latest
19 | outputs:
20 | build-test-matrix: '${{ steps.generate-matrix.outputs.build-test-matrix }}'
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v4
24 | with:
25 | persist-credentials: false
26 | - id: generate-matrix
27 | run: echo "build-test-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT"
28 | env:
29 | MATRIX_LINUX_COMMAND: STRICT_CONCURRENCY=true SWIFT_PACKAGE_DIRECTORIES='TLSify UniversalBootstrapDemo http-responsiveness-server connect-proxy http2-client http2-server json-rpc nio-launchd' dev/build_all.sh && SWIFT_PACKAGE_DIRECTORIES='backpressure-file-io-channel' dev/build_all.sh
30 |
31 | build-tests:
32 | name: Build tests
33 | needs: construct-build-test-matrix
34 | uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main
35 | with:
36 | name: "Build tests"
37 | matrix_string: '${{ needs.construct-build-test-matrix.outputs.build-test-matrix }}'
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .build/
2 | .swiftpm/
3 | .DS_Store
4 | Package.resolved
5 | NIOSMTP/NIOSMTP.xcodeproj/xcshareddata
6 |
--------------------------------------------------------------------------------
/.licenseignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | **/.gitignore
3 | .licenseignore
4 | .gitattributes
5 | .git-blame-ignore-revs
6 | .mailfilter
7 | .mailmap
8 | .spi.yml
9 | .swift-format
10 | .editorconfig
11 | .github/*
12 | *.md
13 | *.txt
14 | *.yml
15 | *.yaml
16 | *.json
17 | Package.swift
18 | **/Package.swift
19 | Package@-*.swift
20 | **/Package@-*.swift
21 | Package.resolved
22 | **/Package.resolved
23 | Makefile
24 | *.modulemap
25 | **/*.modulemap
26 | **/*.docc/*
27 | *.xcprivacy
28 | **/*.xcprivacy
29 | *.symlink
30 | **/*.symlink
31 | Dockerfile
32 | **/Dockerfile
33 | Snippets/*
34 | NIOSMTP/NIOSMTP.xcodeproj/project.pbxproj
35 | NIOSMTP/NIOSMTP/Base.lproj/LaunchScreen.storyboard
36 | NIOSMTP/NIOSMTP/Base.lproj/Main.storyboard
37 | NIOSMTP/build.sh
38 | dev/git.commit.template
39 | .swiftformatignore
40 |
--------------------------------------------------------------------------------
/.swift-format:
--------------------------------------------------------------------------------
1 | {
2 | "version" : 1,
3 | "indentation" : {
4 | "spaces" : 4
5 | },
6 | "tabWidth" : 4,
7 | "fileScopedDeclarationPrivacy" : {
8 | "accessLevel" : "private"
9 | },
10 | "spacesAroundRangeFormationOperators" : false,
11 | "indentConditionalCompilationBlocks" : false,
12 | "indentSwitchCaseLabels" : false,
13 | "lineBreakAroundMultilineExpressionChainComponents" : false,
14 | "lineBreakBeforeControlFlowKeywords" : false,
15 | "lineBreakBeforeEachArgument" : true,
16 | "lineBreakBeforeEachGenericRequirement" : true,
17 | "lineLength" : 120,
18 | "maximumBlankLines" : 1,
19 | "respectsExistingLineBreaks" : true,
20 | "prioritizeKeepingFunctionOutputTogether" : true,
21 | "rules" : {
22 | "AllPublicDeclarationsHaveDocumentation" : false,
23 | "AlwaysUseLiteralForEmptyCollectionInit" : false,
24 | "AlwaysUseLowerCamelCase" : false,
25 | "AmbiguousTrailingClosureOverload" : true,
26 | "BeginDocumentationCommentWithOneLineSummary" : false,
27 | "DoNotUseSemicolons" : true,
28 | "DontRepeatTypeInStaticProperties" : true,
29 | "FileScopedDeclarationPrivacy" : true,
30 | "FullyIndirectEnum" : true,
31 | "GroupNumericLiterals" : true,
32 | "IdentifiersMustBeASCII" : true,
33 | "NeverForceUnwrap" : false,
34 | "NeverUseForceTry" : false,
35 | "NeverUseImplicitlyUnwrappedOptionals" : false,
36 | "NoAccessLevelOnExtensionDeclaration" : true,
37 | "NoAssignmentInExpressions" : true,
38 | "NoBlockComments" : true,
39 | "NoCasesWithOnlyFallthrough" : true,
40 | "NoEmptyTrailingClosureParentheses" : true,
41 | "NoLabelsInCasePatterns" : true,
42 | "NoLeadingUnderscores" : false,
43 | "NoParensAroundConditions" : true,
44 | "NoVoidReturnOnFunctionSignature" : true,
45 | "OmitExplicitReturns" : true,
46 | "OneCasePerLine" : true,
47 | "OneVariableDeclarationPerLine" : true,
48 | "OnlyOneTrailingClosureArgument" : true,
49 | "OrderedImports" : true,
50 | "ReplaceForEachWithForLoop" : true,
51 | "ReturnVoidInsteadOfEmptyTuple" : true,
52 | "UseEarlyExits" : false,
53 | "UseExplicitNilCheckInConditions" : false,
54 | "UseLetInEveryBoundCaseVariable" : false,
55 | "UseShorthandTypeNames" : true,
56 | "UseSingleLinePropertyGetter" : false,
57 | "UseSynthesizedInitializer" : false,
58 | "UseTripleSlashForDocumentationComments" : true,
59 | "UseWhereClausesInForLoops" : false,
60 | "ValidateDocumentationComments" : false
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/.swiftformatignore:
--------------------------------------------------------------------------------
1 | **Package.swift
2 | json-rpc/Tests/JsonRpcTests/XCTestManifests.swift
3 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct.
4 |
5 |
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Legal
2 |
3 | By submitting a pull request, you represent that you have the right to license
4 | your contribution to Apple and the community, and agree by submitting the patch
5 | that your contributions are licensed under the Apache 2.0 license (see
6 | `LICENSE.txt`).
7 |
8 |
9 | ## How to submit a bug report
10 |
11 | Please ensure to specify the following:
12 |
13 | * all relevant commit hashes
14 | * Contextual information (e.g. what you were trying to achieve and which example
15 | you tried to use)
16 | * Simplest possible steps to reproduce
17 | * More complex the steps are, lower the priority will be.
18 | * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description.
19 | * Anything that might be relevant in your opinion, such as:
20 | * Swift version or the output of `swift --version`
21 | * OS version and the output of `uname -a`
22 | * Network configuration
23 |
24 |
25 | ### Example
26 |
27 | ```
28 | - swift-nio commit hash: 22ec043dc9d24bb011b47ece4f9ee97ee5be2757
29 | - swift-nio-examples commit hash: 49d0fd7c1036993dfd538a34cf042d4d24a9792d
30 |
31 | Context:
32 | When running NIOSMTP with an illegal email address, the app crashes.
33 |
34 | Steps to reproduce:
35 | 1. ...
36 | 2. ...
37 | 3. ...
38 | 4. ...
39 |
40 | $ swift --version
41 | Swift version 4.2.0 (swift-4.2.0-RELEASE)
42 | Target: x86_64-unknown-linux-gnu
43 |
44 | Operating system: Ubuntu Linux 18.04 64-bit
45 |
46 | $ uname -a
47 | Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Sep 10 18:29:59 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
48 |
49 | My system has IPv6 disabled.
50 | ```
51 |
52 | ## Writing a Patch
53 |
54 | A good patch is:
55 |
56 | 1. Concise, and contains as few changes as needed to achieve the end result.
57 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it.
58 | 3. Documented, adding API documentation as needed to cover new functions and properties.
59 | 4. Accompanied by a great commit message, using our commit message template.
60 |
61 | ### Commit Message Template
62 |
63 | We require that your commit messages match our template. The easiest way to do that is to get git to help you by explicitly using the template. To do that, `cd` to the root of our repository and run:
64 |
65 | git config commit.template dev/git.commit.template
66 |
67 | ## How to contribute your work
68 |
69 | Please open a pull request at https://github.com/apple/swift-nio-examples and wait for the code review. If you don't receive any within a few days, please kindly
70 | remind the core SwiftNIO team.
71 |
--------------------------------------------------------------------------------
/NIOSMTP/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Xcode
4 | build/
5 | *.pbxuser
6 | !default.pbxuser
7 | *.mode1v3
8 | !default.mode1v3
9 | *.mode2v3
10 | !default.mode2v3
11 | *.perspectivev3
12 | !default.perspectivev3
13 | *.xcworkspace
14 | !default.xcworkspace
15 | xcuserdata
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | .idea/
20 |
21 | # CocoaPods
22 | Pods
23 | Podfile.lock
24 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import UIKit
16 |
17 | @UIApplicationMain
18 | class AppDelegate: UIResponder, UIApplicationDelegate {
19 |
20 | var window: UIWindow?
21 |
22 | func application(
23 | _ application: UIApplication,
24 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
25 | ) -> Bool {
26 |
27 | true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/Configuration.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import NIOTransportServices
16 |
17 | struct Configuration {
18 | static let shared: Configuration = {
19 | // If you don't want to use your real SMTP server, do try out https://mailtrap.io they offer you an
20 | // SMTP server that can be used for testing for free.
21 | let serverConfig = ServerConfiguration(
22 | hostname: "you.need.to.configure.your.providers.smtp.server",
23 | port: 25,
24 | username: "put your username here",
25 | password: "and your password goes here",
26 | tlsConfiguration: .startTLS
27 | )
28 |
29 | precondition(
30 | serverConfig.hostname != "you.need.to.configure.your.providers.smtp.server",
31 | "You need to configure an SMTP server in code."
32 | )
33 |
34 | return Configuration(serverConfig: serverConfig)
35 | }()
36 |
37 | var serverConfig: ServerConfiguration
38 | }
39 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/DataModel.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | enum SMTPRequest {
16 | case sayHello(serverName: String)
17 | case startTLS
18 | case beginAuthentication
19 | case authUser(String)
20 | case authPassword(String)
21 | case mailFrom(String)
22 | case recipient(String)
23 | case data
24 | case transferData(Email)
25 | case quit
26 | }
27 |
28 | enum SMTPResponse {
29 | case ok(Int, String)
30 | case error(String)
31 | }
32 |
33 | struct ServerConfiguration {
34 | enum TLSConfiguration {
35 | /// Use StartTLS, this should be the default and is secure.
36 | case startTLS
37 |
38 | /// Directly open a TLS connection. This secure however not widely supported.
39 | case regularTLS
40 |
41 | /// This should never be used. It will literally _SEND YOUR PASSWORD IN PLAINTEXT OVER THE INTERNET_.
42 | case insecureNoTLS
43 | }
44 | var hostname: String
45 | var port: Int
46 | var username: String
47 | var password: String
48 | var tlsConfiguration: TLSConfiguration
49 | }
50 |
51 | struct Email {
52 | var senderName: String?
53 | var senderEmail: String
54 |
55 | var recipientName: String?
56 | var recipientEmail: String
57 |
58 | var subject: String
59 |
60 | var body: String
61 | }
62 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/PrintEverythingHandler.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 | import NIOCore
17 | import NIOTransportServices
18 |
19 | final class PrintEverythingHandler: ChannelDuplexHandler {
20 | typealias InboundIn = ByteBuffer
21 | typealias InboundOut = ByteBuffer
22 | typealias OutboundIn = ByteBuffer
23 | typealias OutboundOut = ByteBuffer
24 |
25 | private let handler: (String) -> Void
26 |
27 | init(handler: @escaping (String) -> Void) {
28 | self.handler = handler
29 | }
30 |
31 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
32 | let buffer = self.unwrapInboundIn(data)
33 | self.handler("☁️ \(String(decoding: buffer.readableBytesView, as: UTF8.self))")
34 | context.fireChannelRead(data)
35 | }
36 |
37 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) {
38 | let buffer = self.unwrapOutboundIn(data)
39 | if buffer.readableBytesView.starts(
40 | with: Data(Configuration.shared.serverConfig.password.utf8).base64EncodedData()
41 | ) {
42 | self.handler("📱 \r\n")
43 | } else {
44 | self.handler("📱 \(String(decoding: buffer.readableBytesView, as: UTF8.self))")
45 | }
46 | context.write(data, promise: promise)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/SMTPRequestEncoder.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 | import NIOCore
17 | import NIOFoundationCompat
18 |
19 | final class SMTPRequestEncoder: MessageToByteEncoder, Sendable {
20 | typealias OutboundIn = SMTPRequest
21 |
22 | func encode(data: SMTPRequest, out: inout ByteBuffer) throws {
23 | switch data {
24 | case .sayHello(serverName: let server):
25 | out.writeString("EHLO \(server)")
26 | case .startTLS:
27 | out.writeString("STARTTLS")
28 | case .mailFrom(let from):
29 | out.writeString("MAIL FROM:<\(from)>")
30 | case .recipient(let rcpt):
31 | out.writeString("RCPT TO:<\(rcpt)>")
32 | case .data:
33 | out.writeString("DATA")
34 | case .transferData(let email):
35 | let date = Date()
36 | let dateFormatter = DateFormatter()
37 | dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
38 | let dateFormatted = dateFormatter.string(from: date)
39 |
40 | out.writeString("From: \(formatMIME(emailAddress: email.senderEmail, name: email.senderName))\r\n")
41 | out.writeString("To: \(formatMIME(emailAddress: email.recipientEmail, name: email.recipientName))\r\n")
42 | out.writeString("Date: \(dateFormatted)\r\n")
43 | out.writeString("Message-ID: <\(date.timeIntervalSince1970)\(email.senderEmail.drop { $0 != "@" })>\r\n")
44 | out.writeString("Subject: \(email.subject)\r\n\r\n")
45 | out.writeString(email.body)
46 | out.writeString("\r\n.")
47 | case .quit:
48 | out.writeString("QUIT")
49 | case .beginAuthentication:
50 | out.writeString("AUTH LOGIN")
51 | case .authUser(let user):
52 | let userData = Data(user.utf8)
53 | out.writeBytes(userData.base64EncodedData())
54 | case .authPassword(let password):
55 | let passwordData = Data(password.utf8)
56 | out.writeBytes(passwordData.base64EncodedData())
57 | }
58 |
59 | out.writeString("\r\n")
60 | }
61 |
62 | func formatMIME(emailAddress: String, name: String?) -> String {
63 | if let name = name {
64 | return "\(name) <\(emailAddress)>"
65 | } else {
66 | return emailAddress
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/SMTPResponseDecoder.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import NIOCore
16 |
17 | /// `SMTPResponseDecoder` decodes exactly one SMTP response from already newline-framed input messages.
18 | ///
19 | /// To use `SMTPResponseDecoder` you must insert a handler that does the newline based framing in front of the
20 | /// `SMTPResponseDecoder`. Usually, you would insert `LineBasedFrameDecoder` immediately followed by
21 | /// `SMTPResponseDecoder` into the `ChannelPipeline` to do the job.
22 | ///
23 | /// ### Example
24 | ///
25 | /// For example, the following threee incoming events, will be decoded into exactly one
26 | /// `.SMTPResponse.ok(250, "okay")`:
27 | ///
28 | /// 1. `250-smtp.foo.com at your service`
29 | /// 2. `250-SIZE 35882577`
30 | /// 3. `250 okay`
31 | ///
32 | /// On the TCP level, those three messages will have arrived in one of more TCP packets (and also separated by `\r\n`).
33 | /// `LineBasedFrameDecoder` then took care of the framing and forwarded them as three separate events.
34 | ///
35 | /// The reason that those three incoming events only produce only one outgoing event is because the first two are
36 | /// partial SMTP responses (starting with `250-`). The last message always ends with a space character after the
37 | /// response code (`250 `).
38 | ///
39 | /// ### Why is `SMTPResponseDecoder` not a `ByteToMessageDecoder`?
40 | ///
41 | /// On a first look, `SMTPResponseDecoder` looks like a great candidate to be a `ByteToMessageDecoder` and yet it is
42 | /// not one. The reason is that `ByteToMessageDecoder`s are great if the input is a _stream of bytes_ which means
43 | /// that the incoming framing of the messages has no meaning because they are arbitrary chunks of a TCP stream.
44 | ///
45 | /// `SMTPResponseDecoder`'s job is actually simpler because it expects its inbound messages to be already framed. The
46 | /// framing for SMTP is based on newlines (`\r\n`) and `SMTPResponseDecoder` expects to receive exactly one line at a
47 | /// time. That is usually achieved by inserting `SMTPResponseDecoder` right after a `LineBasedFrameDecoder` into the
48 | /// `ChannelPipeline`. That is quite nice because we separate the concerns quite nicely: `LineBasedFrameDecoder` does
49 | /// only the newline-based framing and `SMTPResponseDecoder` just decodes pre-framed SMTP responses.
50 | final class SMTPResponseDecoder: ChannelInboundHandler, Sendable {
51 | typealias InboundIn = ByteBuffer
52 | typealias InboundOut = SMTPResponse
53 |
54 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
55 | var response = self.unwrapInboundIn(data)
56 |
57 | if let firstFourBytes = response.readString(length: 4), let code = Int(firstFourBytes.dropLast()) {
58 | let remainder = response.readString(length: response.readableBytes) ?? ""
59 |
60 | let firstCharacter = firstFourBytes.first!
61 | let fourthCharacter = firstFourBytes.last!
62 |
63 | switch (firstCharacter, fourthCharacter) {
64 | case ("2", " "),
65 | ("3", " "):
66 | let parsedMessage = SMTPResponse.ok(code, remainder)
67 | context.fireChannelRead(self.wrapInboundOut(parsedMessage))
68 | case (_, "-"):
69 | () // intermediate message, ignore
70 | default:
71 | context.fireChannelRead(self.wrapInboundOut(.error(firstFourBytes + remainder)))
72 | }
73 | } else {
74 | context.fireErrorCaught(SMTPResponseDecoderError.malformedMessage)
75 | }
76 | }
77 | }
78 |
79 | enum SMTPResponseDecoderError: Error {
80 | case malformedMessage
81 | }
82 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/SendEmailHandler.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import NIOCore
16 | import NIOSSL
17 | import UIKit
18 |
19 | private let sslContext = try! NIOSSLContext(configuration: TLSConfiguration.makeClientConfiguration())
20 |
21 | final class SendEmailHandler: ChannelInboundHandler {
22 | typealias InboundIn = SMTPResponse
23 | typealias OutboundIn = Email
24 | typealias OutboundOut = SMTPRequest
25 |
26 | enum Expect {
27 | case initialMessageFromServer
28 | case okForOurHello
29 | case okForStartTLS
30 | case tlsHandlerToBeAdded
31 | case okForOurAuthBegin
32 | case okAfterUsername
33 | case okAfterPassword
34 | case okAfterMailFrom
35 | case okAfterRecipient
36 | case okAfterDataCommand
37 | case okAfterMailData
38 | case okAfterQuit
39 | case nothing
40 |
41 | case error(Error)
42 | }
43 |
44 | private var currentlyWaitingFor = Expect.initialMessageFromServer {
45 | didSet {
46 | if case .error(let error) = self.currentlyWaitingFor {
47 | self.allDonePromise.fail(error)
48 | }
49 | }
50 | }
51 | private let email: Email
52 | private let serverConfiguration: ServerConfiguration
53 | private let allDonePromise: EventLoopPromise
54 | private var useStartTLS: Bool {
55 | if case .startTLS = self.serverConfiguration.tlsConfiguration {
56 | return true
57 | } else {
58 | return false
59 | }
60 | }
61 |
62 | init(configuration: ServerConfiguration, email: Email, allDonePromise: EventLoopPromise) {
63 | self.email = email
64 | self.allDonePromise = allDonePromise
65 | self.serverConfiguration = configuration
66 | }
67 |
68 | func send(context: ChannelHandlerContext, command: SMTPRequest) {
69 | context.writeAndFlush(self.wrapOutboundOut(command)).cascadeFailure(to: self.allDonePromise)
70 | }
71 |
72 | func sendAuthenticationStart(context: ChannelHandlerContext) {
73 | func goAhead() {
74 | self.send(context: context, command: .beginAuthentication)
75 | self.currentlyWaitingFor = .okForOurAuthBegin
76 | }
77 |
78 | switch self.serverConfiguration.tlsConfiguration {
79 | case .regularTLS, .startTLS:
80 | // Let's make sure we actually have a TLS handler. This code is here purely to make sure we don't have a
81 | // bug in the code base that would lead to sending any sensitive data without TLS (unless the user asked
82 | // us to do so.)
83 | context.channel.pipeline.handler(type: NIOSSLClientHandler.self).map { (_: NIOSSLClientHandler) in
84 | // we don't actually care about the NIOSSLClientHandler but we must be sure it's there.
85 | goAhead()
86 | }.whenFailure { error in
87 | if NetworkImplementation.best == .transportServices
88 | && self.serverConfiguration.tlsConfiguration == .regularTLS
89 | {
90 | // If we're using NIOTransportServices and regular TLS, then TLS must have been configured ahead
91 | // of time, we can't check it here.
92 | } else {
93 | preconditionFailure(
94 | "serious NIOSMTP bug: TLS handler should be present in "
95 | + "\(self.serverConfiguration.tlsConfiguration) but SSL handler \(error)"
96 | )
97 | }
98 | }
99 | case .insecureNoTLS:
100 | // sad times here, plaintext
101 | goAhead()
102 | }
103 | }
104 |
105 | func channelInactive(context: ChannelHandlerContext) {
106 | self.allDonePromise.fail(ChannelError.eof)
107 | }
108 |
109 | func errorCaught(context: ChannelHandlerContext, error: Error) {
110 | self.currentlyWaitingFor = .error(error)
111 | self.allDonePromise.fail(error)
112 | context.close(promise: nil)
113 | }
114 |
115 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
116 | let result = self.unwrapInboundIn(data)
117 | switch result {
118 | case .error(let message):
119 | self.allDonePromise.fail(NSError(domain: "sending email", code: 1, userInfo: ["reason": message]))
120 | return
121 | case .ok:
122 | () // cool
123 | }
124 |
125 | switch self.currentlyWaitingFor {
126 | case .initialMessageFromServer:
127 | self.send(context: context, command: .sayHello(serverName: self.serverConfiguration.hostname))
128 | self.currentlyWaitingFor = .okForOurHello
129 | case .okForOurHello:
130 | if self.useStartTLS {
131 | self.send(context: context, command: .startTLS)
132 | self.currentlyWaitingFor = .okForStartTLS
133 | } else {
134 | self.sendAuthenticationStart(context: context)
135 | }
136 | case .okForStartTLS:
137 | self.currentlyWaitingFor = .tlsHandlerToBeAdded
138 | context.channel.eventLoop.makeCompletedFuture {
139 | try context.channel.pipeline.syncOperations.addHandler(
140 | try NIOSSLClientHandler(
141 | context: sslContext,
142 | serverHostname: serverConfiguration.hostname
143 | ),
144 | position: .first
145 | )
146 | }
147 | .whenComplete { result in
148 | guard case .tlsHandlerToBeAdded = self.currentlyWaitingFor else {
149 | preconditionFailure("wrong state \(self.currentlyWaitingFor)")
150 | }
151 |
152 | switch result {
153 | case .failure(let error):
154 | self.currentlyWaitingFor = .error(error)
155 | case .success:
156 | self.sendAuthenticationStart(context: context)
157 | }
158 | }
159 | case .okForOurAuthBegin:
160 | self.send(context: context, command: .authUser(self.serverConfiguration.username))
161 | self.currentlyWaitingFor = .okAfterUsername
162 | case .okAfterUsername:
163 | self.send(context: context, command: .authPassword(self.serverConfiguration.password))
164 | self.currentlyWaitingFor = .okAfterPassword
165 | case .okAfterPassword:
166 | self.send(context: context, command: .mailFrom(self.email.senderEmail))
167 | self.currentlyWaitingFor = .okAfterMailFrom
168 | case .okAfterMailFrom:
169 | self.send(context: context, command: .recipient(self.email.recipientEmail))
170 | self.currentlyWaitingFor = .okAfterRecipient
171 | case .okAfterRecipient:
172 | self.send(context: context, command: .data)
173 | self.currentlyWaitingFor = .okAfterDataCommand
174 | case .okAfterDataCommand:
175 | self.send(context: context, command: .transferData(email))
176 | self.currentlyWaitingFor = .okAfterMailData
177 | case .okAfterMailData:
178 | self.send(context: context, command: .quit)
179 | self.currentlyWaitingFor = .okAfterQuit
180 | case .okAfterQuit:
181 | self.allDonePromise.succeed(())
182 | context.close(promise: nil)
183 | self.currentlyWaitingFor = .nothing
184 | case .nothing:
185 | () // ignoring more data whilst quit (it's odd though)
186 | case .error:
187 | fatalError("error state")
188 | case .tlsHandlerToBeAdded:
189 | fatalError("bug in NIOTS: we shouldn't hit this state here")
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/NIOSMTP/NIOSMTP/ViewController.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import NIOCore
16 | import NIOExtras
17 | import NIOPosix
18 | import NIOSSL
19 | import NIOTLS
20 | import NIOTransportServices
21 | import UIKit
22 |
23 | #if canImport(Network)
24 | import Network
25 | #endif
26 |
27 | func sendEmail(
28 | _ email: Email,
29 | group: EventLoopGroup,
30 | communicationHandler: @escaping (String) -> Void,
31 | queue: DispatchQueue,
32 | _ handler: @escaping (Error?) -> Void
33 | ) {
34 | let emailSentPromise: EventLoopPromise = group.next().makePromise()
35 |
36 | let bootstrap: NIOClientTCPBootstrap
37 | do {
38 | bootstrap = try configureBootstrap(
39 | group: group,
40 | email: email,
41 | emailSentPromise: emailSentPromise,
42 | communicationHandler: communicationHandler
43 | )
44 | } catch {
45 | queue.async {
46 | handler(error)
47 | }
48 | return
49 | }
50 |
51 | let connection = bootstrap.connect(
52 | host: Configuration.shared.serverConfig.hostname,
53 | port: Configuration.shared.serverConfig.port
54 | )
55 |
56 | connection.cascadeFailure(to: emailSentPromise)
57 | emailSentPromise.futureResult.map {
58 | connection.whenSuccess { $0.close(promise: nil) }
59 | queue.async {
60 | handler(nil)
61 | }
62 | }.whenFailure { error in
63 | connection.whenSuccess { $0.close(promise: nil) }
64 | queue.async {
65 | handler(error)
66 | }
67 | }
68 | }
69 |
70 | class ViewController: UIViewController {
71 | var group: EventLoopGroup? = nil
72 |
73 | @IBOutlet weak var sendEmailButton: UIButton!
74 | @IBOutlet weak var logView: UITextView!
75 | @IBOutlet weak var textField: UITextView!
76 | @IBOutlet weak var subjectField: UITextField!
77 | @IBOutlet weak var toField: UITextField!
78 | @IBOutlet weak var fromField: UITextField!
79 |
80 | @IBAction func sendEmailAction(_ sender: UIButton) {
81 | self.logView.text = ""
82 | let email = Email(
83 | senderName: nil,
84 | senderEmail: self.fromField.text!,
85 | recipientName: nil,
86 | recipientEmail: self.toField.text!,
87 | subject: self.subjectField.text!,
88 | body: self.textField.text!
89 | )
90 | let commHandler: (String) -> Void = { str in
91 | DispatchQueue.main.async {
92 | self.logView.text += str + "\n"
93 | }
94 | }
95 | sendEmail(email, group: self.group!, communicationHandler: commHandler, queue: DispatchQueue.main) {
96 | maybeError in
97 | assert(Thread.isMainThread)
98 | if let error = maybeError {
99 | self.sendEmailButton.titleLabel?.text = "❌"
100 | self.logView.text += "ERROR: \(error)\n"
101 | } else {
102 | self.sendEmailButton.titleLabel?.text = "✅"
103 | }
104 | }
105 | }
106 |
107 | override func viewWillAppear(_ animated: Bool) {
108 | let group = makeEventLoopGroup(loopCount: 1, implementation: .best)
109 | self.group = group
110 | }
111 |
112 | override func viewWillDisappear(_ animated: Bool) {
113 | try! self.group?.syncShutdownGracefully()
114 | self.group = nil
115 | }
116 |
117 | override func viewDidLoad() {
118 | super.viewDidLoad()
119 | // Do any additional setup after loading the view, typically from a nib.
120 | }
121 | }
122 |
123 | // MARK: - NIO/NIOTS handling
124 |
125 | func makeNIOSMTPHandlers(
126 | communicationHandler: @escaping (String) -> Void,
127 | email: Email,
128 | emailSentPromise: EventLoopPromise
129 | ) -> [ChannelHandler] {
130 | [
131 | PrintEverythingHandler(handler: communicationHandler),
132 | ByteToMessageHandler(LineBasedFrameDecoder()),
133 | SMTPResponseDecoder(),
134 | MessageToByteHandler(SMTPRequestEncoder()),
135 | SendEmailHandler(
136 | configuration: Configuration.shared.serverConfig,
137 | email: email,
138 | allDonePromise: emailSentPromise
139 | ),
140 | ]
141 | }
142 |
143 | private let sslContext = try! NIOSSLContext(configuration: TLSConfiguration.makeClientConfiguration())
144 |
145 | func configureBootstrap(
146 | group: EventLoopGroup,
147 | email: Email,
148 | emailSentPromise: EventLoopPromise,
149 | communicationHandler: @escaping (String) -> Void
150 | ) throws -> NIOClientTCPBootstrap {
151 | let hostname = Configuration.shared.serverConfig.hostname
152 | let bootstrap: NIOClientTCPBootstrap
153 |
154 | switch (NetworkImplementation.best, Configuration.shared.serverConfig.tlsConfiguration) {
155 | case (.transportServices, .regularTLS), (.transportServices, .insecureNoTLS):
156 | if #available(macOS 10.14, iOS 12, tvOS 12, watchOS 3, *) {
157 | bootstrap = NIOClientTCPBootstrap(
158 | NIOTSConnectionBootstrap(group: group),
159 | tls: NIOTSClientTLSProvider()
160 | )
161 | } else {
162 | fatalError("Network.framework unsupported on this OS yet it was selected as the best option.")
163 | }
164 | case (.transportServices, .startTLS):
165 | if #available(macOS 10.14, iOS 12, tvOS 12, watchOS 3, *) {
166 | bootstrap = try NIOClientTCPBootstrap(
167 | NIOTSConnectionBootstrap(group: group),
168 | tls: NIOSSLClientTLSProvider(
169 | context: sslContext,
170 | serverHostname: hostname
171 | )
172 | )
173 | } else {
174 | fatalError("Network.framework unsupported on this OS yet it was selected as the best option.")
175 | }
176 | case (.posix, _):
177 | bootstrap = try NIOClientTCPBootstrap(
178 | ClientBootstrap(group: group),
179 | tls: NIOSSLClientTLSProvider(
180 | context: sslContext,
181 | serverHostname: hostname
182 | )
183 | )
184 | }
185 |
186 | switch Configuration.shared.serverConfig.tlsConfiguration {
187 | case .regularTLS:
188 | bootstrap.enableTLS()
189 | case .insecureNoTLS, .startTLS:
190 | () // no TLS to start with
191 | }
192 |
193 | return bootstrap.channelInitializer { channel in
194 | channel.pipeline.addHandlers(
195 | makeNIOSMTPHandlers(
196 | communicationHandler: communicationHandler,
197 | email: email,
198 | emailSentPromise: emailSentPromise
199 | )
200 | )
201 | }
202 | }
203 |
204 | /// Network implementation and by extension which version of NIO to use.
205 | enum NetworkImplementation {
206 | /// POSIX sockets and NIO.
207 | case posix
208 |
209 | /// NIOTransportServices (and Network.framework).
210 | case transportServices
211 |
212 | /// Return the best implementation available for this platform, that is NIOTransportServices
213 | /// when it is available or POSIX and NIO otherwise.
214 | static var best: NetworkImplementation {
215 | if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
216 | return .transportServices
217 | } else {
218 | return .posix
219 | }
220 | }
221 | }
222 |
223 | /// Makes an appropriate `EventLoopGroup` based on the given implementation.
224 | ///
225 | /// For `.posix` this is a `MultiThreadedEventLoopGroup`, for `.networkFramework` it is a
226 | /// `NIOTSEventLoopGroup`.
227 | ///
228 | /// - Parameter implementation: The network implementation to use.
229 | func makeEventLoopGroup(loopCount: Int, implementation: NetworkImplementation) -> EventLoopGroup {
230 | switch implementation {
231 | case .transportServices:
232 | guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else {
233 | // This is gated by the availability of `.networkFramework` so should never happen.
234 | fatalError(".networkFramework is being used on an unsupported platform")
235 | }
236 | return NIOTSEventLoopGroup(loopCount: loopCount, defaultQoS: .utility)
237 | case .posix:
238 | return MultiThreadedEventLoopGroup(numberOfThreads: loopCount)
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/NIOSMTP/README.md:
--------------------------------------------------------------------------------
1 | # NIOSMTP
2 |
3 | NIOSMTP is a demo app that is a very simple SMTP client and therefore allows you to send email. NIOSMTP is an iOS 12 app that uses [`swift-nio-transport-services`](https://github.com/apple/swift-nio-transport-services) on top of [`Network.framework`](https://developer.apple.com/documentation/network) to do all the networking. It supports plain text SMTP (boo), SMTPS, as well as SMTP with STARTTLS which is probably what your mail provider wants.
4 |
5 | ## Prerequisites
6 |
7 | - Xcode 11+
8 |
9 | ## Caveats
10 |
11 | - if you want to try this out you'll have to put your SMTP server configuration
12 | in [`Configuration.swift`](https://github.com/apple/swift-nio-examples/blob/main/NIOSMTP/NIOSMTP/Configuration.swift), there's no configuration UI at this moment
13 | - before trying out the app you need to configure your SMTP server in `NIOSMTP/Configuration.swift`
14 | - it's a very basic SMTP/MIME implementation, the email body isn't even base64 encoded neither is any other data.
15 | - The `SendEmailHandler` should accept `Email` objects through the pipeline to be more widely usable. Currently it requires the `Email` object in its initialiser which means it can only ever send one email per connection (`Channel`)
16 |
17 | ## Screenshots
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/NIOSMTP/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eu
4 | set -x
5 |
6 | xcodebuild -project NIOSMTP.xcodeproj -scheme NIOSMTP -arch x86_64 -sdk iphonesimulator
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## SwiftNIO Example Apps
2 |
3 | The point of this repository is to be a collection of ready-to-use SwiftNIO example apps. The other `apple/swift-nio` repositories contain libraries which do sometimes contain example code but usually not whole applications.
4 |
5 | The definition of app includes any sort of application, command line utilities, iOS Apps, macOS GUI applications or whatever you can think of.
6 |
7 | ### Organisation
8 |
9 | Each example application should be fully contained in its own sub-directory together with a `README.md` explaining what the application demonstrates. Each application must be buildable through either `cd AppName && swift build` or `cd AppName && ./build.sh`.
10 |
11 | Like all other code in the SwiftNIO project, the license for all the code contained in this repository is the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html). See also `LICENSE.txt`.
12 |
13 |
14 | ### Quality
15 |
16 | Example applications must go through pre-commit code review like all other code in the SwiftNIO project. It is however acceptable to publish demo applications that only work for a subset of the supported platforms if that limitation is clearly documented in the project's `README.md`.
17 |
18 |
19 | ### NIO versions
20 |
21 | The [`main`](https://github.com/apple/swift-nio-examples) branch contains the examples for the SwiftNIO 2 family. For the examples working with NIO 1, please use the [`nio-1`](https://github.com/apple/swift-nio-examples/tree/nio-1) branch.
22 |
--------------------------------------------------------------------------------
/TLSify/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/TLSify/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "TLSify",
38 | products: [
39 | .executable(name: "TLSify", targets: ["TLSify"])
40 | ],
41 | dependencies: [
42 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
43 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.14.0"),
44 | .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
45 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
46 | ],
47 | targets: [
48 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
49 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
50 | .executableTarget(
51 | name: "TLSify",
52 | dependencies: [
53 | "TLSifyLib",
54 | .product(name: "ArgumentParser", package: "swift-argument-parser"),
55 | .product(name: "NIOCore", package: "swift-nio"),
56 | .product(name: "NIOPosix", package: "swift-nio"),
57 | .product(name: "Logging", package: "swift-log"),
58 | ],
59 | swiftSettings: strictConcurrencySettings
60 | ),
61 | .target(
62 | name: "TLSifyLib",
63 | dependencies: [
64 | .product(name: "NIOCore", package: "swift-nio"),
65 | .product(name: "NIOPosix", package: "swift-nio"),
66 | .product(name: "NIOSSL", package: "swift-nio-ssl"),
67 | .product(name: "Logging", package: "swift-log"),
68 | ],
69 | swiftSettings: strictConcurrencySettings
70 | ),
71 | ]
72 | )
73 |
--------------------------------------------------------------------------------
/TLSify/README.md:
--------------------------------------------------------------------------------
1 | # TLSify
2 |
3 | TLSify is a simple TLS proxy. It accepts plaintext (unencrypted) connections and TLS-wraps them to a target.
4 |
5 | This functionality can be very useful if you want to use Wireshark, `tcpdump`, or similar tools to snoop on traffic whilst still having everything
6 | that leaves your machine fully encrypted.
7 |
8 | ## Example
9 |
10 | First, get three terminal windows ready.
11 |
12 | Run this in the first window:
13 |
14 | ```
15 | swift run -c release TLSify 8080 httpbin.org 443
16 | ```
17 |
18 | Then, in the second one run
19 |
20 | ```
21 | sudo tcpdump -i lo0 -A '' 'port 8080'
22 | ```
23 |
24 | and finally, in the third you can kick off a `curl`:
25 |
26 | ```
27 | curl -H "host: httpbin.org" http://localhost:8080/anything
28 | ```
29 |
30 | The output from `curl` will look something like
31 |
32 | ```
33 | {
34 | "args": {},
35 | "data": "",
36 | "files": {},
37 | "form": {},
38 | "headers": {
39 | "Accept": "*/*",
40 | "Host": "httpbin.org",
41 | "User-Agent": "curl/7.64.1",
42 | "X-Amzn-Trace-Id": "Root=1-5ec8fe25-2847b5847f049b288f87e72e"
43 | },
44 | "json": null,
45 | "method": "GET",
46 | "origin": "213.1.9.208",
47 | "url": "https://httpbin.org/anything"
48 | }
49 | ```
50 |
51 | As you can see, `httpbin.org` says `"url": "https://httpbin.org/anything"` so it came encrypted. But the `tcpdump` should output
52 | (amongst many other things) something like
53 |
54 | ```
55 | 11:45:46.650625 IP6 localhost.50297 > localhost.http-alt: Flags [P.], seq 1:84, ack 1, win 6371, options [nop,nop,TS val 855170339 ecr 855170339], length 83: HTTP: GET /anything HTTP/1.1
56 | `.h..s.@.................................y...~..Y........{.....
57 | 2..#2..#GET /anything HTTP/1.1
58 | Host: httpbin.org
59 | User-Agent: curl/7.64.1
60 | Accept: */*
61 |
62 |
63 | 11:45:46.650638 IP6 localhost.http-alt > localhost.50297: Flags [.], ack 84, win 6370, options [nop,nop,TS val 855170339 ecr 855170339], length 0
64 | `.#.. .@...................................yY....~.......(.....
65 | 2..#2..#
66 | 11:45:47.021538 IP6 localhost.http-alt > localhost.50297: Flags [P.], seq 1:572, ack 84, win 6370, options [nop,nop,TS val 855170706 ecr 855170339], length 571: HTTP: HTTP/1.1 200 OK
67 | `.#..[.@...................................yY....~.......c.....
68 | 2...2..#HTTP/1.1 200 OK
69 | Date: Sat, 23 May 2020 10:46:41 GMT
70 | Content-Type: application/json
71 | Content-Length: 341
72 | Connection: keep-alive
73 | Server: gunicorn/19.9.0
74 | Access-Control-Allow-Origin: *
75 | Access-Control-Allow-Credentials: true
76 |
77 | {
78 | "args": {},
79 | "data": "",
80 | "files": {},
81 | "form": {},
82 | "headers": {
83 | "Accept": "*/*",
84 | "Host": "httpbin.org",
85 | "User-Agent": "curl/7.64.1",
86 | "X-Amzn-Trace-Id": "Root=1-5ec8ff11-f8495d1407f5ace02a4251fc"
87 | },
88 | "json": null,
89 | "method": "GET",
90 | "origin": "213.1.9.208",
91 | "url": "https://httpbin.org/anything"
92 | }
93 | ```
94 |
95 | where you can see both request and response in plain text :).
96 |
--------------------------------------------------------------------------------
/TLSify/Sources/TLSify/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import ArgumentParser
16 | import Logging
17 | import NIOCore
18 | import NIOPosix
19 | import NIOSSL
20 | import TLSifyLib
21 |
22 | let rootLogger: Logger = {
23 | var rootLogger = Logger(label: "TLSify")
24 | rootLogger.logLevel = .debug
25 | return rootLogger
26 | }()
27 |
28 | struct TLSifyCommand: ParsableCommand {
29 | @Option(name: .shortAndLong, help: "The host to listen to.")
30 | var listenHost: String = "localhost"
31 |
32 | @Argument(help: "The port to listen to.")
33 | var listenPort: Int
34 |
35 | @Argument(help: "The host to connect to.")
36 | var connectHost: String
37 |
38 | @Argument(help: "The port to connect to.")
39 | var connectPort: Int
40 |
41 | @Option(name: .long, help: "TLS certificate verfication: full (default)/no-hostname/none.")
42 | var tlsCertificateValidation: String = "full"
43 |
44 | @Option(help: "The ALPN protocols to send.")
45 | var alpn: [String] = []
46 |
47 | func run() throws {
48 | var tlsConfig = TLSConfiguration.makeClientConfiguration()
49 | switch self.tlsCertificateValidation {
50 | case "none":
51 | tlsConfig.certificateVerification = .none
52 | case "no-hostname":
53 | tlsConfig.certificateVerification = .noHostnameVerification
54 | default:
55 | tlsConfig.certificateVerification = .fullVerification
56 | }
57 | tlsConfig.applicationProtocols = self.alpn
58 | let sslContext = try NIOSSLContext(configuration: tlsConfig)
59 | MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { el in
60 | ServerBootstrap(group: el)
61 | .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
62 | .childChannelInitializer { [connectHost, connectPort] channel in
63 | channel.eventLoop.makeCompletedFuture {
64 | try channel.pipeline.syncOperations.addHandlers([
65 | TLSProxy(
66 | host: connectHost,
67 | port: connectPort,
68 | sslContext: sslContext,
69 | logger: rootLogger
70 | ),
71 | CloseOnErrorHandler(logger: rootLogger),
72 | ])
73 | }
74 | }
75 | .bind(host: self.listenHost, port: self.listenPort)
76 | .map { channel in
77 | rootLogger.info("Listening on \(channel.localAddress!)")
78 | }
79 | .whenFailure { [listenHost, listenPort] error in
80 | rootLogger.error("Couldn't bind to \(listenHost):\(listenPort): \(error)")
81 | el.shutdownGracefully { error in
82 | if let error = error {
83 | preconditionFailure("EL shutdown failed: \(error)")
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
91 | TLSifyCommand.main()
92 |
--------------------------------------------------------------------------------
/TLSify/Sources/TLSifyLib/CloseOnErrorHandler.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Logging
16 | import NIOCore
17 |
18 | public final class CloseOnErrorHandler: ChannelInboundHandler, Sendable {
19 | public typealias InboundIn = Never
20 |
21 | private let logger: Logger
22 |
23 | public init(logger: Logger) {
24 | self.logger = logger
25 | }
26 |
27 | public func errorCaught(context: ChannelHandlerContext, error: Error) {
28 | self.logger.info("unhandled error \(error), closing \(context.channel)")
29 | context.close(promise: nil)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/TLSify/Sources/TLSifyLib/GlueHandler.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Logging
16 | import NIOCore
17 |
18 | final class GlueHandler {
19 |
20 | var logger: Logger
21 | var context: ChannelHandlerContext? = nil
22 | var partner: GlueHandler? = nil
23 | private var pendingRead = false
24 |
25 | internal init(logger: Logger) {
26 | self.logger = logger
27 | }
28 | }
29 |
30 | extension GlueHandler {
31 | private func partnerWrite(_ data: NIOAny) {
32 | self.context?.write(data, promise: nil)
33 | }
34 |
35 | private func partnerFlush() {
36 | self.context?.flush()
37 | }
38 |
39 | private func partnerWriteEOF() {
40 | self.context?.close(mode: .output, promise: nil)
41 | }
42 |
43 | private func partnerCloseFull() {
44 | self.context?.close(promise: nil)
45 | }
46 |
47 | private func partnerBecameWritable() {
48 | if self.pendingRead {
49 | self.pendingRead = false
50 | self.context?.read()
51 | }
52 | }
53 |
54 | private var partnerWritable: Bool {
55 | self.context?.channel.isWritable ?? false
56 | }
57 | }
58 |
59 | extension GlueHandler: ChannelDuplexHandler {
60 | typealias InboundIn = ByteBuffer
61 | typealias InboundOut = ByteBuffer
62 |
63 | typealias OutboundIn = ByteBuffer
64 | typealias OutboundOut = ByteBuffer
65 |
66 | func handlerAdded(context: ChannelHandlerContext) {
67 | self.logger[metadataKey: "channel"] = "\(context.channel)"
68 | self.context = context
69 | }
70 |
71 | func handlerRemoved(context: ChannelHandlerContext) {
72 | self.context = nil
73 | self.partner = nil
74 | }
75 |
76 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
77 | self.partner?.partnerWrite(data)
78 | }
79 |
80 | func channelReadComplete(context: ChannelHandlerContext) {
81 | self.partner?.partnerFlush()
82 | context.fireChannelReadComplete()
83 | }
84 |
85 | func channelInactive(context: ChannelHandlerContext) {
86 | self.logger.debug("channel inactive")
87 | self.partner?.partnerCloseFull()
88 | context.fireChannelInactive()
89 | }
90 |
91 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
92 | if let event = event as? ChannelEvent, case .inputClosed = event {
93 | // We have read EOF.
94 | self.partner?.partnerWriteEOF()
95 | }
96 | context.fireUserInboundEventTriggered(event)
97 | }
98 |
99 | func errorCaught(context: ChannelHandlerContext, error: Error) {
100 | context.fireErrorCaught(error)
101 | self.partner?.partnerCloseFull()
102 | }
103 |
104 | func channelWritabilityChanged(context: ChannelHandlerContext) {
105 | if context.channel.isWritable {
106 | self.partner?.partnerBecameWritable()
107 | }
108 | }
109 |
110 | func read(context: ChannelHandlerContext) {
111 | if let partner = self.partner, partner.partnerWritable {
112 | context.read()
113 | } else {
114 | self.pendingRead = true
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/TLSify/Sources/TLSifyLib/TLSProxy.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Logging
16 | import NIOCore
17 | import NIOPosix
18 | import NIOSSL
19 |
20 | public final class TLSProxy {
21 | enum State {
22 | case waitingToBeActivated
23 | case connecting(ByteBuffer)
24 | case connected
25 | case error(Error)
26 | case closed
27 | }
28 | private var state = State.waitingToBeActivated {
29 | didSet {
30 | self.logger.trace("SM new state: \(self.state)")
31 | }
32 | }
33 |
34 | private let host: String
35 | private let port: Int
36 | private var logger: Logger
37 | private let sslContext: NIOSSLContext
38 |
39 | public init(host: String, port: Int, sslContext: NIOSSLContext, logger: Logger) {
40 | self.host = host
41 | self.port = port
42 | self.sslContext = sslContext
43 | self.logger = logger
44 | self.logger[metadataKey: "side"] = "Source <--[plain text]--> Proxy"
45 |
46 | }
47 |
48 | func illegalTransition(to: String = #function) -> Never {
49 | preconditionFailure("illegal transition to \(to) in \(self.state)")
50 | }
51 |
52 | func gotError(_ error: Error) {
53 | self.logger.warning("unexpected error: \(#function): \(error)")
54 |
55 | switch self.state {
56 | case .connected, .connecting, .waitingToBeActivated, .closed:
57 | self.state = .error(error)
58 | case .error:
59 | ()
60 | }
61 | }
62 |
63 | func connected(
64 | partnerChannel: Channel,
65 | myChannel: Channel,
66 | contextForInitialData: ChannelHandlerContext
67 | ) {
68 | self.logger.debug("connected to \(partnerChannel)")
69 |
70 | let bytes: ByteBuffer
71 | switch self.state {
72 | case .waitingToBeActivated, .connected:
73 | self.illegalTransition()
74 | case .error(let error):
75 | partnerChannel.pipeline.fireErrorCaught(error)
76 | myChannel.pipeline.fireErrorCaught(error)
77 | partnerChannel.close(promise: nil)
78 | return
79 | case .closed:
80 | self.logger.warning("discarding \(partnerChannel) because we're already closed.")
81 | partnerChannel.close(promise: nil)
82 | return
83 | case .connecting(let buffer):
84 | bytes = buffer
85 | self.state = .connected
86 | // fall through
87 | }
88 |
89 | var partnerLogger = self.logger
90 | partnerLogger[metadataKey: "side"] = "Proxy <--[TLS]--> Target"
91 | let myGlue = GlueHandler(logger: self.logger)
92 | let partnerGlue = GlueHandler(logger: partnerLogger)
93 | myGlue.partner = partnerGlue
94 | partnerGlue.partner = myGlue
95 |
96 | assert(partnerChannel.eventLoop === myChannel.eventLoop)
97 |
98 | do {
99 | try myChannel.pipeline.syncOperations.addHandler(myGlue, position: .after(contextForInitialData.handler))
100 | try partnerChannel.pipeline.syncOperations.addHandler(partnerGlue)
101 | } catch {
102 | self.gotError(error)
103 |
104 | partnerChannel.pipeline.fireErrorCaught(error)
105 | contextForInitialData.fireErrorCaught(error)
106 | }
107 | guard case .connected = self.state else {
108 | return
109 | }
110 | assert(myGlue.context != nil)
111 | assert(partnerGlue.context != nil)
112 |
113 | if bytes.readableBytes > 0 {
114 | contextForInitialData.fireChannelRead(self.wrapInboundOut(bytes))
115 | contextForInitialData.fireChannelReadComplete()
116 | }
117 | contextForInitialData.read()
118 | }
119 |
120 | func connectPartner(eventLoop: EventLoop) -> EventLoopFuture {
121 | self.logger.debug("connecting to \(self.host):\(self.port)")
122 |
123 | return ClientBootstrap(group: eventLoop)
124 | .channelInitializer { [sslContext, host, logger] channel in
125 | channel.pipeline.eventLoop.makeCompletedFuture {
126 | try channel.pipeline.syncOperations.addHandlers(
127 | try! NIOSSLClientHandler(context: sslContext, serverHostname: host),
128 | CloseOnErrorHandler(logger: logger)
129 | )
130 | }
131 | }
132 | .connect(host: self.host, port: self.port)
133 | }
134 | }
135 |
136 | @available(*, unavailable)
137 | extension TLSProxy: Sendable {}
138 |
139 | extension TLSProxy: ChannelDuplexHandler {
140 | public typealias InboundIn = ByteBuffer
141 | public typealias InboundOut = ByteBuffer
142 |
143 | public typealias OutboundIn = ByteBuffer
144 | public typealias OutboundOut = ByteBuffer
145 |
146 | public func handlerAdded(context: ChannelHandlerContext) {
147 | self.logger[metadataKey: "channel"] = "\(context.channel)"
148 |
149 | let isActive = context.channel.isActive
150 | self.logger.trace("added to Channel", metadata: ["isActive": "\(isActive)"])
151 | if isActive {
152 | self.beginConnecting(context: context)
153 | }
154 | }
155 |
156 | public func channelActive(context: ChannelHandlerContext) {
157 | self.logger.trace("Received channelActive")
158 | self.beginConnecting(context: context)
159 | }
160 |
161 | private func beginConnecting(context: ChannelHandlerContext) {
162 | switch self.state {
163 | case .waitingToBeActivated:
164 | self.state = .connecting(context.channel.allocator.buffer(capacity: 0))
165 | self.connectPartner(eventLoop: context.eventLoop).assumeIsolatedUnsafeUnchecked().whenComplete { result in
166 | switch result {
167 | case .failure(let error):
168 | self.gotError(error)
169 |
170 | context.fireErrorCaught(error)
171 | case .success(let channel):
172 | self.connected(
173 | partnerChannel: channel,
174 | myChannel: context.channel,
175 | contextForInitialData: context
176 | )
177 | }
178 | }
179 | case .connecting, .connected, .error, .closed:
180 | // Duplicate call, fine. Can happen if channelActive is awkwardly
181 | // ordered.
182 | ()
183 | }
184 | }
185 |
186 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
187 | switch self.state {
188 | case .connected:
189 | context.fireChannelRead(data) // Glue will pick that up and forward to other Channel
190 | case .connecting(var buffer):
191 | var incomingBuffer = self.unwrapInboundIn(data)
192 | if buffer.readableBytes == 0 {
193 | self.state = .connecting(incomingBuffer)
194 | } else {
195 | buffer.writeBuffer(&incomingBuffer)
196 | self.state = .connecting(buffer)
197 | }
198 | case .error, .closed:
199 | () // we can drop this
200 | case .waitingToBeActivated:
201 | self.illegalTransition()
202 | }
203 | }
204 |
205 | public func read(context: ChannelHandlerContext) {
206 | switch self.state {
207 | case .connected:
208 | context.read()
209 | case .connecting, .error, .closed:
210 | () // No, let's not read more that we'd need to buffer/drop anyway
211 | case .waitingToBeActivated:
212 | self.illegalTransition()
213 | }
214 | }
215 |
216 | public func channelInactive(context: ChannelHandlerContext) {
217 | self.logger.debug("Channel inactive")
218 | defer {
219 | context.fireChannelInactive()
220 | }
221 | switch self.state {
222 | case .connected, .connecting:
223 | self.state = .closed
224 | case .error:
225 | ()
226 | case .closed, .waitingToBeActivated:
227 | self.illegalTransition()
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/UniversalBootstrapDemo/.gitignore:
--------------------------------------------------------------------------------
1 | *.xcodeproj
2 |
--------------------------------------------------------------------------------
/UniversalBootstrapDemo/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "UniversalBootstrapDemo",
38 | dependencies: [
39 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
40 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.16.1"),
41 | .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.11.3"),
42 | .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0")
43 | ],
44 | targets: [
45 | .executableTarget(
46 | name: "UniversalBootstrapDemo",
47 | dependencies: [
48 | .product(name: "NIOCore", package: "swift-nio"),
49 | .product(name: "NIOPosix", package: "swift-nio"),
50 | .product(name: "NIOSSL", package: "swift-nio-ssl"),
51 | .product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
52 | .product(name: "NIOHTTP1", package: "swift-nio"),
53 | .product(name: "ArgumentParser", package: "swift-argument-parser"),
54 | ],
55 | swiftSettings: strictConcurrencySettings
56 | ),
57 | ]
58 | )
59 |
--------------------------------------------------------------------------------
/UniversalBootstrapDemo/README.md:
--------------------------------------------------------------------------------
1 | # UniversalBootstrapDemo
2 |
3 | This little package demonstrates how you can use SwiftNIO's universal bootstraps. That allows you to fully support Network.framework on
4 | Apple platforms (if new enough) as well as BSD Sockets on Linux (and older Apple platforms).
5 |
6 | ## Understanding this example
7 |
8 | This example mainly consists of three files:
9 |
10 | - [`EventLoopGroupManager.swift`](Sources/UniversalBootstrapDemo/EventLoopGroupManager.swift) which is the main and most important component of this example. It demonstrates a way how you can manage your `EventLoopGroup`, select a matching bootstrap, as well as a TLS implementation.
11 | - [`ExampleHTTPLibrary.swift`](Sources/UniversalBootstrapDemo/ExampleHTTPLibrary.swift) which is an example of how you could implement a basic HTTP library using `EventLoopGroupManager`.
12 | - [`main.swift`](Sources/UniversalBootstrapDemo/main.swift) which is just the driver to run the example programs.
13 |
14 | ## Examples
15 |
16 | ### Platform best
17 |
18 | To use the best networking available on your platform, try
19 |
20 | swift run UniversalBootstrapDemo https://httpbin.org/get
21 |
22 | The output would be for example:
23 |
24 | ```
25 | # Channel
26 | NIOTransportServices.NIOTSConnectionChannel
27 | ```
28 |
29 | Ah, we're running on a `NIOTSConnectionChannel` which means Network.framework was used to provide the underlying TCP connection.
30 |
31 |
32 | ```
33 | # ChannelPipeline
34 | [I] ↓↑ [O]
35 | ↓↑ HTTPRequestEncoder [handler0]
36 | HTTPResponseDecoder ↓↑ HTTPResponseDecoder [handler1]
37 | PrintToStdoutHandler ↓↑ [handler2]
38 | ```
39 |
40 | Note, that there is no `NIOSSLClientHandler` in the pipeline despite using HTTPS. That is because Network.framework does also providing
41 | the TLS support.
42 |
43 | ```
44 | # HTTP response body
45 | {
46 | "args": {},
47 | "headers": {
48 | "Host": "httpbin.org",
49 | "X-Amzn-Trace-Id": "Root=1-5eb1a4aa-4004f9686506e319aebd44a1"
50 | },
51 | "origin": "86.158.121.11",
52 | "url": "https://httpbin.org/get"
53 | }
54 | ```
55 |
56 | ### Running with an `EventLoopGroup` selected by somebody else
57 |
58 | To imitate your library needing to support an `EventLoopGroup` of unknown backing that was passed in from a client, you may want to try
59 |
60 | swift run UniversalBootstrapDemo --force-bsd-sockets https://httpbin.org/get
61 |
62 | The new output is now
63 |
64 | ```
65 | # Channel
66 | SocketChannel { BaseSocket { fd=9 }, active = true, localAddress = Optional([IPv4]192.168.4.26/192.168.4.26:60266), remoteAddress = Optional([IPv4]35.170.216.115/35.170.216.115:443) }
67 | ```
68 |
69 | Which uses BSD sockets.
70 |
71 | ```
72 | # ChannelPipeline
73 | [I] ↓↑ [O]
74 | NIOSSLClientHandler ↓↑ NIOSSLClientHandler [handler3]
75 | ↓↑ HTTPRequestEncoder [handler0]
76 | HTTPResponseDecoder ↓↑ HTTPResponseDecoder [handler1]
77 | PrintToStdoutHandler ↓↑ [handler2]
78 | ```
79 |
80 | And the `ChannelPipeline` now also contains the `NIOSSLClientHandler` because SwiftNIOSSL now has to take care of TLS encryption.
81 |
82 | ```
83 | # HTTP response body
84 | {
85 | "args": {},
86 | "headers": {
87 | "Host": "httpbin.org",
88 | "X-Amzn-Trace-Id": "Root=1-5eb1a543-8fcbadf00a2b9990969c35c0"
89 | },
90 | "origin": "86.158.121.11",
91 | "url": "https://httpbin.org/get"
92 | }
93 | ```
94 |
--------------------------------------------------------------------------------
/UniversalBootstrapDemo/Sources/UniversalBootstrapDemo/EventLoopGroupManager.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import NIOConcurrencyHelpers
16 | import NIOCore
17 | import NIOPosix
18 | import NIOSSL
19 | import NIOTransportServices
20 |
21 | /// `EventLoopGroupManager` can be used to manage an `EventLoopGroup`, either by creating or by sharing an existing one.
22 | ///
23 | /// When making network client libraries with SwiftNIO that are supposed to work well on both Apple platforms (macOS,
24 | /// iOS, tvOS, ...) as well as Linux, users often find it tedious to select the right combination of:
25 | ///
26 | /// - an `EventLoopGroup`
27 | /// - a bootstrap
28 | /// - a TLS implementation
29 | ///
30 | /// The choices to the above need to be compatible, or else the program won't work.
31 | ///
32 | /// What makes the task even harder is that as a client library, you often want to share the `EventLoopGroup` with other
33 | /// components. That raises the question of how to choose a bootstrap and a matching TLS implementation without even
34 | /// knowing the concrete `EventLoopGroup` type (it may be `SelectableEventLoop` which is an internal `NIO` types).
35 | /// `EventLoopGroupManager` should support all those use cases with a simple API.
36 | public class EventLoopGroupManager: @unchecked Sendable {
37 | private let lock = NIOLock()
38 | private var group: Optional
39 | private let provider: Provider
40 | private var sslContext = try! NIOSSLContext(configuration: .makeClientConfiguration())
41 |
42 | public enum Provider {
43 | case createNew
44 | case shared(EventLoopGroup)
45 | }
46 |
47 | /// Initialize the `EventLoopGroupManager` with a `Provider` of `EventLoopGroup`s.
48 | ///
49 | /// The `Provider` lets you choose whether to use a `.shared(group)` or to `.createNew`.
50 | public init(provider: Provider) {
51 | self.provider = provider
52 | switch self.provider {
53 | case .shared(let group):
54 | self.group = group
55 | case .createNew:
56 | self.group = nil
57 | }
58 | }
59 |
60 | deinit {
61 | assert(self.group == nil, "Please call EventLoopGroupManager.syncShutdown .")
62 | }
63 | }
64 |
65 | // - MARK: Public API
66 | extension EventLoopGroupManager {
67 | /// Create a "universal bootstrap" for the given host.
68 | ///
69 | /// - parameters:
70 | /// - hostname: The hostname to connect to (for SNI).
71 | /// - useTLS: Whether to use TLS or not.
72 | public func makeBootstrap(hostname: String, useTLS: Bool = true) throws -> NIOClientTCPBootstrap {
73 | try self.lock.withLock {
74 | let bootstrap: NIOClientTCPBootstrap
75 | if let group = self.group {
76 | bootstrap = try self.makeUniversalBootstrapWithExistingGroup(group, serverHostname: hostname)
77 | } else {
78 | bootstrap = try self.makeUniversalBootstrapWithSystemDefaults(serverHostname: hostname)
79 | }
80 |
81 | if useTLS {
82 | return bootstrap.enableTLS()
83 | } else {
84 | return bootstrap
85 | }
86 | }
87 | }
88 |
89 | /// Shutdown the `EventLoopGroupManager`.
90 | ///
91 | /// This will release all resources associated with the `EventLoopGroupManager` such as the threads that the
92 | /// `EventLoopGroup` runs on.
93 | ///
94 | /// This method _must_ be called when you're done with this `EventLoopGroupManager`.
95 | public func syncShutdown() throws {
96 | try self.lock.withLock {
97 | switch self.provider {
98 | case .createNew:
99 | try self.group?.syncShutdownGracefully()
100 | case .shared:
101 | () // nothing to do.
102 | }
103 | self.group = nil
104 | }
105 | }
106 | }
107 |
108 | // - MARK: Error types
109 | extension EventLoopGroupManager {
110 | /// The provided `EventLoopGroup` is not compatible with this client.
111 | public struct UnsupportedEventLoopGroupError: Error {
112 | var eventLoopGroup: EventLoopGroup
113 | }
114 | }
115 |
116 | // - MARK: Internal functions
117 | extension EventLoopGroupManager {
118 | // This function combines the right pieces and returns you a "universal client bootstrap"
119 | // (`NIOClientTCPBootstrap`). This allows you to bootstrap connections (with or without TLS) using either the
120 | // NIO on sockets (`NIO`) or NIO on Network.framework (`NIOTransportServices`) stacks.
121 | // The remainder of the code should be platform-independent.
122 | private func makeUniversalBootstrapWithSystemDefaults(serverHostname: String) throws -> NIOClientTCPBootstrap {
123 | if let group = self.group {
124 | return try self.makeUniversalBootstrapWithExistingGroup(group, serverHostname: serverHostname)
125 | }
126 |
127 | let group: EventLoopGroup
128 | #if canImport(Network)
129 | if #available(macOS 10.14, iOS 12, tvOS 12, watchOS 6, *) {
130 | // We run on a new-enough Darwin so we can use Network.framework
131 | group = NIOTSEventLoopGroup()
132 | } else {
133 | // We're on Darwin but not new enough for Network.framework, so we fall back on NIO on BSD sockets.
134 | group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
135 | }
136 | #else
137 | // We are on a non-Darwin platform, so we'll use BSD sockets.
138 | group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
139 | #endif
140 |
141 | // Let's save it for next time.
142 | self.group = group
143 | return try self.makeUniversalBootstrapWithExistingGroup(group, serverHostname: serverHostname)
144 | }
145 |
146 | // If we already know the group, then let's just contruct the correct bootstrap.
147 | private func makeUniversalBootstrapWithExistingGroup(
148 | _ group: EventLoopGroup,
149 | serverHostname: String
150 | ) throws -> NIOClientTCPBootstrap {
151 | if let bootstrap = ClientBootstrap(validatingGroup: group) {
152 | return try NIOClientTCPBootstrap(
153 | bootstrap,
154 | tls: NIOSSLClientTLSProvider(
155 | context: self.sslContext,
156 | serverHostname: serverHostname
157 | )
158 | )
159 | }
160 |
161 | #if canImport(Network)
162 | if #available(macOS 10.14, iOS 12, tvOS 12, watchOS 6, *) {
163 | if let makeBootstrap = NIOTSConnectionBootstrap(validatingGroup: group) {
164 | return NIOClientTCPBootstrap(makeBootstrap, tls: NIOTSClientTLSProvider())
165 | }
166 | }
167 | #endif
168 |
169 | throw UnsupportedEventLoopGroupError(eventLoopGroup: group)
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/UniversalBootstrapDemo/Sources/UniversalBootstrapDemo/ExampleHTTPLibrary.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 | import NIOCore
17 | import NIOHTTP1
18 |
19 | public struct UnsupportedURLError: Error {
20 | var url: String
21 | }
22 |
23 | public final class ExampleHTTPLibrary: Sendable {
24 | let groupManager: EventLoopGroupManager
25 |
26 | public init(groupProvider provider: EventLoopGroupManager.Provider) {
27 | self.groupManager = EventLoopGroupManager(provider: provider)
28 | }
29 |
30 | public func shutdown() throws {
31 | try self.groupManager.syncShutdown()
32 | }
33 |
34 | public func makeRequest(url urlString: String) throws {
35 | final class PrintToStdoutHandler: ChannelInboundHandler {
36 |
37 | typealias InboundIn = HTTPClientResponsePart
38 |
39 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
40 | switch self.unwrapInboundIn(data) {
41 | case .head:
42 | () // ignore
43 | case .body(let buffer):
44 | buffer.withUnsafeReadableBytes { ptr in
45 | _ = write(STDOUT_FILENO, ptr.baseAddress, ptr.count)
46 | }
47 | case .end:
48 | context.close(promise: nil)
49 | }
50 | }
51 | }
52 |
53 | guard let url = URL(string: urlString),
54 | let hostname = url.host,
55 | let scheme = url.scheme?.lowercased(),
56 | ["http", "https"].contains(scheme)
57 | else {
58 | throw UnsupportedURLError(url: urlString)
59 | }
60 | let useTLS = scheme == "https"
61 | let connection = try groupManager.makeBootstrap(hostname: hostname, useTLS: useTLS)
62 | .channelInitializer { channel in
63 | channel.eventLoop.makeCompletedFuture {
64 | try channel.pipeline.syncOperations.addHTTPClientHandlers()
65 | try channel.pipeline.syncOperations.addHandler(PrintToStdoutHandler())
66 | }
67 | }
68 | .connect(host: hostname, port: useTLS ? 443 : 80)
69 | .wait()
70 | print("# Channel")
71 | print(connection)
72 | print("# ChannelPipeline")
73 | print("\(connection.pipeline)")
74 | print("# HTTP response body")
75 | let reqHead = HTTPClientRequestPart.head(
76 | .init(
77 | version: .init(major: 1, minor: 1),
78 | method: .GET,
79 | uri: url.path,
80 | headers: ["host": hostname]
81 | )
82 | )
83 | connection.write(reqHead, promise: nil)
84 | try connection.writeAndFlush(HTTPClientRequestPart.end(nil)).wait()
85 | try connection.closeFuture.wait()
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/UniversalBootstrapDemo/Sources/UniversalBootstrapDemo/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import ArgumentParser
16 | import Foundation
17 | import NIOCore
18 | import NIOPosix
19 | import NIOTransportServices
20 |
21 | struct UniversalBootstrapDemo: ParsableCommand {
22 | struct NoNetworkFrameworkError: Error {}
23 |
24 | static let configuration = CommandConfiguration(
25 | abstract: """
26 | Demonstrates using NIO's universal bootstrap. Try for example
27 |
28 | UniversalBootstrapDemo https://httpbin.org/get
29 | """
30 | )
31 |
32 | @Flag(help: "Force using NIO on Network.framework.")
33 | var forceTransportServices = false
34 |
35 | @Flag(help: "Force using NIO on BSD sockets.")
36 | var forceBSDSockets = false
37 |
38 | @Argument(help: "The URL.")
39 | var url: String = "https://httpbin.org/get"
40 |
41 | func run() throws {
42 | var group: EventLoopGroup? = nil
43 | if self.forceTransportServices {
44 | #if canImport(Network)
45 | if #available(macOS 10.14, *) {
46 | group = NIOTSEventLoopGroup()
47 | } else {
48 | print("Sorry, your OS is too old for Network.framework.")
49 | Self.exit(withError: NoNetworkFrameworkError())
50 | }
51 | #else
52 | print("Sorry, no Network.framework on your OS.")
53 | Self.exit(withError: NoNetworkFrameworkError())
54 | #endif
55 | }
56 | if self.forceBSDSockets {
57 | group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
58 | }
59 | defer {
60 | try? group?.syncShutdownGracefully()
61 | }
62 |
63 | let provider: EventLoopGroupManager.Provider = group.map { .shared($0) } ?? .createNew
64 | let httpClient = ExampleHTTPLibrary(groupProvider: provider)
65 | defer {
66 | try! httpClient.shutdown()
67 | }
68 | try httpClient.makeRequest(url: url)
69 | }
70 | }
71 |
72 | UniversalBootstrapDemo.main()
73 |
--------------------------------------------------------------------------------
/backpressure-file-io-channel/.gitignore:
--------------------------------------------------------------------------------
1 | *.xcodeproj
2 |
--------------------------------------------------------------------------------
/backpressure-file-io-channel/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "backpressure-file-io-channel",
38 | platforms: [
39 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13)
40 | ],
41 | dependencies: [
42 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
43 | .package(url: "https://github.com/apple/swift-log.git", from: "1.1.0"),
44 | ],
45 | targets: [
46 | .target(
47 | name: "BackpressureChannelToFileIO",
48 | dependencies: [
49 | .product(name: "NIOCore", package: "swift-nio"),
50 | .product(name: "NIOPosix", package: "swift-nio"),
51 | .product(name: "NIOHTTP1", package: "swift-nio"),
52 | .product(name: "Logging", package: "swift-log"),
53 | ]),
54 | .executableTarget(
55 | name: "BackpressureChannelToFileIODemo",
56 | dependencies: [
57 | "BackpressureChannelToFileIO",
58 | .product(name: "NIOCore", package: "swift-nio"),
59 | .product(name: "NIOPosix", package: "swift-nio"),
60 | .product(name: "Logging", package: "swift-log"),
61 | ],
62 | swiftSettings: strictConcurrencySettings
63 | ),
64 | .testTarget(
65 | name: "BackpressureChannelToFileIOTests",
66 | dependencies: [
67 | "BackpressureChannelToFileIO",
68 | .product(name: "NIOCore", package: "swift-nio"),
69 | .product(name: "NIOPosix", package: "swift-nio"),
70 | ],
71 | swiftSettings: strictConcurrencySettings
72 | ),
73 | ]
74 | )
75 |
--------------------------------------------------------------------------------
/backpressure-file-io-channel/README.md:
--------------------------------------------------------------------------------
1 | # backpressure-file-io-channel
2 |
3 | This example shows how you can propagate backpressure from the file system into the Channel.
4 |
5 | First, let's establish what it means to propagate backpressure from the file system into the Channel. Let's assume we have a HTTP server
6 | that accepts arbitrary amounts of data and writes it to the file system. If data is received faster over the network than we can write it to the
7 | disk, then the server runs into trouble: It can now only either buffer the data in memory or (at least in theory) drop it on the floor. The former
8 | would easily be usable as a denial of service exploit, the latter means that the server isn't able to provide its core functioniality.
9 |
10 | Backpressure is the mechanism to resolve the the buffering issue above. The idea is that the server stops accepting more data from the client than
11 | it can write to disk. Because HTTP runs over TCP which has flow-control built in, the TCP stacks will then lower the server's receive window
12 | size which means that the client gets slowed down or completely stopped from sending any data. Once the server finishes writing previously
13 | received data to disk, it starts draining the receive buffer which then make TCP's flow control raise the window sizes which allows the client
14 | to send further data.
15 |
16 | ## Backpressure in SwiftNIO
17 |
18 | In SwiftNIO, backpressure is propagated by stopping calls to the outbound [`read`](https://apple.github.io/swift-nio/docs/current/NIO/Protocols/_ChannelOutboundHandler.html#/s:3NIO23_ChannelOutboundHandlerP4read7contextyAA0bD7ContextC_tF) event.
19 |
20 | By default, `Channel`s in SwiftNIO have the [`autoRead`](https://apple.github.io/swift-nio/docs/current/NIO/Structs/ChannelOptions.html#/s:3NIO14ChannelOptionsV8autoReadAC5TypesO04AutoE6OptionVvpZ)
21 | `ChannelOption` enabled. When `autoRead` is enabled, SwiftNIO will automatically send a `read` (note, this is a very different event than the
22 | inbound `channelRead` event that is used to deliver data) event when the previous read burst has
23 | completed (signalled by the inbound [`channelReadComplete`](https://apple.github.io/swift-nio/docs/current/NIO/Protocols/_ChannelInboundHandler.html#/s:3NIO22_ChannelInboundHandlerP19channelReadComplete7contextyAA0bD7ContextC_tF)
24 | event). Therefore, you may be unaware of the existence of the `read` event despite having used SwiftNIO before.
25 |
26 | Suppressing the `read` event is one of the key demonstrations of this example. The fundamental idea is that to start with we let `read` flow
27 | through the `ChannelPipeline` until we have an HTTP request and the first bits of its request body. Once we received the first bits of the
28 | HTTP request body, we will _suppress_ `read` from flowing through the `ChannelPipeline` which means that SwiftNIO will stop reading
29 | further data from the network.
30 | When SwiftNIO stops reading further data from the network, this means that TCP flow control will kick in and slow the client down sending
31 | more of the HTTP request body (once both the client's send and the server's receive buffer are full).
32 | Once the disk writes of the previously received chunks have completed, we will issue a `read` event (assuming we held up at least one). From
33 | then on, `read` events will flow freely until the next bit of the HTTP request body is received, when they'll be suppressed again.
34 |
35 | This means however fast the client or however slow the disk is, we should be able to stream arbitarily size HTTP request bodies to disk in
36 | constant memory.
37 |
38 | ## Example implementation
39 |
40 | The implementation in this example creates a state machine called [`FileIOChannelWriteCoordinator.swift`](Sources/FileIOChannelWriteCoordinator.swift)
41 | which gets notified about the events that happen (both on the `ChannelPipeline` and from `NonBlockingFileIO`). Every input to the state
42 | machine also returns an `Action` which describes what operation needs to be done next.
43 |
44 | The state machine is deliberately implemented completely free of any I/O or any side effects which is why it returns `Action`.
45 |
46 | Because the state machine doesn't do any I/O, it's crucial to tell it about any relevant event that happens in the system.
47 |
48 | The full set of inputs to the state machine are
49 |
50 |
51 | - Tell the state machine that a new HTTP request started.
52 | ```swift
53 | internal mutating func didReceiveRequestBegin(targetPath: String) -> Action
54 | ```
55 |
56 | - Tell the state machine that we received more bytes of the request body.
57 | ```swift
58 | internal mutating func didReceiveRequestBodyBytes(_ bytes: ByteBuffer) -> Action
59 | ```
60 |
61 | - Tell the state machine we received the HTTP request end.
62 | ```swift
63 | internal mutating func didReceiveRequestEnd() -> Action
64 | ```
65 |
66 | - Tell the state machine that we've just finished writing one previously received chunk of the HTTP request body to disk.
67 | ```swift
68 | internal mutating func didFinishWritingOneChunkToFile() -> Action
69 | ```
70 |
71 | - Tell the state machine we finished opening the target file.
72 | ```swift
73 | internal mutating func didOpenTargetFile(_ fileHandle: NIOFileHandle) -> Action
74 | ```
75 |
76 | - Tell the state machine that we've hit an error.
77 | ```swift
78 | internal mutating func didError(_ error: Error) -> Action
79 | ```
80 |
81 | The `Action` returned by the state machine is one of
82 |
83 | - Do nothing, we are waiting for some event: `case nothingWeAreWaiting`
84 | - Start writing chunks to the target file: `case startWritingToTargetFile`
85 | - Open the file: `case openFile(String)`
86 | - We are done, please close the file handle. If an error occured, it is sent here too: `case processingCompletedDiscardResources(NIOFileHandle?, Error?)`
87 | - Just close the file, we have previously completed processing: `case closeFile(NIOFileHandle)`
88 |
89 | `SaveEverythingHTTPHandler` is the `ChannelHandler` which drives the state machines with the events it receives through the
90 | `ChannelPipeline`. Additionally, it tells the state machine about the results of the I/O operations it starts (when `Action` tells it to).
91 |
--------------------------------------------------------------------------------
/backpressure-file-io-channel/Sources/BackpressureChannelToFileIO/SaveEverythingHTTPServer.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Logging
16 | import NIOCore
17 | import NIOHTTP1
18 | import NIOPosix
19 |
20 | public final class SaveEverythingHTTPServer {
21 | private var state = FileIOCoordinatorState() {
22 | didSet {
23 | self.logger.trace("new state \(self.state)")
24 | }
25 | }
26 |
27 | private let fileIO: NonBlockingFileIO
28 | private let uploadDirectory: String
29 | private let logger: Logger
30 |
31 | public init(fileIO: NonBlockingFileIO, uploadDirectory: String, logger: Logger? = nil) {
32 | self.fileIO = fileIO
33 | if let logger = logger {
34 | self.logger = logger
35 | } else {
36 | self.logger = Logger(label: "\(#filePath)")
37 | }
38 | self.uploadDirectory = uploadDirectory
39 | }
40 |
41 | deinit {
42 | assert(self.state.inFinalState, "illegal state on handler removal: \(self.state)")
43 | }
44 |
45 | }
46 |
47 | // MARK: - The handler for the Actions the state machine recommends to do
48 | extension SaveEverythingHTTPServer {
49 | func runAction(_ action: FileIOCoordinatorState.Action, context: ChannelHandlerContext) {
50 | self.logger.trace("doing action \(action)")
51 | switch action.main {
52 | case .closeFile(let fileHandle):
53 | try! fileHandle.close()
54 | case .processingCompletedDiscardResources(let fileHandle, let maybeError):
55 | try! fileHandle?.close()
56 | self.logger.debug("fully handled request: \(maybeError.debugDescription)")
57 | self.requestFullyProcessed(context: context, result: maybeError.map { .failure($0) } ?? .success(()))
58 | case .openFile(let path):
59 | self.fileIO.openFile(
60 | path: path,
61 | mode: .write,
62 | flags: .allowFileCreation(posixMode: 0o600),
63 | eventLoop: context.eventLoop
64 | ).flatMap { fileHandle in
65 | self.fileIO.changeFileSize(
66 | fileHandle: fileHandle,
67 | size: 0,
68 | eventLoop: context.eventLoop
69 | ).map { fileHandle }
70 | }.whenComplete { result in
71 | switch result {
72 | case .success(let fileHandle):
73 | self.runAction(
74 | self.state.didOpenTargetFile(fileHandle),
75 | context: context
76 | )
77 | case .failure(let error):
78 | self.runAction(
79 | self.state.didError(error),
80 | context: context
81 | )
82 | }
83 | }
84 | case .nothingWeAreWaiting:
85 | ()
86 | case .startWritingToTargetFile:
87 | let (fileHandle, bytes) = self.state.pullNextChunkToWrite()
88 | self.fileIO.write(fileHandle: fileHandle, buffer: bytes, eventLoop: context.eventLoop).whenComplete {
89 | result in
90 | switch result {
91 | case .success(()):
92 | self.runAction(self.state.didFinishWritingOneChunkToFile(), context: context)
93 | case .failure(let error):
94 | self.runAction(self.state.didError(error), context: context)
95 | }
96 | }
97 | }
98 | if action.callRead {
99 | context.read()
100 | }
101 | }
102 | }
103 |
104 | // MARK: - Finishing the request
105 | extension SaveEverythingHTTPServer {
106 | func requestFullyProcessed(context: ChannelHandlerContext, result: Result) {
107 | switch result {
108 | case .success:
109 | context.write(
110 | self.wrapOutboundOut(
111 | HTTPServerResponsePart.head(
112 | .init(
113 | version: .init(
114 | major: 1,
115 | minor: 1
116 | ),
117 | status: .ok,
118 | headers: ["content-length": "0"]
119 | )
120 | )
121 | ),
122 | promise: nil
123 | )
124 | context.writeAndFlush(self.wrapOutboundOut(HTTPServerResponsePart.end(nil)), promise: nil)
125 | case .failure(let error):
126 | let errorPage = "ERROR on \(context.channel): \(error)"
127 | context.write(
128 | self.wrapOutboundOut(
129 | HTTPServerResponsePart.head(
130 | .init(
131 | version: .init(
132 | major: 1,
133 | minor: 1
134 | ),
135 | status: .internalServerError,
136 | headers: [
137 | "connection": "close",
138 | "content-length": "\(errorPage.utf8.count)",
139 | ]
140 | )
141 | )
142 | ),
143 | promise: nil
144 | )
145 | var buffer = context.channel.allocator.buffer(capacity: errorPage.utf8.count)
146 | buffer.writeString(errorPage)
147 | context.write(self.wrapOutboundOut(HTTPServerResponsePart.body(.byteBuffer(buffer))), promise: nil)
148 | context.writeAndFlush(self.wrapOutboundOut(HTTPServerResponsePart.end(nil))).whenComplete { _ in
149 | context.close(promise: nil)
150 | }
151 | }
152 | }
153 | }
154 |
155 | // MARK: - ChannelHandler conformance
156 | extension SaveEverythingHTTPServer: ChannelDuplexHandler {
157 | public typealias InboundIn = HTTPServerRequestPart
158 | public typealias OutboundIn = Never
159 | public typealias OutboundOut = HTTPServerResponsePart
160 |
161 | public func errorCaught(context: ChannelHandlerContext, error: Error) {
162 | self.logger.info("error on channel: \(error)")
163 | self.runAction(self.state.didError(error), context: context)
164 | }
165 |
166 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
167 | let reqPart = self.unwrapInboundIn(data)
168 |
169 | switch reqPart {
170 | case .head(let request):
171 | self.runAction(
172 | self.state.didReceiveRequestBegin(targetPath: self.filenameForURI(request.uri)),
173 | context: context
174 | )
175 | case .body(let bytes):
176 | self.runAction(self.state.didReceiveRequestBodyBytes(bytes), context: context)
177 | case .end:
178 | self.runAction(self.state.didReceiveRequestEnd(), context: context)
179 | }
180 | }
181 |
182 | public func read(context: ChannelHandlerContext) {
183 | if self.state.shouldWeReadMoreDataFromNetwork() {
184 | context.read()
185 | }
186 | }
187 | }
188 |
189 | // MARK: - Helpers
190 | extension SaveEverythingHTTPServer {
191 | func filenameForURI(_ uri: String) -> String {
192 | var result = "\(self.uploadDirectory)/uploaded_file_"
193 | result.append(
194 | contentsOf: uri.map { char in
195 | switch char {
196 | case "A"..."Z", "a"..."z", "0"..."9":
197 | return char
198 | default:
199 | return "_"
200 | }
201 | }
202 | )
203 | return result
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/backpressure-file-io-channel/Sources/BackpressureChannelToFileIODemo/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import BackpressureChannelToFileIO
16 | import Logging
17 | import NIOCore
18 | import NIOPosix
19 |
20 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
21 | defer {
22 | try! group.syncShutdownGracefully()
23 | }
24 |
25 | let threadPool = NIOThreadPool(numberOfThreads: 1)
26 | threadPool.start()
27 | defer {
28 | try! threadPool.syncShutdownGracefully()
29 | }
30 |
31 | let fileIO = NonBlockingFileIO(threadPool: threadPool)
32 |
33 | let logger: Logger = {
34 | var logger = Logger(label: "BackpressureChannelToFileIO")
35 | logger.logLevel = .info
36 | return logger
37 | }()
38 |
39 | let server = try ServerBootstrap(group: group)
40 | .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
41 | .childChannelInitializer { [logger] channel in
42 | var logger = logger
43 | logger[metadataKey: "connection"] = "\(channel.remoteAddress!)"
44 | return channel.pipeline.configureHTTPServerPipeline(withErrorHandling: false).flatMap { [logger] in
45 | channel.eventLoop.makeCompletedFuture {
46 | try channel.pipeline.syncOperations.addHandler(
47 | SaveEverythingHTTPServer(
48 | fileIO: fileIO,
49 | uploadDirectory: "/tmp",
50 | logger: logger
51 | )
52 | )
53 | }
54 | }
55 | }
56 | .bind(host: "localhost", port: 8080)
57 | .wait()
58 | logger.info("Server up and running at \(server.localAddress!)")
59 | try! server.closeFuture.wait()
60 |
--------------------------------------------------------------------------------
/backpressure-file-io-channel/Tests/BackpressureChannelToFileIOTests/IntegrationTest.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import BackpressureChannelToFileIO
16 | import NIOCore
17 | import NIOPosix
18 | import XCTest
19 |
20 | final class IntegrationTest: XCTestCase {
21 | private var channel: Channel!
22 | private var group: EventLoopGroup!
23 | private var fileIO: NonBlockingFileIO!
24 | private var threadPool: NIOThreadPool!
25 | private var testToChannel: FileHandle!
26 | private var channelToTest: FileHandle!
27 | private var tempDir: String!
28 |
29 | func testBasicRoundtrip() {
30 | let twoReqs =
31 | "POST /foo HTTP/1.1\r\ncontent-length: 5\r\n\r\nabcde"
32 | + "POST /bar HTTP/1.1\r\ncontent-length: 3\r\n\r\nfoo"
33 | self.testToChannel.write(Data(twoReqs.utf8))
34 | let results = (0..<6).compactMap { _ in self.channelToTest.readLine() }
35 | guard results.count == 6 else {
36 | XCTFail("couldn't read results")
37 | return
38 | }
39 |
40 | XCTAssertEqual(Data("HTTP/1.1 200 OK\r\n".utf8), results[0])
41 | XCTAssertEqual(Data("HTTP/1.1 200 OK\r\n".utf8), results[3])
42 |
43 | XCTAssertNoThrow(
44 | XCTAssertEqual(
45 | Data("abcde".utf8),
46 | try Data(contentsOf: URL(fileURLWithPath: "\(self.tempDir!)/uploaded_file__foo"))
47 | )
48 | )
49 | XCTAssertNoThrow(
50 | XCTAssertEqual(
51 | Data("foo".utf8),
52 | try Data(contentsOf: URL(fileURLWithPath: "\(self.tempDir!)/uploaded_file__bar"))
53 | )
54 | )
55 | }
56 |
57 | func testWeSurviveTheChannelGoingAwayWhilstWriting() {
58 | let semaphore = DispatchSemaphore(value: 0)
59 | let destinationFilePath = "\(self.tempDir!)/uploaded_file__"
60 |
61 | // Let's write the request but not the body, that should open the file.
62 | self.testToChannel.write(Data("POST / HTTP/1.1\r\ncontent-length: 1\r\n\r\n".utf8))
63 | while !FileManager.default.fileExists(atPath: destinationFilePath) {
64 | Thread.sleep(forTimeInterval: 0.1)
65 | }
66 |
67 | // Then, let's block the ThreadPool so the writes won't be able to happen.
68 | let blockedItem = self.threadPool.runIfActive(eventLoop: self.group.next()) {
69 | semaphore.wait()
70 | }
71 |
72 | // And write a byte.
73 | self.testToChannel.write(Data("X".utf8))
74 |
75 | final class InjectReadHandler: ChannelInboundHandler {
76 | typealias InboundIn = ByteBuffer
77 |
78 | func handlerAdded(context: ChannelHandlerContext) {
79 | context.read()
80 | }
81 | }
82 |
83 | // Now, let's close the input side of the channel, which should actually close the whole channel, because
84 | // we have half-closure disabled (default).
85 | XCTAssertNoThrow(try self.testToChannel.close())
86 | self.testToChannel = nil // So tearDown doesn't close it again.
87 |
88 | // To make sure that EOF is seen, we'll inject a `read()` because otherwise there won't be reads because the
89 | // HTTP server implements backpressure correctly... The read injection handler has to go at the very beginning
90 | // of the pipeline so the HTTP server can't hold that `read()`.
91 | XCTAssertNoThrow(try self.channel.pipeline.addHandler(InjectReadHandler(), position: .first).wait())
92 | XCTAssertNoThrow(try self.channel.closeFuture.wait())
93 | self.channel = nil // So tearDown doesn't close it again.
94 |
95 | // The write can't have happened yet (because the thread pool's blocked).
96 | XCTAssertNoThrow(XCTAssertEqual(Data(), try Data(contentsOf: URL(fileURLWithPath: destinationFilePath))))
97 |
98 | // Now, let's kick off the writes.
99 | semaphore.signal()
100 | XCTAssertNoThrow(try blockedItem.wait())
101 |
102 | // And wait for the write to actually happen :).
103 | while Data("X".utf8) != (try? Data(contentsOf: URL(fileURLWithPath: destinationFilePath))) {
104 | Thread.sleep(forTimeInterval: 0.1)
105 | }
106 | }
107 | }
108 |
109 | extension IntegrationTest {
110 | override func setUp() {
111 | XCTAssertNil(self.channel)
112 | XCTAssertNil(self.group)
113 | XCTAssertNil(self.fileIO)
114 | XCTAssertNil(self.threadPool)
115 | XCTAssertNil(self.testToChannel)
116 | XCTAssertNil(self.channelToTest)
117 |
118 | guard
119 | let temp = try? FileManager.default.url(
120 | for: .itemReplacementDirectory,
121 | in: .userDomainMask,
122 | appropriateFor: URL(string: "/")!,
123 | create: true
124 | )
125 | else {
126 | XCTFail("can't create temp dir")
127 | return
128 |
129 | }
130 | self.tempDir = temp.path
131 | self.threadPool = NIOThreadPool(numberOfThreads: 1)
132 | self.threadPool.start()
133 | self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
134 | self.fileIO = NonBlockingFileIO(threadPool: threadPool)
135 | let testToChannel = Pipe()
136 | let channelToTest = Pipe()
137 |
138 | var maybeChannel: Channel? = nil
139 | XCTAssertNoThrow(
140 | try maybeChannel = NIOPipeBootstrap(group: group)
141 | .channelInitializer { channel in
142 | channel.pipeline.configureHTTPServerPipeline(withErrorHandling: false).flatMap {
143 | channel.pipeline.addHandler(
144 | SaveEverythingHTTPServer(
145 | fileIO: self.fileIO,
146 | uploadDirectory: self.tempDir
147 | )
148 | )
149 | }
150 | }
151 | .withPipes(
152 | inputDescriptor: dup(testToChannel.fileHandleForReading.fileDescriptor),
153 | outputDescriptor: dup(channelToTest.fileHandleForWriting.fileDescriptor)
154 | )
155 | .wait()
156 | )
157 | guard let channel = maybeChannel else {
158 | XCTFail("can't get a Channel")
159 | return
160 | }
161 | self.testToChannel = FileHandle(fileDescriptor: dup(testToChannel.fileHandleForWriting.fileDescriptor))
162 | self.channelToTest = FileHandle(fileDescriptor: dup(channelToTest.fileHandleForReading.fileDescriptor))
163 | self.channel = channel
164 | }
165 |
166 | override func tearDown() {
167 | XCTAssertNoThrow(try self.channel?.close().wait())
168 | XCTAssertNoThrow(try self.testToChannel?.close())
169 | XCTAssertNoThrow(try self.channelToTest?.close())
170 | XCTAssertNoThrow(try self.group?.syncShutdownGracefully())
171 | XCTAssertNoThrow(try self.threadPool?.syncShutdownGracefully())
172 | XCTAssertNoThrow(try FileManager.default.removeItem(atPath: self.tempDir))
173 |
174 | self.channel = nil
175 | self.group = nil
176 | self.fileIO = nil
177 | self.threadPool = nil
178 | self.testToChannel = nil
179 | self.channelToTest = nil
180 | self.tempDir = nil
181 | }
182 | }
183 |
184 | extension FileHandle {
185 | func readLine() -> Data? {
186 | var target = Data()
187 | var char: UInt8 = .max
188 | repeat {
189 | if let c = self.readData(ofLength: 1).first {
190 | char = c
191 | target.append(c)
192 | } else {
193 | return nil
194 | }
195 | } while char != UInt8(ascii: "\n")
196 | return target
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/connect-proxy/.gitignore:
--------------------------------------------------------------------------------
1 | *.xcodeproj
2 |
--------------------------------------------------------------------------------
/connect-proxy/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2019-2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "nio-connect-proxy",
38 | products: [
39 | .executable(name: "ConnectProxy", targets: ["ConnectProxy"]),
40 | ],
41 | dependencies: [
42 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
43 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
44 | ],
45 | targets: [
46 | .executableTarget(
47 | name: "ConnectProxy",
48 | dependencies: [
49 | .product(name: "NIOCore", package: "swift-nio"),
50 | .product(name: "NIOPosix", package: "swift-nio"),
51 | .product(name: "NIOHTTP1", package: "swift-nio"),
52 | .product(name: "Logging", package: "swift-log"),
53 | ],
54 | swiftSettings: strictConcurrencySettings
55 | ),
56 | ]
57 | )
58 |
--------------------------------------------------------------------------------
/connect-proxy/Sources/ConnectProxy/ConnectProxyError.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | enum ConnectProxyError: Error {
16 | case invalidHTTPMessageOrdering
17 | }
18 |
--------------------------------------------------------------------------------
/connect-proxy/Sources/ConnectProxy/GlueHandler.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import NIOCore
16 |
17 | final class GlueHandler {
18 |
19 | private var partner: GlueHandler?
20 |
21 | private var context: ChannelHandlerContext?
22 |
23 | private var pendingRead: Bool = false
24 |
25 | private init() {}
26 | }
27 |
28 | extension GlueHandler {
29 | static func matchedPair() -> (GlueHandler, GlueHandler) {
30 | let first = GlueHandler()
31 | let second = GlueHandler()
32 |
33 | first.partner = second
34 | second.partner = first
35 |
36 | return (first, second)
37 | }
38 | }
39 |
40 | extension GlueHandler {
41 | private func partnerWrite(_ data: NIOAny) {
42 | self.context?.write(data, promise: nil)
43 | }
44 |
45 | private func partnerFlush() {
46 | self.context?.flush()
47 | }
48 |
49 | private func partnerWriteEOF() {
50 | self.context?.close(mode: .output, promise: nil)
51 | }
52 |
53 | private func partnerCloseFull() {
54 | self.context?.close(promise: nil)
55 | }
56 |
57 | private func partnerBecameWritable() {
58 | if self.pendingRead {
59 | self.pendingRead = false
60 | self.context?.read()
61 | }
62 | }
63 |
64 | private var partnerWritable: Bool {
65 | self.context?.channel.isWritable ?? false
66 | }
67 | }
68 |
69 | extension GlueHandler: ChannelDuplexHandler {
70 | typealias InboundIn = NIOAny
71 | typealias OutboundIn = NIOAny
72 | typealias OutboundOut = NIOAny
73 |
74 | func handlerAdded(context: ChannelHandlerContext) {
75 | self.context = context
76 | }
77 |
78 | func handlerRemoved(context: ChannelHandlerContext) {
79 | self.context = nil
80 | self.partner = nil
81 | }
82 |
83 | func channelRead(context: ChannelHandlerContext, data: NIOAny) {
84 | self.partner?.partnerWrite(data)
85 | }
86 |
87 | func channelReadComplete(context: ChannelHandlerContext) {
88 | self.partner?.partnerFlush()
89 | }
90 |
91 | func channelInactive(context: ChannelHandlerContext) {
92 | self.partner?.partnerCloseFull()
93 | }
94 |
95 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
96 | if let event = event as? ChannelEvent, case .inputClosed = event {
97 | // We have read EOF.
98 | self.partner?.partnerWriteEOF()
99 | }
100 | }
101 |
102 | func errorCaught(context: ChannelHandlerContext, error: Error) {
103 | self.partner?.partnerCloseFull()
104 | }
105 |
106 | func channelWritabilityChanged(context: ChannelHandlerContext) {
107 | if context.channel.isWritable {
108 | self.partner?.partnerBecameWritable()
109 | }
110 | }
111 |
112 | func read(context: ChannelHandlerContext) {
113 | if let partner = self.partner, partner.partnerWritable {
114 | context.read()
115 | } else {
116 | self.pendingRead = true
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/connect-proxy/Sources/ConnectProxy/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Dispatch
16 | import Logging
17 | import NIOCore
18 | import NIOHTTP1
19 | import NIOPosix
20 |
21 | let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
22 | let bootstrap = ServerBootstrap(group: group)
23 | .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
24 | .childChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
25 | .childChannelInitializer { channel in
26 | channel.eventLoop.makeCompletedFuture {
27 | try channel.pipeline.syncOperations.addHandler(
28 | ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes))
29 | )
30 | try channel.pipeline.syncOperations.addHandler(HTTPResponseEncoder())
31 | try channel.pipeline.syncOperations.addHandler(
32 | ConnectHandler(logger: Logger(label: "com.apple.nio-connect-proxy.ConnectHandler"))
33 | )
34 | }
35 | }
36 |
37 | bootstrap.bind(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 8080)).whenComplete { result in
38 | // Need to create this here for thread-safety purposes
39 | let logger = Logger(label: "com.apple.nio-connect-proxy.main")
40 |
41 | switch result {
42 | case .success(let channel):
43 | logger.info("Listening on \(String(describing: channel.localAddress))")
44 | case .failure(let error):
45 | logger.error("Failed to bind 127.0.0.1:8080, \(error)")
46 | }
47 | }
48 |
49 | bootstrap.bind(to: try! SocketAddress(ipAddress: "::1", port: 8080)).whenComplete { result in
50 | // Need to create this here for thread-safety purposes
51 | let logger = Logger(label: "com.apple.nio-connect-proxy.main")
52 |
53 | switch result {
54 | case .success(let channel):
55 | logger.info("Listening on \(String(describing: channel.localAddress))")
56 | case .failure(let error):
57 | logger.error("Failed to bind [::1]:8080, \(error)")
58 | }
59 | }
60 |
61 | // Run forever
62 | dispatchMain()
63 |
--------------------------------------------------------------------------------
/dev/build_all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ##===----------------------------------------------------------------------===##
3 | ##
4 | ## This source file is part of the SwiftNIO open source project
5 | ##
6 | ## Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
7 | ## Licensed under Apache License v2.0
8 | ##
9 | ## See LICENSE.txt for license information
10 | ## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | ##
12 | ## SPDX-License-Identifier: Apache-2.0
13 | ##
14 | ##===----------------------------------------------------------------------===##
15 |
16 | set -euo pipefail
17 |
18 | log() { printf -- "** %s\n" "$*" >&2; }
19 | error() { printf -- "** ERROR: %s\n" "$*" >&2; }
20 | fatal() { error "$@"; exit 1; }
21 |
22 | default_package_directories="$(while read -r manifest; do dirname "$manifest"; done < <(ls -1 ./*/Package.swift))"
23 | default_project_directories="$(while read -r manifest; do dirname "$manifest"; done < <(ls -1 ./*/*.xcodeproj))"
24 |
25 | # --
26 | strict_concurrency="${STRICT_CONCURRENCY:-""}"
27 | xcode_build_enabled="${XCODE_BUILD_ENABLED:-""}"
28 |
29 | package_directories="${SWIFT_PACKAGE_DIRECTORIES:-$default_package_directories}"
30 | project_directories="${XCODE_PROJECT_DIRECTORIES:-$default_project_directories}"
31 |
32 | # --
33 |
34 | if [ -n "$strict_concurrency" ]; then
35 | swift_build_command="swift build -Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
36 | else
37 | swift_build_command="swift build"
38 | fi
39 |
40 | xcode_build_command="xcodebuild -project NIOSMTP.xcodeproj -scheme NIOSMTP -arch x86_64 -sdk iphonesimulator"
41 |
42 | for directory in $package_directories; do
43 | log "Building: $directory"
44 | $swift_build_command --package-path "$directory"
45 | done
46 |
47 | if [ -n "$xcode_build_enabled" ]; then
48 | for directory in $project_directories; do
49 | log "Building: $directory"
50 | cd "$directory" || fatal "Could not cd to ${directory}."
51 | $xcode_build_command
52 | cd .. || fatal "Could not cd to parent."
53 | done
54 | fi
--------------------------------------------------------------------------------
/dev/git.commit.template:
--------------------------------------------------------------------------------
1 | One line description of your change
2 |
3 | Motivation:
4 |
5 | Explain here the context, and why you're making that change.
6 | What is the problem you're trying to solve.
7 |
8 | Modifications:
9 |
10 | Describe the modifications you've done.
11 |
12 | Result:
13 |
14 | After your change, what will change.
15 |
--------------------------------------------------------------------------------
/http-responsiveness-server/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "nio-http-responsiveness-server",
38 | platforms: [
39 | .macOS(.v14)
40 | ],
41 | products: [
42 | .executable(name: "HTTPResponsivenessServer", targets: ["HTTPResponsivenessServer"])
43 | ],
44 | dependencies: [
45 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.79.0"),
46 | .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.35.0"),
47 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.27.0"),
48 | .package(
49 | url: "https://github.com/apple/swift-nio-extras.git",
50 | revision: "4804de1953c14ce71cfca47a03fb4581a6b3301c"
51 | ),
52 | .package(url: "https://github.com/apple/swift-http-types.git", from: "1.1.0"),
53 | .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"),
54 | .package(url: "https://github.com/swift-extras/swift-extras-json.git", from: "0.6.0"),
55 | .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.23.0"),
56 | ],
57 | targets: [
58 | .executableTarget(
59 | name: "HTTPResponsivenessServer",
60 | dependencies: [
61 | .product(name: "NIOCore", package: "swift-nio"),
62 | .product(name: "NIOPosix", package: "swift-nio"),
63 | .product(name: "NIOHTTP1", package: "swift-nio"),
64 | .product(name: "NIOHTTP2", package: "swift-nio-http2"),
65 | .product(name: "NIOSSL", package: "swift-nio-ssl"),
66 | .product(name: "ArgumentParser", package: "swift-argument-parser"),
67 | .product(name: "NIOHTTPTypesHTTP2", package: "swift-nio-extras"),
68 | .product(name: "NIOHTTPTypesHTTP1", package: "swift-nio-extras"),
69 | .product(name: "NIOHTTPResponsiveness", package: "swift-nio-extras"),
70 | .product(name: "ExtrasJSON", package: "swift-extras-json"),
71 | .product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
72 | ],
73 | swiftSettings: strictConcurrencySettings
74 | )
75 | ]
76 | )
77 |
--------------------------------------------------------------------------------
/http2-client/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | Package.pins
6 | *.pem
7 | /docs
8 | Package.resolved
9 | .podspecs
10 |
--------------------------------------------------------------------------------
/http2-client/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "http2-client",
38 | dependencies: [
39 | // Dependencies declare other packages that this package depends on.
40 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
41 | .package(url: "https://github.com/apple/swift-nio-ssl", from: "2.6.0"),
42 | .package(url: "https://github.com/apple/swift-nio-http2", from: "1.9.0"),
43 | .package(url: "https://github.com/apple/swift-nio-extras", from: "1.0.0"),
44 | ],
45 | targets: [
46 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
47 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
48 | .executableTarget(
49 | name: "http2-client",
50 | dependencies: [
51 | .product(name: "NIOCore", package: "swift-nio"),
52 | .product(name: "NIOPosix", package: "swift-nio"),
53 | .product(name: "NIOTLS", package: "swift-nio"),
54 | .product(name: "NIOSSL", package: "swift-nio-ssl"),
55 | .product(name: "NIOHTTP1", package: "swift-nio"),
56 | .product(name: "NIOHTTP2", package: "swift-nio-http2"),
57 | .product(name: "NIOExtras", package: "swift-nio-extras"),
58 | ],
59 | swiftSettings: strictConcurrencySettings
60 | ),
61 | ]
62 | )
63 |
--------------------------------------------------------------------------------
/http2-client/Sources/http2-client/Types.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import NIOHTTP1
16 |
17 | struct HostAndPort: Equatable, Hashable {
18 | var host: String
19 | var port: Int
20 | }
21 |
22 | public struct HTTPRequest {
23 | class _Storage {
24 | var method: HTTPMethod
25 | var target: String
26 | var version: HTTPVersion
27 | var headers: [(String, String)]
28 | var body: [UInt8]?
29 | var trailers: [(String, String)]?
30 |
31 | init(
32 | method: HTTPMethod = .GET,
33 | target: String,
34 | version: HTTPVersion,
35 | headers: [(String, String)],
36 | body: [UInt8]?,
37 | trailers: [(String, String)]?
38 | ) {
39 | self.method = method
40 | self.target = target
41 | self.version = version
42 | self.headers = headers
43 | self.body = body
44 | self.trailers = trailers
45 | }
46 |
47 | }
48 |
49 | private var _storage: _Storage
50 |
51 | public init(
52 | method: HTTPMethod = .GET,
53 | target: String,
54 | version: HTTPVersion = HTTPVersion(major: 1, minor: 1),
55 | headers: [(String, String)],
56 | body: [UInt8]?,
57 | trailers: [(String, String)]?
58 | ) {
59 | self._storage = _Storage(
60 | method: method,
61 | target: target,
62 | version: version,
63 | headers: headers,
64 | body: body,
65 | trailers: trailers
66 | )
67 | }
68 | }
69 |
70 | extension HTTPRequest._Storage {
71 | func copy() -> HTTPRequest._Storage {
72 | HTTPRequest._Storage(
73 | method: self.method,
74 | target: self.target,
75 | version: self.version,
76 | headers: self.headers,
77 | body: self.body,
78 | trailers: self.trailers
79 | )
80 | }
81 | }
82 |
83 | extension HTTPRequest {
84 | public var method: HTTPMethod {
85 | get {
86 | self._storage.method
87 | }
88 | set {
89 | if !isKnownUniquelyReferenced(&self._storage) {
90 | self._storage = self._storage.copy()
91 | }
92 | self._storage.method = newValue
93 | }
94 | }
95 |
96 | public var target: String {
97 | get {
98 | self._storage.target
99 | }
100 | set {
101 | if !isKnownUniquelyReferenced(&self._storage) {
102 | self._storage = self._storage.copy()
103 | }
104 | self._storage.target = newValue
105 | }
106 | }
107 |
108 | public var version: HTTPVersion {
109 | get {
110 | self._storage.version
111 | }
112 | set {
113 | if !isKnownUniquelyReferenced(&self._storage) {
114 | self._storage = self._storage.copy()
115 | }
116 | self._storage.version = newValue
117 | }
118 | }
119 |
120 | public var headers: [(String, String)] {
121 | get {
122 | self._storage.headers
123 | }
124 | set {
125 | if !isKnownUniquelyReferenced(&self._storage) {
126 | self._storage = self._storage.copy()
127 | }
128 | self._storage.headers = newValue
129 | }
130 | }
131 |
132 | public var body: [UInt8]? {
133 | get {
134 | self._storage.body
135 | }
136 | set {
137 | if !isKnownUniquelyReferenced(&self._storage) {
138 | self._storage = self._storage.copy()
139 | }
140 | self._storage.body = newValue
141 | }
142 | }
143 |
144 | public var trailers: [(String, String)]? {
145 | get {
146 | self._storage.trailers
147 | }
148 | set {
149 | if !isKnownUniquelyReferenced(&self._storage) {
150 | self._storage = self._storage.copy()
151 | }
152 | self._storage.trailers = newValue
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/http2-client/scripts/test_top_sites.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ##===----------------------------------------------------------------------===##
3 | ##
4 | ## This source file is part of the SwiftNIO open source project
5 | ##
6 | ## Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
7 | ## Licensed under Apache License v2.0
8 | ##
9 | ## See LICENSE.txt for license information
10 | ## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | ##
12 | ## SPDX-License-Identifier: Apache-2.0
13 | ##
14 | ##===----------------------------------------------------------------------===##
15 |
16 | set -eu
17 |
18 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
19 | cd "$here/.."
20 |
21 | tmp=$(mktemp -d /tmp/.test_top_sites_XXXXXX)
22 | nio_errors=0
23 |
24 | echo -n 'compiling...'
25 | swift run http2-client https://google.com > "$tmp/compiling" 2>&1 || { cat "$tmp/compiling"; exit 1; }
26 | echo OK
27 |
28 | while read -r site; do
29 | url="https://$site"
30 | is_http2=true
31 | echo "testing $url"
32 | if curl --connect-timeout 10 --http2-prior-knowledge -Iv "$url" > "$tmp/curl" 2>&1; then
33 | if grep -q HTTP/1 "$tmp/curl"; then
34 | echo -n 'curl HTTP/1.x only'
35 | is_http2=false
36 | else
37 | echo -n 'curl HTTP/2 ok '
38 | fi
39 | else
40 | is_http2=false
41 | echo -n 'curl failed '
42 | fi
43 |
44 | if swift run http2-client "$url" > "$tmp/nio"; then
45 | echo '; NIO ok'
46 | else
47 | if grep -q serverDoesNotSpeakHTTP2 "$tmp/nio"; then
48 | if $is_http2; then
49 | nio_errors=$(( nio_errors + 1 ))
50 | echo '; NIO WRONGLY detected no HTTP/2'
51 | else
52 | echo '; NIO correctly detected no HTTP/2'
53 | fi
54 | else
55 | nio_errors=$(( nio_errors + 1 ))
56 | echo '; NIO DID NOT DETECT MISSING HTTP/2'
57 | echo "--- NIO DEBUG INFO: BEGIN ---"
58 | cat "$tmp/nio"
59 | echo "--- NIO DEBUG INFO: END ---"
60 | fi
61 | fi
62 | done < <(curl -qs https://moz.com/top-500/download?table=top500Domains | sed 1d | head -n 100 | cut -d, -f2 | tr -d '"' | \
63 | grep -v -e ^qq.com -e ^go.com -e ^who.int)
64 | rm -rf "$tmp"
65 | exit "$nio_errors"
66 |
--------------------------------------------------------------------------------
/http2-server/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | Package.resolved
7 | .swiftpm
8 |
--------------------------------------------------------------------------------
/http2-server/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2019-2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "http2-server",
38 | dependencies: [
39 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.42.0"),
40 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.6.0"),
41 | .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.9.0"),
42 | ],
43 | targets: [
44 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
45 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
46 | .executableTarget(
47 | name: "http2-server",
48 | dependencies: [
49 | .product(name: "NIOCore", package: "swift-nio"),
50 | .product(name: "NIOPosix", package: "swift-nio"),
51 | .product(name: "NIOSSL", package: "swift-nio-ssl"),
52 | .product(name: "NIOHTTP1", package: "swift-nio"),
53 | .product(name: "NIOHTTP2", package: "swift-nio-http2"),
54 | ],
55 | swiftSettings: strictConcurrencySettings
56 | ),
57 | ]
58 | )
59 |
--------------------------------------------------------------------------------
/http2-server/README.md:
--------------------------------------------------------------------------------
1 | # http2-server
2 |
3 | This is a very simple example HTTP/2 server that configures TLS (and ALPN)
4 | similar to how you would configure it in a real system.
5 |
6 | To run, just open the project in Xcode or type
7 |
8 | swift run
9 |
10 |
--------------------------------------------------------------------------------
/http2-server/Sources/http2-server/HardcodedPrivateKeyAndCerts.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | // WARNING
16 | // =======
17 | //
18 | // This is an example certificate/private key. DEFINITELY DO NOT USE THESE IN PRODUCTION.
19 | // The only reason they are here is to make the example server easier to start.
20 |
21 | let samplePemCert = """
22 | -----BEGIN CERTIFICATE-----
23 | MIIC+zCCAeOgAwIBAgIJANG6W1v704/aMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
24 | BAMMCWxvY2FsaG9zdDAeFw0xOTA4MDExMDMzMjhaFw0yOTA3MjkxMDMzMjhaMBQx
25 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
26 | ggEBAMLw9InBMGKUNZKXFIpjUYt+Tby42GRQaRFmHfUrlYkvI9L7i9cLqltX/Pta
27 | XL9zISJIEgIgOW1R3pQ4xRP3xC+C3lKpo5lnD9gaMnDIsXhXLQzvTo+tFgtShXsU
28 | /xGl4U2Oc2BbPmydd+sfOPKXOYk/0TJsuSb1U5pA8FClyJUrUlykHkN120s5GhfA
29 | P2KYP+RMZuaW7gNlDEhiInqYUxBpLE+qutAYIDdpKWgxmHKW1oLhZ70TT1Zs7tUI
30 | 22ydjo81vbtB4214EDX0KRRBep+Kq9vTigss34CwhYvyhaCP6l305Z9Vjtu1q1vp
31 | a3nfMeVtcg6JDn3eogv0CevZMc0CAwEAAaNQME4wHQYDVR0OBBYEFK6KIoQAlLog
32 | bBT3snTQ22x5gmXQMB8GA1UdIwQYMBaAFK6KIoQAlLogbBT3snTQ22x5gmXQMAwG
33 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAEgoqcGDMriG4cCxWzuiXuV7
34 | 3TthA8TbdHQOeucNvXt9b3HUG1fQo7a0Tv4X3136SfCy3SsXXJr43snzVUK9SuNb
35 | ntqhAOIudZNw8KSwe+qJwmSEO4y3Lwr5pFfUUkGkV4K86wv3LmBpo3jep5hbkpAc
36 | kvbzTynFrOILV0TaDkF46KHIoyAb5vPneapdW7rXbX1Jba3jo9PyvHRMeoh/I8zt
37 | 4g+Je2PpH1TJ/GT9dmYhYgJaIssVpv/fWkWphVXwMmpqiH9vEbln8piXHxvCj9XU
38 | y7uc6N1VUvIvygzUsR+20wjODoGiXp0g0cj+38n3oG5S9rBd1iGEPMAA/2lQS/0=
39 | -----END CERTIFICATE-----
40 | """
41 |
42 | let samplePKCS8PemPrivateKey = """
43 | -----BEGIN RSA PRIVATE KEY-----
44 | MIIEowIBAAKCAQEAwvD0icEwYpQ1kpcUimNRi35NvLjYZFBpEWYd9SuViS8j0vuL
45 | 1wuqW1f8+1pcv3MhIkgSAiA5bVHelDjFE/fEL4LeUqmjmWcP2BoycMixeFctDO9O
46 | j60WC1KFexT/EaXhTY5zYFs+bJ136x848pc5iT/RMmy5JvVTmkDwUKXIlStSXKQe
47 | Q3XbSzkaF8A/Ypg/5Exm5pbuA2UMSGIiephTEGksT6q60BggN2kpaDGYcpbWguFn
48 | vRNPVmzu1QjbbJ2OjzW9u0HjbXgQNfQpFEF6n4qr29OKCyzfgLCFi/KFoI/qXfTl
49 | n1WO27WrW+lred8x5W1yDokOfd6iC/QJ69kxzQIDAQABAoIBAQCX+KZ62cuxnh8h
50 | l3wg4oqIt788l9HCallugfBq2D5sQv6nlQiQbfyx1ydWgDx71/IFuq+nTp3WVpOx
51 | c4xYI7ii3WAaizsJ9SmJ6+pUuHB6A2QQiGLzaRkdXIjIyjaK+IlrH9lcTeWdYSlC
52 | eAW6QSBOmhypNc8lyu0Q/P0bshJsDow5iuy3d8PeT3klxgRPWjgjLZj0eUA0Orfp
53 | s6rC3t7wq8S8+YscKNS6dO0Vp2rF5ZHYYZ9kG5Y0PbAx24hDoYcgMJYJSw5LuR9D
54 | TkNcstHI8aKM7t9TZN0eXeLmzKXAbkD0uyaK0ZwI2panFDBjkjnkwS7FjHDusk1S
55 | Or36zCV1AoGBAOj8ALqa5y4HHl2QF8+dkH7eEFnKmExd1YX90eUuO1v7oTW4iQN+
56 | Z/me45exNDrG27+w8JqF66zH+WAfHv5Va0AUnTuFAyBmOEqit0m2vFzOLBgDGub1
57 | xOVYQQ5LetIbiXYU4H3IQDSO+UY27u1yYsgYMrO1qiyGgEkFSbK5xh6HAoGBANYy
58 | 3rv9ULu3ZzeLqmkO+uYxBaEzAzubahgcDniKrsKfLVywYlF1bzplgT9OdGRkwMR8
59 | K7K5s+6ehrIu8pOadP1fZO7GC7w5lYypbrH74E7mBXSP53NOOebKYpojPhxjMrtI
60 | HLOxGg742WY5MTtDZ81Va0TrhErb4PxccVQEIY4LAoGAc8TMw+y21Ps6jnlMK6D6
61 | rN/BNiziUogJ0qPWCVBYtJMrftssUe0c0z+tjbHC5zXq+ax9UfsbqWZQtv+f0fc1
62 | 7MiRfILSk+XXMNb7xogjvuW/qUrZskwLQ38ADI9a/04pluA20KmRpcwpd0dSn/BH
63 | v2+uufeaELfgxOf4v/Npy78CgYBqmqzB8QQCOPg069znJp52fEVqAgKE4wd9clE9
64 | awApOqGP9PUpx4GRFb2qrTg+Uuqhn478B3Jmux0ch0MRdRjulVCdiZGDn0Ev3Y+L
65 | I2lyuwZSCeDOQUuN8oH6Zrnd1P0FupEWWXk3pGBGgQZgkV6TEgUuKu0PeLlTwApj
66 | Hx84GwKBgHWqSoiaBml/KX+GBUDu8Yp0v+7dkNaiU/RVaSEOFl2wHkJ+bq4V+DX1
67 | lgofMC2QvBrSinEjHrQPZILl+lOq/ppDcnxhY/3bljsutcgHhIT7PKYDOxFqflMi
68 | ahwyQwRg2oQ2rBrBevgOKFEuIV62WfDYXi8SlT8QaZpTt2r4PYt4
69 | -----END RSA PRIVATE KEY-----
70 | """
71 |
--------------------------------------------------------------------------------
/http2-server/Sources/http2-server/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import NIOCore
16 | import NIOHTTP1
17 | import NIOHTTP2
18 | import NIOPosix
19 | import NIOSSL
20 |
21 | final class HTTP1TestServer: ChannelInboundHandler {
22 | public typealias InboundIn = HTTPServerRequestPart
23 | public typealias OutboundOut = HTTPServerResponsePart
24 |
25 | enum HTTP1TestServerError: Error {
26 | case noChannelOptions
27 | }
28 |
29 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
30 | guard case .end = self.unwrapInboundIn(data) else {
31 | return
32 | }
33 |
34 | // Insert an event loop tick here. This more accurately represents real workloads in SwiftNIO, which will not
35 | // re-entrantly write their response frames.
36 | context.eventLoop.assumeIsolated().execute {
37 | do {
38 | guard let streamID = try context.channel.syncOptions?.getOption(HTTP2StreamChannelOptions.streamID)
39 | else {
40 | throw HTTP1TestServerError.noChannelOptions
41 | }
42 | var headers = HTTPHeaders()
43 | headers.add(name: "content-length", value: "5")
44 | headers.add(name: "x-stream-id", value: String(Int(streamID)))
45 | context.channel.write(
46 | HTTPServerResponsePart.head(
47 | HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok, headers: headers)
48 | ),
49 | promise: nil
50 | )
51 |
52 | var buffer = context.channel.allocator.buffer(capacity: 12)
53 | buffer.writeStaticString("hello")
54 | context.channel.write(HTTPServerResponsePart.body(.byteBuffer(buffer)), promise: nil)
55 | context.channel.writeAndFlush(HTTPServerResponsePart.end(nil)).assumeIsolated().whenComplete { _ in
56 | context.close(promise: nil)
57 | }
58 | } catch {
59 | print("Encountered error on channelRead: \(error)")
60 | }
61 | }
62 | }
63 | }
64 |
65 | final class ErrorHandler: ChannelInboundHandler, Sendable {
66 | typealias InboundIn = Never
67 |
68 | func errorCaught(context: ChannelHandlerContext, error: Error) {
69 | print("Server received error: \(error)")
70 | context.close(promise: nil)
71 | }
72 | }
73 |
74 | // First argument is the program path
75 | let arguments = CommandLine.arguments
76 | let arg1 = arguments.dropFirst().first
77 | let arg2 = arguments.dropFirst().dropFirst().first
78 | let arg3 = arguments.dropFirst().dropFirst().dropFirst().first
79 |
80 | let defaultHost = "::1"
81 | let defaultPort: Int = 8888
82 | let defaultHtdocs = "/dev/null/"
83 |
84 | enum BindTo {
85 | case ip(host: String, port: Int)
86 | case unixDomainSocket(path: String)
87 | }
88 |
89 | let htdocs: String
90 | let bindTarget: BindTo
91 | switch (arg1, arg1.flatMap { Int($0) }, arg2, arg2.flatMap { Int($0) }, arg3) {
92 | case (.some(let h), _, _, .some(let p), let maybeHtdocs):
93 | // second arg an integer --> host port [htdocs]
94 | bindTarget = .ip(host: h, port: p)
95 | htdocs = maybeHtdocs ?? defaultHtdocs
96 | case (_, .some(let p), let maybeHtdocs, _, _):
97 | // first arg an integer --> port [htdocs]
98 | bindTarget = .ip(host: defaultHost, port: p)
99 | htdocs = maybeHtdocs ?? defaultHtdocs
100 | case (.some(let portString), .none, let maybeHtdocs, .none, .none):
101 | // couldn't parse as number --> uds-path [htdocs]
102 | bindTarget = .unixDomainSocket(path: portString)
103 | htdocs = maybeHtdocs ?? defaultHtdocs
104 | default:
105 | htdocs = defaultHtdocs
106 | bindTarget = BindTo.ip(host: defaultHost, port: defaultPort)
107 | }
108 |
109 | // The following lines load the example private key/cert from HardcodedPrivateKeyAndCerts.swift .
110 | // DO NOT USE THESE KEYS/CERTIFICATES IN PRODUCTION.
111 | // For a real server, you would obtain a real key/cert and probably put them in files and load them with
112 | //
113 | // NIOSSLPrivateKeySource.file("/path/to/private.key")
114 | // NIOSSLCertificateSource.file("/path/to/my.cert")
115 |
116 | // Load the private key
117 | let sslPrivateKey = try! NIOSSLPrivateKeySource.privateKey(
118 | NIOSSLPrivateKey(
119 | bytes: Array(samplePKCS8PemPrivateKey.utf8),
120 | format: .pem
121 | ) { providePassword in
122 | providePassword("thisisagreatpassword".utf8)
123 | }
124 | )
125 |
126 | // Load the certificate
127 | let sslCertificate = try! NIOSSLCertificateSource.certificate(
128 | NIOSSLCertificate(
129 | bytes: Array(samplePemCert.utf8),
130 | format: .pem
131 | )
132 | )
133 |
134 | // Set up the TLS configuration, it's important to set the `applicationProtocols` to
135 | // `NIOHTTP2SupportedALPNProtocols` which (using ALPN (https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation))
136 | // advertises the support of HTTP/2 to the client.
137 | var serverConfig = TLSConfiguration.makeServerConfiguration(
138 | certificateChain: [sslCertificate],
139 | privateKey: sslPrivateKey
140 | )
141 | serverConfig.applicationProtocols = ["h2"]
142 | // Configure the SSL context that is used by all SSL handlers.
143 | let sslContext = try! NIOSSLContext(configuration: serverConfig)
144 |
145 | let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
146 | let bootstrap = ServerBootstrap(group: group)
147 | // Specify backlog and enable SO_REUSEADDR for the server itself
148 | .serverChannelOption(ChannelOptions.backlog, value: 256)
149 | .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
150 |
151 | // Set the handlers that are applied to the accepted Channels
152 | .childChannelInitializer { channel in
153 | // First, we need an SSL handler because HTTP/2 is almost always spoken over TLS.
154 | channel.pipeline.eventLoop.makeCompletedFuture {
155 | try channel.pipeline.syncOperations.addHandler(NIOSSLServerHandler(context: sslContext))
156 | }.flatMapThrowing {
157 | // Right after the SSL handler, we can configure the HTTP/2 pipeline.
158 | _ = try channel.pipeline.syncOperations.configureHTTP2Pipeline(
159 | mode: .server,
160 | connectionConfiguration: .init(),
161 | streamConfiguration: .init()
162 | ) { streamChannel in
163 | streamChannel.pipeline.eventLoop.makeCompletedFuture {
164 | // For every HTTP/2 stream that the client opens, we put in the `HTTP2ToHTTP1ServerCodec` which
165 | // transforms the HTTP/2 frames to the HTTP/1 messages from the `NIOHTTP1` module.
166 | try streamChannel.pipeline.syncOperations.addHandler(HTTP2FramePayloadToHTTP1ServerCodec())
167 | // And lastly, we put in our very basic HTTP server :).
168 | try streamChannel.pipeline.syncOperations.addHandler(HTTP1TestServer())
169 | try streamChannel.pipeline.syncOperations.addHandler(ErrorHandler())
170 | }
171 | }
172 | }.flatMap {
173 | channel.pipeline.addHandler(ErrorHandler())
174 | }
175 | }
176 |
177 | // Enable TCP_NODELAY and SO_REUSEADDR for the accepted Channels
178 | .childChannelOption(ChannelOptions.socket(.init(IPPROTO_TCP), .init(TCP_NODELAY)), value: 1)
179 | .childChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
180 | .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
181 |
182 | defer {
183 | try! group.syncShutdownGracefully()
184 | }
185 |
186 | print("htdocs = \(htdocs)")
187 |
188 | let channel = try { () -> Channel in
189 | switch bindTarget {
190 | case .ip(let host, let port):
191 | return try bootstrap.bind(host: host, port: port).wait()
192 | case .unixDomainSocket(let path):
193 | return try bootstrap.bind(unixDomainSocketPath: path).wait()
194 | }
195 | }()
196 |
197 | print("Server started and listening on \(channel.localAddress!), htdocs path \(htdocs)")
198 | print("\nTry it out by running")
199 | print(" # WARNING: We're passing --insecure here because we don't have a real cert/private key!")
200 | print(" # In production NEVER use --insecure.")
201 | switch bindTarget {
202 | case .ip(let host, let port):
203 | let hostFormatted: String
204 | switch channel.localAddress!.protocol {
205 | case .inet6:
206 | hostFormatted = host.contains(":") ? "[\(host)]" : host
207 | default:
208 | hostFormatted = "\(host)"
209 | }
210 | print(" curl --insecure \"https://\(hostFormatted):\(port)/hello-world\"")
211 | case .unixDomainSocket(let path):
212 | print(" curl --insecure --unix-socket '\(path)' \"https://ignore-the-server.name/hello-world\"")
213 | }
214 |
215 | // This will never unblock as we don't close the ServerChannel
216 | try channel.closeFuture.wait()
217 |
218 | print("Server closed")
219 |
--------------------------------------------------------------------------------
/json-rpc/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | Package.pins
6 | *.pem
7 | /docs
8 | Package.resolved
9 | .podspecs
10 |
--------------------------------------------------------------------------------
/json-rpc/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "swift-json-rpc",
38 | products: [
39 | .library(name: "JSONRPC", targets: ["JSONRPC"]),
40 | .executable(name: "ServerExample", targets: ["ServerExample"]),
41 | .executable(name: "ClientExample", targets: ["ClientExample"]),
42 | .executable(name: "LightsdDemo", targets: ["LightsdDemo"]),
43 | ],
44 | dependencies: [
45 | .package(url: "https://github.com/apple/swift-nio", from: "2.42.0"),
46 | .package(url: "https://github.com/apple/swift-nio-extras", from: "1.0.0"),
47 | ],
48 | targets: [
49 | .target(
50 | name: "JSONRPC",
51 | dependencies: [
52 | .product(name: "NIOCore", package: "swift-nio"),
53 | .product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
54 | .product(name: "NIOPosix", package: "swift-nio"),
55 | .product(name: "NIOFoundationCompat", package: "swift-nio"),
56 | .product(name: "NIOExtras", package: "swift-nio-extras"),
57 | ],
58 | path: "Sources/JsonRpc"
59 | ),
60 | .executableTarget(
61 | name: "ServerExample",
62 | dependencies: [
63 | "JSONRPC"
64 | ],
65 | swiftSettings: strictConcurrencySettings
66 | ),
67 | .executableTarget(
68 | name: "ClientExample",
69 | dependencies: [
70 | "JSONRPC"
71 | ],
72 | swiftSettings: strictConcurrencySettings
73 | ),
74 | .executableTarget(
75 | name: "LightsdDemo",
76 | dependencies: [
77 | "JSONRPC"
78 | ],
79 | swiftSettings: strictConcurrencySettings
80 | ),
81 | .testTarget(
82 | name: "JSONRPCTests",
83 | dependencies: [
84 | "JSONRPC",
85 | .product(name: "NIOCore", package: "swift-nio"),
86 | .product(name: "NIOPosix", package: "swift-nio"),
87 | ],
88 | path: "Tests/JsonRpcTests",
89 | swiftSettings: strictConcurrencySettings
90 | ),
91 | ]
92 | )
93 |
--------------------------------------------------------------------------------
/json-rpc/README.md:
--------------------------------------------------------------------------------
1 | # JSONRPC
2 |
3 | a demo library that implements json-rpc protocol over tcp, using swift-nio. the library includes multiple framing alternatives since json-rpc spec does not define one
4 |
5 | # ClientExample
6 |
7 | a simple json-rpc client example built using the JSONRPC library
8 |
9 | # ServerExample
10 |
11 | a simple json-rpc server example built using the JSONRPC library
12 |
13 | # LightsdExample
14 |
15 | a simple lightds client example built using the JSONRPC library
16 |
--------------------------------------------------------------------------------
/json-rpc/Sources/ClientExample/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import JSONRPC
16 | import NIOCore
17 | import NIOPosix
18 |
19 | guard CommandLine.arguments.count > 1 else {
20 | fatalError("invalid arguments")
21 | }
22 |
23 | let address = ("127.0.0.1", 8000)
24 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
25 | let client = TCPClient(group: eventLoopGroup)
26 | _ = try! client.connect(host: address.0, port: address.1).wait()
27 | // perform the method call
28 | let method = CommandLine.arguments[1]
29 | let params = CommandLine.arguments[2...].map { Int($0) }.compactMap { $0 }
30 | let result = try! client.call(method: method, params: RPCObject(params)).wait()
31 | switch result {
32 | case .success(let response):
33 | print("\(response)")
34 | case .failure(let error):
35 | print("failed with \(error)")
36 | }
37 | // shutdown
38 | try! client.disconnect().wait()
39 |
--------------------------------------------------------------------------------
/json-rpc/Sources/JsonRpc/Client.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 | import NIOConcurrencyHelpers
17 | import NIOCore
18 | import NIOPosix
19 |
20 | public final class TCPClient: @unchecked Sendable {
21 | private let lock = NIOLock()
22 | private var state = State.initializing
23 | public let group: MultiThreadedEventLoopGroup
24 | public let config: Config
25 | private var channel: Channel?
26 |
27 | public init(group: MultiThreadedEventLoopGroup, config: Config = Config()) {
28 | self.group = group
29 | self.config = config
30 | self.channel = nil
31 | self.state = .initializing
32 | }
33 |
34 | deinit {
35 | assert(.disconnected == self.state)
36 | }
37 |
38 | public func connect(host: String, port: Int) -> EventLoopFuture {
39 | self.lock.withLock {
40 | assert(.initializing == self.state)
41 |
42 | let bootstrap = ClientBootstrap(group: self.group)
43 | .channelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
44 | .channelInitializer { channel in
45 | channel.pipeline.eventLoop.makeCompletedFuture {
46 | try channel.pipeline.syncOperations.addTimeoutHandlers(self.config.timeout)
47 | try channel.pipeline.syncOperations.addFramingHandlers(framing: self.config.framing)
48 | try channel.pipeline.syncOperations.addHandlers([
49 | CodableCodec(),
50 | Handler(),
51 | ])
52 | }
53 | }
54 |
55 | self.state = .connecting("\(host):\(port)")
56 | return bootstrap.connect(host: host, port: port).flatMap { channel in
57 | self.lock.withLock {
58 | self.channel = channel
59 | self.state = .connected
60 | }
61 | return channel.eventLoop.makeSucceededFuture(self)
62 | }
63 | }
64 | }
65 |
66 | public func disconnect() -> EventLoopFuture {
67 | self.lock.withLock {
68 | if .connected != self.state {
69 | return self.group.next().makeFailedFuture(ClientError.notReady)
70 | }
71 | guard let channel = self.channel else {
72 | return self.group.next().makeFailedFuture(ClientError.notReady)
73 | }
74 | self.state = .disconnecting
75 | channel.closeFuture.whenComplete { _ in
76 | self.lock.withLock {
77 | self.state = .disconnected
78 | }
79 | }
80 | channel.close(promise: nil)
81 | return channel.closeFuture
82 | }
83 | }
84 |
85 | public func call(method: String, params: RPCObject) -> EventLoopFuture {
86 | self.lock.withLock {
87 | if .connected != self.state {
88 | return self.group.next().makeFailedFuture(ClientError.notReady)
89 | }
90 | guard let channel = self.channel else {
91 | return self.group.next().makeFailedFuture(ClientError.notReady)
92 | }
93 | let promise: EventLoopPromise = channel.eventLoop.makePromise()
94 | let request = JSONRequest(id: NSUUID().uuidString, method: method, params: JSONObject(params))
95 | let requestWrapper = JSONRequestWrapper(request: request, promise: promise)
96 | let future = channel.writeAndFlush(requestWrapper)
97 | future.cascadeFailure(to: promise) // if write fails
98 | return future.flatMap {
99 | promise.futureResult.map { Result($0) }
100 | }
101 | }
102 | }
103 |
104 | private enum State: Equatable {
105 | case initializing
106 | case connecting(String)
107 | case connected
108 | case disconnecting
109 | case disconnected
110 | }
111 |
112 | public typealias Result = ResultType
113 |
114 | public struct Error: Swift.Error, Equatable {
115 | public let kind: Kind
116 | public let description: String
117 |
118 | init(kind: Kind, description: String) {
119 | self.kind = kind
120 | self.description = description
121 | }
122 |
123 | internal init(_ error: JSONError) {
124 | self.init(
125 | kind: JSONErrorCode(rawValue: error.code).map { Kind($0) } ?? .otherServerError,
126 | description: error.message
127 | )
128 | }
129 |
130 | public enum Kind {
131 | case invalidMethod
132 | case invalidParams
133 | case invalidRequest
134 | case invalidServerResponse
135 | case otherServerError
136 |
137 | internal init(_ code: JSONErrorCode) {
138 | switch code {
139 | case .invalidRequest:
140 | self = .invalidRequest
141 | case .methodNotFound:
142 | self = .invalidMethod
143 | case .invalidParams:
144 | self = .invalidParams
145 | case .parseError:
146 | self = .invalidServerResponse
147 | case .internalError, .other:
148 | self = .otherServerError
149 | }
150 | }
151 | }
152 | }
153 |
154 | public struct Config {
155 | public let timeout: TimeAmount
156 | public let framing: Framing
157 |
158 | public init(timeout: TimeAmount = TimeAmount.seconds(5), framing: Framing = .default) {
159 | self.timeout = timeout
160 | self.framing = framing
161 | }
162 | }
163 | }
164 |
165 | private class Handler: ChannelInboundHandler, ChannelOutboundHandler {
166 | public typealias InboundIn = JSONResponse
167 | public typealias OutboundIn = JSONRequestWrapper
168 | public typealias OutboundOut = JSONRequest
169 |
170 | private var queue = CircularBuffer<(String, EventLoopPromise)>()
171 |
172 | // outbound
173 | public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) {
174 | let requestWrapper = self.unwrapOutboundIn(data)
175 | queue.append((requestWrapper.request.id, requestWrapper.promise))
176 | context.write(wrapOutboundOut(requestWrapper.request), promise: promise)
177 | }
178 |
179 | // inbound
180 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
181 | if self.queue.isEmpty {
182 | context.fireChannelRead(data) // already complete
183 | return
184 | }
185 | let promise = queue.removeFirst().1
186 | let response = unwrapInboundIn(data)
187 | promise.succeed(response)
188 | }
189 |
190 | public func errorCaught(context: ChannelHandlerContext, error: Error) {
191 | if let remoteAddress = context.remoteAddress {
192 | print("server", remoteAddress, "error", error)
193 | }
194 | if self.queue.isEmpty {
195 | context.fireErrorCaught(error) // already complete
196 | return
197 | }
198 | let item = queue.removeFirst()
199 | let requestId = item.0
200 | let promise = item.1
201 | switch error {
202 | case CodecError.requestTooLarge, CodecError.badFraming, CodecError.badJSON:
203 | promise.succeed(JSONResponse(id: requestId, errorCode: .parseError, error: error))
204 | default:
205 | promise.fail(error)
206 | // close the connection
207 | context.close(promise: nil)
208 | }
209 | }
210 |
211 | public func channelActive(context: ChannelHandlerContext) {
212 | if let remoteAddress = context.remoteAddress {
213 | print("server", remoteAddress, "connected")
214 | }
215 | }
216 |
217 | public func channelInactive(context: ChannelHandlerContext) {
218 | if let remoteAddress = context.remoteAddress {
219 | print("server ", remoteAddress, "disconnected")
220 | }
221 | if !self.queue.isEmpty {
222 | self.errorCaught(context: context, error: ClientError.connectionResetByPeer)
223 | }
224 | }
225 |
226 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
227 | if (event as? IdleStateHandler.IdleStateEvent) == .read {
228 | self.errorCaught(context: context, error: ClientError.timeout)
229 | } else {
230 | context.fireUserInboundEventTriggered(event)
231 | }
232 | }
233 | }
234 |
235 | private struct JSONRequestWrapper {
236 | let request: JSONRequest
237 | let promise: EventLoopPromise
238 | }
239 |
240 | internal enum ClientError: Error {
241 | case notReady
242 | case cantBind
243 | case timeout
244 | case connectionResetByPeer
245 | }
246 |
247 | extension ResultType where Value == RPCObject, Error == TCPClient.Error {
248 | init(_ response: JSONResponse) {
249 | if let result = response.result {
250 | self = .success(RPCObject(result))
251 | } else if let error = response.error {
252 | self = .failure(TCPClient.Error(error))
253 | } else {
254 | self = .failure(TCPClient.Error(kind: .invalidServerResponse, description: "invalid server response"))
255 | }
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/json-rpc/Sources/JsonRpc/Server.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 | import NIOConcurrencyHelpers
17 | import NIOCore
18 | import NIOPosix
19 |
20 | public final class TCPServer: @unchecked Sendable {
21 | private let group: MultiThreadedEventLoopGroup
22 | private let config: Config
23 | private var channel: Channel?
24 | private let closure: RPCClosure
25 | private var state = State.initializing
26 | private let lock = NIOLock()
27 |
28 | public init(group: MultiThreadedEventLoopGroup, config: Config = Config(), closure: @escaping RPCClosure) {
29 | self.group = group
30 | self.config = config
31 | self.closure = closure
32 | self.state = .initializing
33 | }
34 |
35 | deinit {
36 | assert(.stopped == self.state)
37 | }
38 |
39 | public func start(host: String, port: Int) -> EventLoopFuture {
40 | self.lock.withLock {
41 | assert(.initializing == self.state)
42 |
43 | let bootstrap = ServerBootstrap(group: group)
44 | .serverChannelOption(ChannelOptions.backlog, value: 256)
45 | .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
46 | .childChannelInitializer { channel in
47 | channel.pipeline.eventLoop.makeCompletedFuture {
48 | try channel.pipeline.syncOperations.addTimeoutHandlers(self.config.timeout)
49 | try channel.pipeline.syncOperations.addFramingHandlers(framing: self.config.framing)
50 | try channel.pipeline.syncOperations.addHandlers([
51 | CodableCodec(),
52 | Handler(self.closure),
53 | ])
54 | }
55 | }
56 | .childChannelOption(ChannelOptions.socket(.init(IPPROTO_TCP), .init(TCP_NODELAY)), value: 1)
57 | .childChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
58 |
59 | self.state = .starting("\(host):\(port)")
60 | return bootstrap.bind(host: host, port: port).flatMap { channel in
61 | self.lock.withLock {
62 | self.channel = channel
63 | self.state = .started
64 | }
65 | return channel.eventLoop.makeSucceededFuture(self)
66 | }
67 | }
68 | }
69 |
70 | public func stop() -> EventLoopFuture {
71 | self.lock.withLock {
72 | if .started != self.state {
73 | return self.group.next().makeFailedFuture(ServerError.notReady)
74 | }
75 | guard let channel = self.channel else {
76 | return self.group.next().makeFailedFuture(ServerError.notReady)
77 | }
78 | self.state = .stopping
79 | channel.closeFuture.whenComplete { _ in
80 | self.state = .stopped
81 | }
82 | return channel.close()
83 | }
84 | }
85 |
86 | private enum State: Equatable {
87 | case initializing
88 | case starting(String)
89 | case started
90 | case stopping
91 | case stopped
92 | }
93 |
94 | public struct Config {
95 | public let timeout: TimeAmount
96 | public let framing: Framing
97 |
98 | public init(timeout: TimeAmount = TimeAmount.seconds(5), framing: Framing = .default) {
99 | self.timeout = timeout
100 | self.framing = framing
101 | }
102 | }
103 | }
104 |
105 | private class Handler: ChannelInboundHandler {
106 | public typealias InboundIn = JSONRequest
107 | public typealias OutboundOut = JSONResponse
108 |
109 | private let closure: RPCClosure
110 |
111 | public init(_ closure: @escaping RPCClosure) {
112 | self.closure = closure
113 | }
114 |
115 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
116 | let request = unwrapInboundIn(data)
117 | self.closure(
118 | request.method,
119 | RPCObject(request.params),
120 | { result in
121 | let response: JSONResponse
122 | switch result {
123 | case .success(let handlerResult):
124 | print("rpc handler returned success", handlerResult)
125 | response = JSONResponse(id: request.id, result: handlerResult)
126 | case .failure(let handlerError):
127 | print("rpc handler returned failure", handlerError)
128 | response = JSONResponse(id: request.id, error: handlerError)
129 | }
130 | context.channel.writeAndFlush(response, promise: nil)
131 | }
132 | )
133 | }
134 |
135 | public func errorCaught(context: ChannelHandlerContext, error: Error) {
136 | if let remoteAddress = context.remoteAddress {
137 | print("client", remoteAddress, "error", error)
138 | }
139 | switch error {
140 | case CodecError.badFraming, CodecError.badJSON:
141 | let response = JSONResponse(id: "unknown", errorCode: .parseError, error: error)
142 | context.channel.writeAndFlush(response, promise: nil)
143 | case CodecError.requestTooLarge:
144 | let response = JSONResponse(id: "unknown", errorCode: .invalidRequest, error: error)
145 | context.channel.writeAndFlush(response, promise: nil)
146 | default:
147 | let response = JSONResponse(id: "unknown", errorCode: .internalError, error: error)
148 | context.channel.writeAndFlush(response, promise: nil)
149 | }
150 | // close the client connection
151 | context.close(promise: nil)
152 | }
153 |
154 | public func channelActive(context: ChannelHandlerContext) {
155 | if let remoteAddress = context.remoteAddress {
156 | print("client", remoteAddress, "connected")
157 | }
158 | }
159 |
160 | public func channelInactive(context: ChannelHandlerContext) {
161 | if let remoteAddress = context.remoteAddress {
162 | print("client", remoteAddress, "disconnected")
163 | }
164 | }
165 |
166 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
167 | if (event as? IdleStateHandler.IdleStateEvent) == .read {
168 | self.errorCaught(context: context, error: ServerError.timeout)
169 | } else {
170 | context.fireUserInboundEventTriggered(event)
171 | }
172 | }
173 | }
174 |
175 | internal enum ServerError: Error {
176 | case notReady
177 | case cantBind
178 | case timeout
179 | }
180 |
--------------------------------------------------------------------------------
/json-rpc/Sources/JsonRpc/Utils.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Foundation
16 |
17 | public enum ResultType: Sendable where Value: Sendable, Error: Sendable {
18 | case success(Value)
19 | case failure(Error)
20 | }
21 |
22 | public enum Framing: CaseIterable {
23 | case `default`
24 | case jsonpos
25 | case brute
26 | }
27 |
28 | extension NSLock {
29 | func withLock(_ body: () -> T) -> T {
30 | self.lock()
31 | defer {
32 | self.unlock()
33 | }
34 | return body()
35 | }
36 | }
37 |
38 | extension String {
39 | func leftPadding(toLength: Int, withPad character: Character) -> String {
40 | let stringLength = self.count
41 | if stringLength < toLength {
42 | return String(repeatElement(character, count: toLength - stringLength)) + self
43 | } else {
44 | return String(self.suffix(toLength))
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/json-rpc/Sources/LightsdDemo/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import JSONRPC
16 | import NIOCore
17 | import NIOPosix
18 |
19 | struct Lifx {
20 | let id: String
21 | init?(_ object: RPCObject) {
22 | switch object {
23 | case .dictionary(let value):
24 | switch value["_lifx"] ?? RPCObject("") {
25 | case .dictionary(let value):
26 | switch value["addr"] ?? RPCObject("") {
27 | case .string(let value):
28 | self.id = value.split(separator: ":").joined()
29 | default:
30 | return nil
31 | }
32 | default:
33 | return nil
34 | }
35 | default:
36 | return nil
37 | }
38 | }
39 | }
40 |
41 | enum Color: Int, CaseIterable {
42 | case red = 360
43 | case orange = 50
44 | case yellow = 70
45 | case green = 90
46 | case blue = 225
47 | case purple = 280
48 | case pink = 300
49 | case white = 0
50 |
51 | init(_ name: String) {
52 | switch name.lowercased() {
53 | case "white":
54 | self = .white
55 | case "red":
56 | self = .red
57 | case "orange":
58 | self = .orange
59 | case "yellow":
60 | self = .yellow
61 | case "green":
62 | self = .green
63 | case "blue":
64 | self = .blue
65 | case "purple":
66 | self = .purple
67 | case "pink":
68 | self = .pink
69 | default:
70 | self = .white
71 | }
72 | }
73 | }
74 |
75 | func reset(_ client: TCPClient) {
76 | off(client, id: "*")
77 | sleep(1)
78 | on(client, id: "*")
79 | sleep(1)
80 | off(client, id: "*")
81 | }
82 |
83 | func list(_ client: TCPClient) -> [Lifx] {
84 | switch try! client.call(method: "get_light_state", params: RPCObject(["target": "*"])).wait() {
85 | case .failure(let error):
86 | fatalError("get_light_state failed with \(error)")
87 | case .success(let response):
88 | switch response {
89 | case .list(let value):
90 | return value.map { Lifx($0) }.compactMap { $0 }
91 | default:
92 | fatalError("unexpected reponse with \(response)")
93 | }
94 | }
95 | return []
96 | }
97 |
98 | func on(_ client: TCPClient, id: String, color: Color = .white, transition: Int = 0) {
99 | let saturation = .white == color ? 0.0 : 1.0
100 | switch try! client.call(
101 | method: "set_light_from_hsbk",
102 | params: hsbk(
103 | target: id,
104 | hue: color.rawValue,
105 | saturation: saturation,
106 | brightness: 0.1,
107 | temperature: 5000,
108 | transition: transition
109 | )
110 | ).wait() {
111 | case .failure(let error):
112 | fatalError("set_light_from_hsbk failed with \(error)")
113 | case .success:
114 | break
115 | }
116 | switch try! client.call(method: "power_on", params: RPCObject(["target": id])).wait() {
117 | case .failure(let error):
118 | fatalError("power_on failed with \(error)")
119 | case .success:
120 | break
121 | }
122 | }
123 |
124 | func off(_ client: TCPClient, id: String) {
125 | switch try! client.call(method: "power_off", params: RPCObject(["target": id])).wait() {
126 | case .failure(let error):
127 | fatalError("power_off failed with \(error)")
128 | case .success:
129 | break
130 | }
131 | }
132 |
133 | func show(_ client: TCPClient) {
134 | let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple]
135 |
136 | // read lights state
137 | var bulbs: [Lifx]
138 | switch try! client.call(method: "get_light_state", params: RPCObject(["target": "*"])).wait() {
139 | case .failure(let error):
140 | fatalError("get_light_state failed with \(error)")
141 | case .success(let response):
142 | switch response {
143 | case .list(let value):
144 | bulbs = value.map { Lifx($0) }.compactMap { $0 }
145 | default:
146 | fatalError("unexpected reponse with \(response)")
147 | }
148 | }
149 | if 0 == bulbs.count {
150 | fatalError("no bulbs found")
151 | }
152 |
153 | off(client, id: "*")
154 | sleep(1)
155 |
156 | var bulbIndex = 0
157 | for _ in 0.. RPCObject {
230 | assert(hue >= 0 && hue <= 360, "Hue from 0 to 360")
231 | assert(saturation >= 0 && saturation <= 1, "Saturation from 0 to 1")
232 | assert(brightness >= 0 && brightness <= 1, "Brightness from 0 to 1")
233 | assert(temperature >= 2500 && temperature <= 9000, "Temperature in Kelvin from 2500 to 9000")
234 | //assert (transition >= 0 && transition <= 60000, "Transition duration to this color in ms")
235 | return RPCObject([
236 | RPCObject(target), RPCObject(hue), RPCObject(saturation), RPCObject(brightness), RPCObject(temperature),
237 | RPCObject(transition),
238 | ])
239 | }
240 |
241 | let address = ("127.0.0.1", 7000)
242 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
243 | let client = TCPClient(group: eventLoopGroup, config: TCPClient.Config(framing: .brute))
244 | _ = try! client.connect(host: address.0, port: address.1).wait()
245 |
246 | // run command
247 | if CommandLine.arguments.count < 2 {
248 | fatalError("not enough arguments")
249 | }
250 |
251 | switch CommandLine.arguments[1] {
252 | case "reset":
253 | reset(client)
254 | case "list":
255 | let bulbs = list(client)
256 | print("======= bulbs =======")
257 | for bulb in bulbs { print(" \(bulb)") }
258 | print("=====================")
259 | case "on":
260 | if CommandLine.arguments.count < 3 {
261 | fatalError("not enough arguments")
262 | }
263 | on(client, id: CommandLine.arguments[2])
264 | case "off":
265 | if CommandLine.arguments.count < 3 {
266 | fatalError("not enough arguments")
267 | }
268 | off(client, id: CommandLine.arguments[2])
269 | case "color":
270 | if CommandLine.arguments.count < 4 {
271 | fatalError("not enough arguments")
272 | }
273 | on(client, id: CommandLine.arguments[2], color: Color(CommandLine.arguments[3]))
274 | case "show":
275 | show(client)
276 | default:
277 | fatalError("unknown command \(CommandLine.arguments[1])")
278 | }
279 | // shutdown
280 | try! client.disconnect().wait()
281 |
--------------------------------------------------------------------------------
/json-rpc/Sources/ServerExample/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import Dispatch
16 | import JSONRPC
17 | import NIOCore
18 | import NIOPosix
19 |
20 | private final class Calculator: Sendable {
21 | func handle(method: String, params: RPCObject, callback: (RPCResult) -> Void) {
22 | switch method.lowercased() {
23 | case "add":
24 | self.add(params: params, callback: callback)
25 | case "subtract":
26 | self.subtract(params: params, callback: callback)
27 | default:
28 | callback(.failure(RPCError(.invalidMethod)))
29 | }
30 | }
31 |
32 | func add(params: RPCObject, callback: (RPCResult) -> Void) {
33 | let values = extractNumbers(params)
34 | guard values.count > 1 else {
35 | return callback(.failure(RPCError(.invalidParams("expected 2 arguments or more"))))
36 | }
37 | return callback(.success(.integer(values.reduce(0, +))))
38 | }
39 |
40 | func subtract(params: RPCObject, callback: (RPCResult) -> Void) {
41 | let values = extractNumbers(params)
42 | guard values.count > 1 else {
43 | return callback(.failure(RPCError(.invalidParams("expected 2 arguments or more"))))
44 | }
45 | return callback(.success(.integer(values[1...].reduce(values[0], -))))
46 | }
47 |
48 | func extractNumbers(_ object: RPCObject) -> [Int] {
49 | switch object {
50 | case .list(let items):
51 | return items.map {
52 | switch $0 {
53 | case .integer(let value):
54 | return value
55 | default:
56 | return nil
57 | }
58 | }.compactMap { $0 }
59 | default:
60 | return []
61 | }
62 | }
63 | }
64 |
65 | private func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> DispatchSourceSignal {
66 | let queue = DispatchQueue(label: "ExampleServer")
67 | let signalSource = DispatchSource.makeSignalSource(signal: sig.rawValue, queue: queue)
68 | signal(sig.rawValue, SIG_IGN)
69 | signalSource.setEventHandler(handler: {
70 | signalSource.cancel()
71 | handler(sig)
72 | })
73 | signalSource.resume()
74 | return signalSource
75 | }
76 |
77 | private enum Signal: Int32 {
78 | case HUP = 1
79 | case INT = 2
80 | case QUIT = 3
81 | case ABRT = 6
82 | case KILL = 9 // ignore-unacceptable-language
83 | case ALRM = 14
84 | case TERM = 15
85 | }
86 |
87 | private let group = DispatchGroup()
88 | private let address = ("127.0.0.1", 8000)
89 | private let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
90 | // start server
91 | private let calculator = Calculator()
92 | private let server = TCPServer(group: eventLoopGroup, closure: calculator.handle)
93 | _ = try! server.start(host: address.0, port: address.1).wait()
94 |
95 | // trap
96 | group.enter()
97 | let signalSource = trap(signal: Signal.INT) { signal in
98 | print("intercepted signal: \(signal)")
99 | server.stop().whenComplete { _ in
100 | group.leave()
101 | }
102 | }
103 |
104 | group.wait()
105 | // cleanup
106 | signalSource.cancel()
107 |
--------------------------------------------------------------------------------
/json-rpc/Tests/JsonRpcTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 |
16 | #if !canImport(ObjectiveC)
17 | import XCTest
18 |
19 | extension JSONRPCTests {
20 | // DO NOT MODIFY: This is autogenerated, use:
21 | // `swift test --generate-linuxmain`
22 | // to regenerate.
23 | static let __allTests__JSONRPCTests = [
24 | ("testBadClientRequest1", testBadClientRequest1),
25 | ("testBadClientRequest2", testBadClientRequest2),
26 | ("testBadClientRequest3", testBadClientRequest3),
27 | ("testBadClientRequest4", testBadClientRequest4),
28 | ("testBadServerResponse1", testBadServerResponse1),
29 | ("testBadServerResponse2", testBadServerResponse2),
30 | ("testBadServerResponse3", testBadServerResponse3),
31 | ("testClientTimeout", testClientTimeout),
32 | ("testConcurrency", testConcurrency),
33 | ("testCustomFailure", testCustomFailure),
34 | ("testDisconnectAfterBadClientRequest", testDisconnectAfterBadClientRequest),
35 | ("testFailure", testFailure),
36 | ("testParamTypes", testParamTypes),
37 | ("testResponseTypes", testResponseTypes),
38 | ("testServerDisconnect", testServerDisconnect),
39 | ("testSuccess", testSuccess),
40 | ]
41 | }
42 |
43 | public func __allTests() -> [XCTestCaseEntry] {
44 | return [
45 | testCase(JSONRPCTests.__allTests__JSONRPCTests),
46 | ]
47 | }
48 | #endif
49 |
--------------------------------------------------------------------------------
/json-rpc/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import JSONRPCTests
16 | import XCTest
17 |
18 | var tests = [XCTestCaseEntry]()
19 | tests += JSONRPCTests.__allTests()
20 |
21 | XCTMain(tests)
22 |
--------------------------------------------------------------------------------
/nio-launchd/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm
8 |
--------------------------------------------------------------------------------
/nio-launchd/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | //===----------------------------------------------------------------------===//
3 | //
4 | // This source file is part of the SwiftNIO open source project
5 | //
6 | // Copyright (c) 2025 Apple Inc. and the SwiftNIO project authors
7 | // Licensed under Apache License v2.0
8 | //
9 | // See LICENSE.txt for license information
10 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11 | //
12 | // SPDX-License-Identifier: Apache-2.0
13 | //
14 | //===----------------------------------------------------------------------===//
15 |
16 | import PackageDescription
17 |
18 | let strictConcurrencyDevelopment = false
19 |
20 | let strictConcurrencySettings: [SwiftSetting] = {
21 | var initialSettings: [SwiftSetting] = []
22 | initialSettings.append(contentsOf: [
23 | .enableUpcomingFeature("StrictConcurrency"),
24 | .enableUpcomingFeature("InferSendableFromCaptures"),
25 | ])
26 |
27 | if strictConcurrencyDevelopment {
28 | // -warnings-as-errors here is a workaround so that IDE-based development can
29 | // get tripped up on -require-explicit-sendable.
30 | initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
31 | }
32 |
33 | return initialSettings
34 | }()
35 |
36 | let package = Package(
37 | name: "nio-launchd",
38 | dependencies: [
39 | .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
40 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.42.0"),
41 | ],
42 | targets: [
43 | .executableTarget(
44 | name: "nio-launchd",
45 | dependencies: [
46 | .product(name: "NIOCore", package: "swift-nio"),
47 | .product(name: "NIOPosix", package: "swift-nio"),
48 | .product(name: "ArgumentParser", package: "swift-argument-parser"),
49 | ],
50 | swiftSettings: strictConcurrencySettings
51 | ),
52 | ]
53 | )
54 |
--------------------------------------------------------------------------------
/nio-launchd/README.md:
--------------------------------------------------------------------------------
1 | # nio-launchd
2 |
3 | This is an example of launching a NIO server using `launchd` on macOS.
4 |
5 | First, create this file at ~/Library/LaunchAgents/io.swiftnio.nio-launchd.plist
6 |
7 | ```
8 |
9 |
10 |
11 |
12 | Labelio.swiftnio.nio-launchd
13 | ProgramArguments
14 |
15 | /path/to/nio-launchd
16 | server
17 |
18 | WorkingDirectory/tmp
19 | Sockets
20 |
21 | Listeners
22 |
23 | SockPathName
24 | /tmp/nio.launchd.sock
25 |
26 |
27 |
28 |
29 | ```
30 |
31 | Load the plist using `launchctl` command:
32 |
33 | ```
34 | $ launchctl load -w ~/Library/LaunchAgents/io.swiftnio.nio-launchd.plist
35 | ```
36 |
37 | Finally, run the client to test the connection:
38 |
39 | ```
40 | $ swift run nio-launchd client
41 | ```
42 |
43 | You can stop and unload agent using these commands:
44 |
45 | ```
46 | $ launchctl stop io.swiftnio.nio-launchd
47 | $ launchctl unload -w ~/Library/LaunchAgents/io.swiftnio.nio-launchd.plist
48 | ```
49 |
--------------------------------------------------------------------------------
/nio-launchd/Sources/nio-launchd/Client.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import ArgumentParser
16 | import NIOCore
17 | import NIOPosix
18 |
19 | struct Client: ParsableCommand {
20 |
21 | func run() throws {
22 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
23 |
24 | let bootstrap = ClientBootstrap(group: group)
25 | // Enable SO_REUSEADDR.
26 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
27 | .channelInitializer { channel in
28 | channel.eventLoop.makeCompletedFuture {
29 | try channel.pipeline.syncOperations.addHandler(EchoHandler())
30 | }
31 | }
32 |
33 | let channel = try bootstrap.connect(unixDomainSocketPath: "/tmp/nio.launchd.sock").wait()
34 | try channel.closeFuture.wait()
35 | }
36 | }
37 |
38 | private final class EchoHandler: ChannelInboundHandler {
39 | public typealias InboundIn = ByteBuffer
40 | public typealias OutboundOut = ByteBuffer
41 | private var numBytes = 0
42 |
43 | public func channelActive(context: ChannelHandlerContext) {
44 | print("Client connected to \(context.remoteAddress!)")
45 |
46 | // We are connected. It's time to send the message to the server to initialize the ping-pong sequence.
47 | let buffer = context.channel.allocator.buffer(string: "hello")
48 | self.numBytes = buffer.readableBytes
49 | context.writeAndFlush(self.wrapOutboundOut(buffer), promise: nil)
50 | }
51 |
52 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
53 | let byteBuffer = self.unwrapInboundIn(data)
54 | self.numBytes -= byteBuffer.readableBytes
55 |
56 | if self.numBytes == 0 {
57 | let string = String(buffer: byteBuffer)
58 | print("Received: '\(string)' back from the server, closing channel.")
59 | context.close(promise: nil)
60 | }
61 | }
62 |
63 | public func errorCaught(context: ChannelHandlerContext, error: Error) {
64 | print("error: ", error)
65 |
66 | // As we are not really interested getting notified on success or failure we just pass nil as promise to
67 | // reduce allocations.
68 | context.close(promise: nil)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/nio-launchd/Sources/nio-launchd/Server.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | #if canImport(launch)
16 | import NIOCore
17 | import NIOPosix
18 | import ArgumentParser
19 | import launch
20 |
21 | struct Server: ParsableCommand {
22 |
23 | func getServerFileDescriptorFromLaunchd() throws -> CInt {
24 | let fds = UnsafeMutablePointer>.allocate(capacity: 1)
25 | defer {
26 | fds.deallocate()
27 | }
28 |
29 | var count: Int = 0
30 | let ret = launch_activate_socket("Listeners", fds, &count)
31 |
32 | // Check the return code.
33 | guard ret == 0 else {
34 | print("error: launch_activate_socket returned with a non-zero exit code \(ret)")
35 | throw ExitCode(-1)
36 | }
37 |
38 | // launchd allows arbitary number of listeners but we only expect one in this example.
39 | guard count == 1 else {
40 | print("error: expected launch_activate_socket to return exactly one file descriptor")
41 | throw ExitCode(-1)
42 | }
43 |
44 | // This is safe because we already checked that we have exactly one result.
45 | let fd = fds.pointee.pointee
46 |
47 | defer {
48 | free(&fds.pointee.pointee)
49 | }
50 |
51 | return fd
52 | }
53 |
54 | func run() throws {
55 | // Get the server socket from launchd so we can bootstrap our echo server.
56 | let fd = try getServerFileDescriptorFromLaunchd()
57 |
58 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
59 | let bootstrap = ServerBootstrap(group: group)
60 | // Specify backlog and enable SO_REUSEADDR for the server itself
61 | .serverChannelOption(ChannelOptions.backlog, value: 256)
62 | .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
63 |
64 | // Set the handlers that are appled to the accepted Channels
65 | .childChannelInitializer { channel in
66 | channel.eventLoop.makeCompletedFuture {
67 | // Ensure we don't read faster than we can write by adding the BackPressureHandler into the pipeline.
68 | try channel.pipeline.syncOperations.addHandler(BackPressureHandler())
69 | try channel.pipeline.syncOperations.addHandler(EchoHandler())
70 | }
71 |
72 | }
73 |
74 | // Enable SO_REUSEADDR for the accepted Channels
75 | .childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
76 | .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 16)
77 | .childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
78 |
79 | // Bootstrap using the socket we got from launchd.
80 | let server = try bootstrap.withBoundSocket(fd).wait()
81 | try server.closeFuture.wait()
82 | }
83 | }
84 |
85 | private final class EchoHandler: ChannelInboundHandler {
86 | public typealias InboundIn = ByteBuffer
87 | public typealias OutboundOut = ByteBuffer
88 |
89 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
90 | // As we are not really interested getting notified on success or failure we just pass nil as promise to
91 | // reduce allocations.
92 | context.write(data, promise: nil)
93 | }
94 |
95 | // Flush it out. This can make use of gathering writes if multiple buffers are pending
96 | public func channelReadComplete(context: ChannelHandlerContext) {
97 | context.flush()
98 | }
99 |
100 | public func errorCaught(context: ChannelHandlerContext, error: Error) {
101 | print("error: ", error)
102 |
103 | // As we are not really interested getting notified on success or failure we just pass nil as promise to
104 | // reduce allocations.
105 | context.close(promise: nil)
106 | }
107 | }
108 | #endif
109 |
--------------------------------------------------------------------------------
/nio-launchd/Sources/nio-launchd/main.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the SwiftNIO open source project
4 | //
5 | // Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6 | // Licensed under Apache License v2.0
7 | //
8 | // See LICENSE.txt for license information
9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10 | //
11 | // SPDX-License-Identifier: Apache-2.0
12 | //
13 | //===----------------------------------------------------------------------===//
14 |
15 | import ArgumentParser
16 |
17 | #if canImport(launch)
18 | struct CLI: ParsableCommand {
19 | static let configuration = CommandConfiguration(
20 | subcommands: [
21 | Client.self,
22 | Server.self,
23 | ]
24 | )
25 | }
26 |
27 | CLI.main()
28 |
29 | #else
30 | print("This example is Darwin-only.")
31 |
32 | #endif
33 |
--------------------------------------------------------------------------------