├── .dockerignore ├── .editorconfig ├── .github ├── release.yml └── workflows │ ├── main.yml │ ├── pull_request.yml │ └── pull_request_label.yml ├── .gitignore ├── .licenseignore ├── .mailmap ├── .spi.yml ├── .swift-format ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── Examples ├── GetHTML │ └── GetHTML.swift ├── GetJSON │ └── GetJSON.swift ├── Package.swift ├── README.md └── StreamingByteCounter │ └── StreamingByteCounter.swift ├── LICENSE.txt ├── NOTICE.txt ├── Package.swift ├── README.md ├── SECURITY.md ├── Sources ├── AsyncHTTPClient │ ├── AsyncAwait │ │ ├── AnyAsyncSequence.swift │ │ ├── AnyAsyncSequenceProucerDelete.swift │ │ ├── AsyncLazySequence.swift │ │ ├── HTTPClient+execute.swift │ │ ├── HTTPClient+shutdown.swift │ │ ├── HTTPClientRequest+Prepared.swift │ │ ├── HTTPClientRequest+auth.swift │ │ ├── HTTPClientRequest.swift │ │ ├── HTTPClientResponse.swift │ │ ├── SingleIteratorPrecondition.swift │ │ ├── Transaction+StateMachine.swift │ │ └── Transaction.swift │ ├── Base64.swift │ ├── BasicAuth.swift │ ├── BestEffortHashableTLSConfiguration.swift │ ├── Configuration+BrowserLike.swift │ ├── ConnectionPool.swift │ ├── ConnectionPool │ │ ├── ChannelHandler │ │ │ ├── HTTP1ProxyConnectHandler.swift │ │ │ ├── SOCKSEventsHandler.swift │ │ │ └── TLSEventsHandler.swift │ │ ├── HTTP1 │ │ │ ├── HTTP1ClientChannelHandler.swift │ │ │ ├── HTTP1Connection.swift │ │ │ └── HTTP1ConnectionStateMachine.swift │ │ ├── HTTP2 │ │ │ ├── HTTP2ClientRequestHandler.swift │ │ │ ├── HTTP2Connection.swift │ │ │ └── HTTP2IdleHandler.swift │ │ ├── HTTPConnectionEvent.swift │ │ ├── HTTPConnectionPool+Factory.swift │ │ ├── HTTPConnectionPool+Manager.swift │ │ ├── HTTPConnectionPool.swift │ │ ├── HTTPExecutableRequest.swift │ │ ├── HTTPRequestStateMachine+Demand.swift │ │ ├── HTTPRequestStateMachine.swift │ │ ├── RequestBodyLength.swift │ │ ├── RequestFramingMetadata.swift │ │ ├── RequestOptions.swift │ │ └── State Machine │ │ │ ├── HTTPConnectionPool+Backoff.swift │ │ │ ├── HTTPConnectionPool+HTTP1Connections.swift │ │ │ ├── HTTPConnectionPool+HTTP1StateMachine.swift │ │ │ ├── HTTPConnectionPool+HTTP2Connections.swift │ │ │ ├── HTTPConnectionPool+HTTP2StateMachine.swift │ │ │ ├── HTTPConnectionPool+RequestQueue.swift │ │ │ └── HTTPConnectionPool+StateMachine.swift │ ├── ConnectionTarget.swift │ ├── DeconstructedURL.swift │ ├── Docs.docc │ │ └── index.md │ ├── FileDownloadDelegate.swift │ ├── FoundationExtensions.swift │ ├── HTTPClient+HTTPCookie.swift │ ├── HTTPClient+Proxy.swift │ ├── HTTPClient+StructuredConcurrency.swift │ ├── HTTPClient.swift │ ├── HTTPHandler.swift │ ├── LRUCache.swift │ ├── NIOLoopBound+Execute.swift │ ├── NIOTransportServices │ │ ├── NWErrorHandler.swift │ │ ├── NWWaitingHandler.swift │ │ └── TLSConfiguration.swift │ ├── RedirectState.swift │ ├── RequestBag+StateMachine.swift │ ├── RequestBag.swift │ ├── RequestValidation.swift │ ├── SSLContextCache.swift │ ├── Scheme.swift │ ├── Singleton.swift │ ├── StringConvertibleInstances.swift │ ├── StructuredConcurrencyHelpers.swift │ └── Utils.swift └── CAsyncHTTPClient │ ├── CAsyncHTTPClient.c │ └── include │ └── CAsyncHTTPClient.h ├── Tests └── AsyncHTTPClientTests │ ├── AsyncAwaitEndToEndTests.swift │ ├── AsyncTestHelpers.swift │ ├── ConnectionPoolSizeConfigValueIsRespectedTests.swift │ ├── EmbeddedChannel+HTTPConvenience.swift │ ├── HTTP1ClientChannelHandlerTests.swift │ ├── HTTP1ConnectionStateMachineTests.swift │ ├── HTTP1ConnectionTests.swift │ ├── HTTP1ProxyConnectHandlerTests.swift │ ├── HTTP2ClientRequestHandlerTests.swift │ ├── HTTP2ClientTests.swift │ ├── HTTP2ConnectionTests.swift │ ├── HTTP2IdleHandlerTests.swift │ ├── HTTPClient+SOCKSTests.swift │ ├── HTTPClient+StructuredConcurrencyTests.swift │ ├── HTTPClientBase.swift │ ├── HTTPClientCookieTests.swift │ ├── HTTPClientInformationalResponsesTests.swift │ ├── HTTPClientInternalTests.swift │ ├── HTTPClientNIOTSTests.swift │ ├── HTTPClientRequestTests.swift │ ├── HTTPClientResponseTests.swift │ ├── HTTPClientTestUtils.swift │ ├── HTTPClientTests.swift │ ├── HTTPClientUncleanSSLConnectionShutdownTests.swift │ ├── HTTPConnectionPool+FactoryTests.swift │ ├── HTTPConnectionPool+HTTP1ConnectionsTest.swift │ ├── HTTPConnectionPool+HTTP1StateTests.swift │ ├── HTTPConnectionPool+HTTP2ConnectionsTest.swift │ ├── HTTPConnectionPool+HTTP2StateMachineTests.swift │ ├── HTTPConnectionPool+ManagerTests.swift │ ├── HTTPConnectionPool+RequestQueueTests.swift │ ├── HTTPConnectionPool+StateTestUtils.swift │ ├── HTTPConnectionPoolTests.swift │ ├── HTTPRequestStateMachineTests.swift │ ├── IdleTimeoutNoReuseTests.swift │ ├── LRUCacheTests.swift │ ├── Mocks │ ├── MockConnectionPool.swift │ ├── MockHTTPExecutableRequest.swift │ ├── MockRequestExecutor.swift │ └── MockRequestQueuer.swift │ ├── NWWaitingHandlerTests.swift │ ├── NoBytesSentOverBodyLimitTests.swift │ ├── RacePoolIdleConnectionsAndGetTests.swift │ ├── RequestBagTests.swift │ ├── RequestValidationTests.swift │ ├── Resources │ ├── example.com.cert.pem │ ├── example.com.private-key.pem │ ├── self_signed_cert.pem │ └── self_signed_key.pem │ ├── ResponseDelayGetTests.swift │ ├── SOCKSEventsHandlerTests.swift │ ├── SOCKSTestUtils.swift │ ├── SSLContextCacheTests.swift │ ├── StressGetHttpsTests.swift │ ├── TLSEventsHandlerTests.swift │ ├── Transaction+StateMachineTests.swift │ ├── TransactionTests.swift │ └── XCTest+AsyncAwait.swift ├── dev └── git.commit.template └── docs └── logging-design.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .build 2 | .git -------------------------------------------------------------------------------- /.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/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: SemVer Major 4 | labels: 5 | - ⚠️ semver/major 6 | - title: SemVer Minor 7 | labels: 8 | - 🆕 semver/minor 9 | - title: SemVer Patch 10 | labels: 11 | - 🔨 semver/patch 12 | - title: Other Changes 13 | labels: 14 | - semver/none 15 | -------------------------------------------------------------------------------- /.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 | unit-tests: 11 | name: Unit tests 12 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 13 | with: 14 | linux_5_10_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors" 15 | linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors" 16 | linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors" 17 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" 18 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" 19 | 20 | static-sdk: 21 | name: Static SDK 22 | # Workaround https://github.com/nektos/act/issues/1875 23 | uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 24 | -------------------------------------------------------------------------------- /.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: "AsyncHTTPClient" 13 | unit-tests: 14 | name: Unit tests 15 | uses: apple/swift-nio/.github/workflows/unit_tests.yml@main 16 | with: 17 | linux_5_10_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors" 18 | linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors" 19 | linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors" 20 | linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" 21 | linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" 22 | 23 | cxx-interop: 24 | name: Cxx interop 25 | uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main 26 | with: 27 | linux_5_9_enabled: false 28 | 29 | static-sdk: 30 | name: Static SDK 31 | # Workaround https://github.com/nektos/act/issues/1875 32 | uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 33 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_label.yml: -------------------------------------------------------------------------------- 1 | name: PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, reopened, synchronize] 6 | 7 | jobs: 8 | semver-label-check: 9 | name: Semantic version label check 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 1 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | with: 16 | persist-credentials: false 17 | - name: Check for Semantic Version label 18 | uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | Package.resolved 3 | *.xcodeproj 4 | DerivedData 5 | .DS_Store 6 | .swiftpm/ 7 | .SourceKitten 8 | -------------------------------------------------------------------------------- /.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 | .dockerignore 34 | Snippets/* 35 | dev/git.commit.template 36 | .unacceptablelanguageignore 37 | Tests/AsyncHTTPClientTests/Resources/*.pem 38 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Artem Redkin 2 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [AsyncHTTPClient] 5 | -------------------------------------------------------------------------------- /.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 | "noAssignmentInExpressions" : { 22 | "allowedFunctions" : [ 23 | "XCTAssertNoThrow", 24 | "XCTAssertThrowsError" 25 | ] 26 | }, 27 | "rules" : { 28 | "AllPublicDeclarationsHaveDocumentation" : false, 29 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 30 | "AlwaysUseLowerCamelCase" : false, 31 | "AmbiguousTrailingClosureOverload" : true, 32 | "BeginDocumentationCommentWithOneLineSummary" : false, 33 | "DoNotUseSemicolons" : true, 34 | "DontRepeatTypeInStaticProperties" : true, 35 | "FileScopedDeclarationPrivacy" : true, 36 | "FullyIndirectEnum" : true, 37 | "GroupNumericLiterals" : true, 38 | "IdentifiersMustBeASCII" : true, 39 | "NeverForceUnwrap" : false, 40 | "NeverUseForceTry" : false, 41 | "NeverUseImplicitlyUnwrappedOptionals" : false, 42 | "NoAccessLevelOnExtensionDeclaration" : true, 43 | "NoAssignmentInExpressions" : true, 44 | "NoBlockComments" : true, 45 | "NoCasesWithOnlyFallthrough" : true, 46 | "NoEmptyTrailingClosureParentheses" : true, 47 | "NoLabelsInCasePatterns" : true, 48 | "NoLeadingUnderscores" : false, 49 | "NoParensAroundConditions" : true, 50 | "NoVoidReturnOnFunctionSignature" : true, 51 | "OmitExplicitReturns" : true, 52 | "OneCasePerLine" : true, 53 | "OneVariableDeclarationPerLine" : true, 54 | "OnlyOneTrailingClosureArgument" : true, 55 | "OrderedImports" : true, 56 | "ReplaceForEachWithForLoop" : true, 57 | "ReturnVoidInsteadOfEmptyTuple" : true, 58 | "UseEarlyExits" : false, 59 | "UseExplicitNilCheckInConditions" : false, 60 | "UseLetInEveryBoundCaseVariable" : false, 61 | "UseShorthandTypeNames" : true, 62 | "UseSingleLinePropertyGetter" : false, 63 | "UseSynthesizedInitializer" : false, 64 | "UseTripleSlashForDocumentationComments" : true, 65 | "UseWhereClausesInForLoops" : false, 66 | "ValidateDocumentationComments" : false 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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 | * AsyncHTTPClient commit hash 14 | * Contextual information (e.g. what you were trying to achieve with AsyncHTTPClient) 15 | * Simplest possible steps to reproduce 16 | * More complex the steps are, lower the priority will be. 17 | * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. 18 | * Anything that might be relevant in your opinion, such as: 19 | * Swift version or the output of `swift --version` 20 | * OS version and the output of `uname -a` 21 | * Network configuration 22 | 23 | 24 | ### Example 25 | 26 | ``` 27 | AsyncHTTPClient commit hash: 22ec043dc9d24bb011b47ece4f9ee97ee5be2757 28 | 29 | Context: 30 | While load testing my program written with AsyncHTTPClient, I noticed 31 | that one file descriptor is leaked per request. 32 | 33 | Steps to reproduce: 34 | 1. ... 35 | 2. ... 36 | 3. ... 37 | 4. ... 38 | 39 | $ swift --version 40 | Swift version 4.0.2 (swift-4.0.2-RELEASE) 41 | Target: x86_64-unknown-linux-gnu 42 | 43 | Operating system: Ubuntu Linux 16.04 64-bit 44 | 45 | $ uname -a 46 | Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux 47 | 48 | My system has IPv6 disabled. 49 | ``` 50 | 51 | ## Writing a Patch 52 | 53 | A good AsyncHTTPClient patch is: 54 | 55 | 1. Concise, and contains as few changes as needed to achieve the end result. 56 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it. 57 | 3. Documented, adding API documentation as needed to cover new functions and properties. 58 | 4. Accompanied by a great commit message, using our commit message template. 59 | 60 | *Note* as of version 1.10.0 AsyncHTTPClient requires Swift 5.4. Earlier versions support as far back as Swift 5.0. 61 | 62 | ### Commit Message Template 63 | 64 | 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: 65 | 66 | git config commit.template dev/git.commit.template 67 | 68 | 69 | ### Run CI checks locally 70 | 71 | You can run the Github Actions workflows locally using [act](https://github.com/nektos/act). For detailed steps on how to do this please see [https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally](https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally). 72 | 73 | ## How to contribute your work 74 | 75 | Please open a pull request at https://github.com/swift-server/async-http-client. Make sure the CI passes, and then wait for code review. 76 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to the AsyncHTTPClient. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | - Apple Inc. (all contributors with '@apple.com') 11 | 12 | ### Contributors 13 | 14 | - Andrew Lees <32634907+Andrew-Lees11@users.noreply.github.com> 15 | - Artem Redkin 16 | - George Barnett 17 | - Ian Partridge 18 | - Joe Smith 19 | - Johannes Weiss 20 | - Ludovic Dewailly 21 | - Tanner 22 | - Tobias 23 | - Trevör 24 | - tomer doron 25 | - tomer doron 26 | - vkill 27 | 28 | **Updating this list** 29 | 30 | Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` 31 | -------------------------------------------------------------------------------- /Examples/GetHTML/GetHTML.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import NIOCore 17 | 18 | @main 19 | struct GetHTML { 20 | static func main() async throws { 21 | let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) 22 | do { 23 | let request = HTTPClientRequest(url: "https://apple.com") 24 | let response = try await httpClient.execute(request, timeout: .seconds(30)) 25 | print("HTTP head", response) 26 | let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB 27 | print(String(buffer: body)) 28 | } catch { 29 | print("request failed:", error) 30 | } 31 | // it is important to shutdown the httpClient after all requests are done, even if one failed 32 | try await httpClient.shutdown() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/GetJSON/GetJSON.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Foundation 17 | import NIOCore 18 | import NIOFoundationCompat 19 | 20 | struct Comic: Codable { 21 | var num: Int 22 | var title: String 23 | var day: String 24 | var month: String 25 | var year: String 26 | var img: String 27 | var alt: String 28 | var news: String 29 | var link: String 30 | var transcript: String 31 | } 32 | 33 | @main 34 | struct GetJSON { 35 | static func main() async throws { 36 | let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) 37 | do { 38 | let request = HTTPClientRequest(url: "https://xkcd.com/info.0.json") 39 | let response = try await httpClient.execute(request, timeout: .seconds(30)) 40 | print("HTTP head", response) 41 | let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB 42 | // we use an overload defined in `NIOFoundationCompat` for `decode(_:from:)` to 43 | // efficiently decode from a `ByteBuffer` 44 | let comic = try JSONDecoder().decode(Comic.self, from: body) 45 | dump(comic) 46 | } catch { 47 | print("request failed:", error) 48 | } 49 | // it is important to shutdown the httpClient after all requests are done, even if one failed 50 | try await httpClient.shutdown() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Examples/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | //===----------------------------------------------------------------------===// 3 | // 4 | // This source file is part of the AsyncHTTPClient open source project 5 | // 6 | // Copyright (c) 2018-2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 11 | // 12 | // SPDX-License-Identifier: Apache-2.0 13 | // 14 | //===----------------------------------------------------------------------===// 15 | 16 | import PackageDescription 17 | 18 | let package = Package( 19 | name: "async-http-client-examples", 20 | platforms: [ 21 | .macOS(.v10_15), 22 | .iOS(.v13), 23 | .tvOS(.v13), 24 | .watchOS(.v6), 25 | ], 26 | products: [ 27 | .executable(name: "GetHTML", targets: ["GetHTML"]), 28 | .executable(name: "GetJSON", targets: ["GetJSON"]), 29 | .executable(name: "StreamingByteCounter", targets: ["StreamingByteCounter"]), 30 | ], 31 | dependencies: [ 32 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.38.0"), 33 | 34 | // in real-world projects this would be 35 | // .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0") 36 | .package(name: "async-http-client", path: "../"), 37 | ], 38 | targets: [ 39 | // MARK: - Examples 40 | 41 | .executableTarget( 42 | name: "GetHTML", 43 | dependencies: [ 44 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 45 | .product(name: "NIOCore", package: "swift-nio"), 46 | ], 47 | path: "GetHTML" 48 | ), 49 | .executableTarget( 50 | name: "GetJSON", 51 | dependencies: [ 52 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 53 | .product(name: "NIOCore", package: "swift-nio"), 54 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 55 | ], 56 | path: "GetJSON" 57 | ), 58 | .executableTarget( 59 | name: "StreamingByteCounter", 60 | dependencies: [ 61 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 62 | .product(name: "NIOCore", package: "swift-nio"), 63 | ], 64 | path: "StreamingByteCounter" 65 | ), 66 | ] 67 | ) 68 | -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | This folder includes a couple of Examples for `AsyncHTTPClient`. 3 | You can run them by opening the `Package.swift` in this folder through Xcode. 4 | In Xcode you can then select the scheme for the example you want run e.g. `GetHTML`. 5 | 6 | You can also run the examples from the command line by executing the follow command in this folder: 7 | ``` 8 | swift run GetHTML 9 | ``` 10 | To run other examples you can just replace `GetHTML` with the name of the example you want to run. 11 | 12 | ## [GetHTML](./GetHTML/GetHTML.swift) 13 | 14 | This examples sends a HTTP GET request to `https://apple.com/` and first `await`s and `print`s the HTTP Response Head. 15 | Afterwards it buffers the full response body in memory and prints the response as a `String`. 16 | 17 | ## [GetJSON](./GetJSON/GetJSON.swift) 18 | 19 | This examples sends a HTTP GET request to `https://xkcd.com/info.0.json` and first `await`s and `print`s the HTTP Response Head. 20 | Afterwards it buffers the full response body in memory, decodes the buffer using a `JSONDecoder` and `dump`s the decoded response. 21 | 22 | ## [StreamingByteCounter](./StreamingByteCounter/StreamingByteCounter.swift) 23 | 24 | This examples sends a HTTP GET request to `https://apple.com/` and first `await`s and `print`s the HTTP Response Head. 25 | Afterwards it asynchronously iterates over all body fragments, counts the received bytes and prints a progress indicator (if the server send a content-length header). 26 | At the end the total received bytes are printed. 27 | Note that we drop all received fragment and therefore do **not** buffer the whole response body in-memory. 28 | -------------------------------------------------------------------------------- /Examples/StreamingByteCounter/StreamingByteCounter.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import NIOCore 17 | 18 | @main 19 | struct StreamingByteCounter { 20 | static func main() async throws { 21 | let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) 22 | do { 23 | let request = HTTPClientRequest(url: "https://apple.com") 24 | let response = try await httpClient.execute(request, timeout: .seconds(30)) 25 | print("HTTP head", response) 26 | 27 | // if defined, the content-length headers announces the size of the body 28 | let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) 29 | 30 | var receivedBytes = 0 31 | // asynchronously iterates over all body fragments 32 | // this loop will automatically propagate backpressure correctly 33 | for try await buffer in response.body { 34 | // For this example, we are just interested in the size of the fragment 35 | receivedBytes += buffer.readableBytes 36 | 37 | if let expectedBytes = expectedBytes { 38 | // if the body size is known, we calculate a progress indicator 39 | let progress = Double(receivedBytes) / Double(expectedBytes) 40 | print("progress: \(Int(progress * 100))%") 41 | } 42 | } 43 | print("did receive \(receivedBytes) bytes") 44 | } catch { 45 | print("request failed:", error) 46 | } 47 | // it is important to shutdown the httpClient after all requests are done, even if one failed 48 | try await httpClient.shutdown() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The AsyncHTTPClient Project 3 | =========================== 4 | 5 | Please visit the AsyncHTTPClient web site for more information: 6 | 7 | * https://github.com/swift-server/async-http-client 8 | 9 | Copyright 2017-2021 The AsyncHTTPClient Project 10 | 11 | The AsyncHTTPClient Project licenses this file to you under the Apache License, 12 | version 2.0 (the "License"); you may not use this file except in compliance 13 | with the License. You may obtain a copy of the License at: 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 20 | License for the specific language governing permissions and limitations 21 | under the License. 22 | 23 | Also, please refer to each LICENSE.txt file, which is located in 24 | the 'license' directory of the distribution file, for the license terms of the 25 | components that this product depends on. 26 | 27 | --- 28 | 29 | This product contains derivations of various scripts from SwiftNIO. 30 | 31 | * LICENSE (Apache License 2.0): 32 | * https://www.apache.org/licenses/LICENSE-2.0 33 | * HOMEPAGE: 34 | * https://github.com/apple/swift-nio 35 | 36 | --- 37 | 38 | This product contains a derivation of "XCTest+AsyncAwait.swift" from gRPC Swift. 39 | 40 | * LICENSE (Apache License 2.0): 41 | * https://www.apache.org/licenses/LICENSE-2.0 42 | * HOMEPAGE: 43 | * https://github.com/grpc/grpc-swift 44 | 45 | --- 46 | 47 | This product contains a derivation of the Tony Stone's 'process_test_files.rb'. 48 | 49 | * LICENSE (Apache License 2.0): 50 | * https://www.apache.org/licenses/LICENSE-2.0 51 | * HOMEPAGE: 52 | * https://github.com/tonystone/build-tools/commit/6c417b7569df24597a48a9aa7b505b636e8f73a1 53 | * https://github.com/tonystone/build-tools/blob/cf3440f43bde2053430285b4ed0709c865892eb5/source/xctest_tool.rb 54 | 55 | --- 56 | 57 | This product contains a derivation of Fabian Fett's 'Base64.swift'. 58 | 59 | * LICENSE (Apache License 2.0): 60 | * https://github.com/swift-extras/swift-extras-base64/blob/b8af49699d59ad065b801715a5009619100245ca/LICENSE 61 | * HOMEPAGE: 62 | * https://github.com/fabianfett/swift-base64-kit 63 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | //===----------------------------------------------------------------------===// 3 | // 4 | // This source file is part of the AsyncHTTPClient open source project 5 | // 6 | // Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient 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: "async-http-client", 38 | products: [ 39 | .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) 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.30.0"), 44 | .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"), 45 | .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"), 46 | .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.24.0"), 47 | .package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"), 48 | .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), 49 | .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), 50 | ], 51 | targets: [ 52 | .target( 53 | name: "CAsyncHTTPClient", 54 | cSettings: [ 55 | .define("_GNU_SOURCE") 56 | ] 57 | ), 58 | .target( 59 | name: "AsyncHTTPClient", 60 | dependencies: [ 61 | .target(name: "CAsyncHTTPClient"), 62 | .product(name: "NIO", package: "swift-nio"), 63 | .product(name: "NIOTLS", package: "swift-nio"), 64 | .product(name: "NIOCore", package: "swift-nio"), 65 | .product(name: "NIOPosix", package: "swift-nio"), 66 | .product(name: "NIOHTTP1", package: "swift-nio"), 67 | .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), 68 | .product(name: "NIOHTTP2", package: "swift-nio-http2"), 69 | .product(name: "NIOSSL", package: "swift-nio-ssl"), 70 | .product(name: "NIOHTTPCompression", package: "swift-nio-extras"), 71 | .product(name: "NIOSOCKS", package: "swift-nio-extras"), 72 | .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), 73 | .product(name: "Logging", package: "swift-log"), 74 | .product(name: "Atomics", package: "swift-atomics"), 75 | .product(name: "Algorithms", package: "swift-algorithms"), 76 | ], 77 | swiftSettings: strictConcurrencySettings 78 | ), 79 | .testTarget( 80 | name: "AsyncHTTPClientTests", 81 | dependencies: [ 82 | .target(name: "AsyncHTTPClient"), 83 | .product(name: "NIOTLS", package: "swift-nio"), 84 | .product(name: "NIOCore", package: "swift-nio"), 85 | .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), 86 | .product(name: "NIOEmbedded", package: "swift-nio"), 87 | .product(name: "NIOFoundationCompat", package: "swift-nio"), 88 | .product(name: "NIOTestUtils", package: "swift-nio"), 89 | .product(name: "NIOSSL", package: "swift-nio-ssl"), 90 | .product(name: "NIOHTTP2", package: "swift-nio-http2"), 91 | .product(name: "NIOSOCKS", package: "swift-nio-extras"), 92 | .product(name: "Logging", package: "swift-log"), 93 | .product(name: "Atomics", package: "swift-atomics"), 94 | .product(name: "Algorithms", package: "swift-algorithms"), 95 | ], 96 | resources: [ 97 | .copy("Resources/self_signed_cert.pem"), 98 | .copy("Resources/self_signed_key.pem"), 99 | .copy("Resources/example.com.cert.pem"), 100 | .copy("Resources/example.com.private-key.pem"), 101 | ], 102 | swiftSettings: strictConcurrencySettings 103 | ), 104 | ] 105 | ) 106 | 107 | // --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // 108 | for target in package.targets { 109 | switch target.type { 110 | case .regular, .test, .executable: 111 | var settings = target.swiftSettings ?? [] 112 | // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md 113 | settings.append(.enableUpcomingFeature("MemberImportVisibility")) 114 | target.swiftSettings = settings 115 | case .macro, .plugin, .system, .binary: 116 | () // not applicable 117 | @unknown default: 118 | () // we don't know what to do here, do nothing 119 | } 120 | } 121 | // --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // 122 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | This document specifies the security process for the AsyncHTTPClient project. 4 | 5 | ## Disclosures 6 | 7 | ### Private Disclosure Process 8 | 9 | The AsyncHTTPClient maintainers ask that known and suspected vulnerabilities be 10 | privately and responsibly disclosed by emailing 11 | [sswg-security-reports@forums.swift.org](mailto:sswg-security-reports@forums.swift.org) 12 | with the all the required detail. 13 | **Do not file a public issue.** 14 | 15 | #### When to report a vulnerability 16 | 17 | * You think you have discovered a potential security vulnerability in 18 | AsyncHTTPClient. 19 | * You are unsure how a vulnerability affects AsyncHTTPClient. 20 | 21 | #### What happens next? 22 | 23 | * A member of the team will acknowledge receipt of the report within 3 24 | working days (United States). This may include a request for additional 25 | information about reproducing the vulnerability. 26 | * We will privately inform the Swift Server Work Group ([SSWG][sswg]) of the 27 | vulnerability within 10 days of the report as per their [security 28 | guidelines][sswg-security]. 29 | * Once we have identified a fix we may ask you to validate it. We aim to do this 30 | within 30 days. In some cases this may not be possible, for example when the 31 | vulnerability exists at the protocol level and the industry must coordinate on 32 | the disclosure process. 33 | * If a CVE number is required, one will be requested from [MITRE][mitre] 34 | providing you with full credit for the discovery. 35 | * We will decide on a planned release date and let you know when it is. 36 | * Prior to release, we will inform major dependents that a security-related 37 | patch is impending. 38 | * Once the fix has been released we will publish a security advisory on GitHub 39 | and in the Server → Security Updates category on the [Swift forums][swift-forums-sec]. 40 | 41 | [sswg]: https://github.com/swift-server/sswg 42 | [sswg-security]: https://github.com/swift-server/sswg/blob/main/security/README.md 43 | [swift-forums-sec]: https://forums.swift.org/c/server/security-updates/ 44 | [mitre]: https://cveform.mitre.org/ 45 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @usableFromInline 16 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 17 | struct AnyAsyncSequence: Sendable, AsyncSequence { 18 | @usableFromInline typealias AsyncIteratorNextCallback = () async throws -> Element? 19 | 20 | @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { 21 | @usableFromInline let nextCallback: AsyncIteratorNextCallback 22 | 23 | @inlinable init(nextCallback: @escaping AsyncIteratorNextCallback) { 24 | self.nextCallback = nextCallback 25 | } 26 | 27 | @inlinable mutating func next() async throws -> Element? { 28 | try await self.nextCallback() 29 | } 30 | } 31 | 32 | @usableFromInline var makeAsyncIteratorCallback: @Sendable () -> AsyncIteratorNextCallback 33 | 34 | @inlinable init( 35 | _ asyncSequence: SequenceOfBytes 36 | ) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == Element { 37 | self.makeAsyncIteratorCallback = { 38 | var iterator = asyncSequence.makeAsyncIterator() 39 | return { 40 | try await iterator.next() 41 | } 42 | } 43 | } 44 | 45 | @inlinable func makeAsyncIterator() -> AsyncIterator { 46 | .init(nextCallback: self.makeAsyncIteratorCallback()) 47 | } 48 | } 49 | 50 | @available(*, unavailable) 51 | extension AnyAsyncSequence.AsyncIterator: Sendable {} 52 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequenceProucerDelete.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | 17 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 18 | @usableFromInline 19 | struct AnyAsyncSequenceProducerDelegate: NIOAsyncSequenceProducerDelegate { 20 | @usableFromInline 21 | var delegate: NIOAsyncSequenceProducerDelegate 22 | 23 | @inlinable 24 | init(_ delegate: Delegate) { 25 | self.delegate = delegate 26 | } 27 | 28 | @inlinable 29 | func produceMore() { 30 | self.delegate.produceMore() 31 | } 32 | 33 | @inlinable 34 | func didTerminate() { 35 | self.delegate.didTerminate() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/AsyncAwait/AsyncLazySequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 16 | @usableFromInline 17 | struct AsyncLazySequence: AsyncSequence { 18 | @usableFromInline typealias Element = Base.Element 19 | @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { 20 | @usableFromInline var iterator: Base.Iterator 21 | @inlinable init(iterator: Base.Iterator) { 22 | self.iterator = iterator 23 | } 24 | 25 | @inlinable mutating func next() async throws -> Base.Element? { 26 | self.iterator.next() 27 | } 28 | } 29 | 30 | @usableFromInline var base: Base 31 | 32 | @inlinable init(base: Base) { 33 | self.base = base 34 | } 35 | 36 | @inlinable func makeAsyncIterator() -> AsyncIterator { 37 | .init(iterator: self.base.makeIterator()) 38 | } 39 | } 40 | 41 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 42 | extension AsyncLazySequence: Sendable where Base: Sendable {} 43 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 44 | extension AsyncLazySequence.AsyncIterator: Sendable where Base.Iterator: Sendable {} 45 | 46 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 47 | extension Sequence { 48 | /// Turns `self` into an `AsyncSequence` by vending each element of `self` asynchronously. 49 | @inlinable var async: AsyncLazySequence { 50 | .init(base: self) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+shutdown.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | 17 | extension HTTPClient { 18 | /// Shuts down the client and `EventLoopGroup` if it was created by the client. 19 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 20 | public func shutdown() async throws { 21 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 22 | self.shutdown { error in 23 | switch error { 24 | case .none: 25 | continuation.resume() 26 | case .some(let error): 27 | continuation.resume(throwing: error) 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | import NIOHTTP1 17 | import NIOSSL 18 | 19 | import struct Foundation.URL 20 | 21 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 22 | extension HTTPClientRequest { 23 | struct Prepared { 24 | enum Body { 25 | case asyncSequence( 26 | length: RequestBodyLength, 27 | makeAsyncIterator: @Sendable () -> ((ByteBufferAllocator) async throws -> ByteBuffer?) 28 | ) 29 | case sequence( 30 | length: RequestBodyLength, 31 | canBeConsumedMultipleTimes: Bool, 32 | makeCompleteBody: (ByteBufferAllocator) -> ByteBuffer 33 | ) 34 | case byteBuffer(ByteBuffer) 35 | } 36 | 37 | var url: URL 38 | var poolKey: ConnectionPool.Key 39 | var requestFramingMetadata: RequestFramingMetadata 40 | var head: HTTPRequestHead 41 | var body: Body? 42 | var tlsConfiguration: TLSConfiguration? 43 | } 44 | } 45 | 46 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 47 | extension HTTPClientRequest.Prepared { 48 | init(_ request: HTTPClientRequest, dnsOverride: [String: String] = [:]) throws { 49 | guard !request.url.isEmpty, let url = URL(string: request.url) else { 50 | throw HTTPClientError.invalidURL 51 | } 52 | 53 | let deconstructedURL = try DeconstructedURL(url: url) 54 | 55 | var headers = request.headers 56 | headers.addHostIfNeeded(for: deconstructedURL) 57 | let metadata = try headers.validateAndSetTransportFraming( 58 | method: request.method, 59 | bodyLength: .init(request.body) 60 | ) 61 | 62 | self.init( 63 | url: url, 64 | poolKey: .init(url: deconstructedURL, tlsConfiguration: request.tlsConfiguration, dnsOverride: dnsOverride), 65 | requestFramingMetadata: metadata, 66 | head: .init( 67 | version: .http1_1, 68 | method: request.method, 69 | uri: deconstructedURL.uri, 70 | headers: headers 71 | ), 72 | body: request.body.map { .init($0) }, 73 | tlsConfiguration: request.tlsConfiguration 74 | ) 75 | } 76 | } 77 | 78 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 79 | extension HTTPClientRequest.Prepared.Body { 80 | init(_ body: HTTPClientRequest.Body) { 81 | switch body.mode { 82 | case .asyncSequence(let length, let makeAsyncIterator): 83 | self = .asyncSequence(length: length, makeAsyncIterator: makeAsyncIterator) 84 | case .sequence(let length, let canBeConsumedMultipleTimes, let makeCompleteBody): 85 | self = .sequence( 86 | length: length, 87 | canBeConsumedMultipleTimes: canBeConsumedMultipleTimes, 88 | makeCompleteBody: makeCompleteBody 89 | ) 90 | case .byteBuffer(let byteBuffer): 91 | self = .byteBuffer(byteBuffer) 92 | } 93 | } 94 | } 95 | 96 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 97 | extension RequestBodyLength { 98 | init(_ body: HTTPClientRequest.Body?) { 99 | switch body?.mode { 100 | case .none: 101 | self = .known(0) 102 | case .byteBuffer(let buffer): 103 | self = .known(Int64(buffer.readableBytes)) 104 | case .sequence(let length, _, _), .asyncSequence(let length, _): 105 | self = length 106 | } 107 | } 108 | } 109 | 110 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 111 | extension HTTPClientRequest { 112 | func followingRedirect( 113 | from originalURL: URL, 114 | to redirectURL: URL, 115 | status: HTTPResponseStatus 116 | ) -> HTTPClientRequest { 117 | let (method, headers, body) = transformRequestForRedirect( 118 | from: originalURL, 119 | method: self.method, 120 | headers: self.headers, 121 | body: self.body, 122 | to: redirectURL, 123 | status: status 124 | ) 125 | var newRequest = HTTPClientRequest(url: redirectURL.absoluteString) 126 | newRequest.method = method 127 | newRequest.headers = headers 128 | newRequest.body = body 129 | return newRequest 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+auth.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 18 | extension HTTPClientRequest { 19 | /// Set basic auth for a request. 20 | /// 21 | /// - parameters: 22 | /// - username: the username to authenticate with 23 | /// - password: authentication password associated with the username 24 | public mutating func setBasicAuth(username: String, password: String) { 25 | self.headers.setBasicAuth(username: username, password: password) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Atomics 16 | 17 | /// Makes sure that a consumer of this `AsyncSequence` only calls `makeAsyncIterator()` at most once. 18 | /// If `makeAsyncIterator()` is called multiple times, the program crashes. 19 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 20 | @usableFromInline struct SingleIteratorPrecondition: AsyncSequence { 21 | @usableFromInline let base: Base 22 | @usableFromInline let didCreateIterator: ManagedAtomic = .init(false) 23 | @usableFromInline typealias Element = Base.Element 24 | @inlinable init(base: Base) { 25 | self.base = base 26 | } 27 | 28 | @inlinable func makeAsyncIterator() -> Base.AsyncIterator { 29 | precondition( 30 | self.didCreateIterator.exchange(true, ordering: .relaxed) == false, 31 | "makeAsyncIterator() is only allowed to be called at most once." 32 | ) 33 | return self.base.makeAsyncIterator() 34 | } 35 | } 36 | 37 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 38 | extension SingleIteratorPrecondition: @unchecked Sendable where Base: Sendable {} 39 | 40 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 41 | extension AsyncSequence { 42 | @inlinable var singleIteratorPrecondition: SingleIteratorPrecondition { 43 | .init(base: self) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/BasicAuth.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import NIOHTTP1 17 | 18 | /// Generates base64 encoded username + password for http basic auth. 19 | /// 20 | /// - Parameters: 21 | /// - username: the username to authenticate with 22 | /// - password: authentication password associated with the username 23 | /// - Returns: encoded credentials to use the Authorization: Basic http header. 24 | func encodeBasicAuthCredentials(username: String, password: String) -> String { 25 | var value = Data() 26 | value.reserveCapacity(username.utf8.count + password.utf8.count + 1) 27 | value.append(contentsOf: username.utf8) 28 | value.append(UInt8(ascii: ":")) 29 | value.append(contentsOf: password.utf8) 30 | return value.base64EncodedString() 31 | } 32 | 33 | extension HTTPHeaders { 34 | /// Sets the basic auth header 35 | mutating func setBasicAuth(username: String, password: String) { 36 | let encoded = encodeBasicAuthCredentials(username: username, password: password) 37 | self.replaceOrAdd(name: "Authorization", value: "Basic \(encoded)") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/BestEffortHashableTLSConfiguration.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOSSL 16 | 17 | /// Wrapper around `TLSConfiguration` from NIOSSL to provide a best effort implementation of `Hashable` 18 | struct BestEffortHashableTLSConfiguration: Hashable { 19 | let base: TLSConfiguration 20 | 21 | init(wrapping base: TLSConfiguration) { 22 | self.base = base 23 | } 24 | 25 | func hash(into hasher: inout Hasher) { 26 | self.base.bestEffortHash(into: &hasher) 27 | } 28 | 29 | static func == (lhs: BestEffortHashableTLSConfiguration, rhs: BestEffortHashableTLSConfiguration) -> Bool { 30 | lhs.base.bestEffortEquals(rhs.base) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/Configuration+BrowserLike.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | import NIOCore 15 | import NIOHTTPCompression 16 | import NIOSSL 17 | 18 | // swift-format-ignore: DontRepeatTypeInStaticProperties 19 | extension HTTPClient.Configuration { 20 | /// The ``HTTPClient/Configuration`` for ``HTTPClient/shared`` which tries to mimic the platform's default or prevalent browser as closely as possible. 21 | /// 22 | /// Don't rely on specific values of this configuration as they're subject to change. You can rely on them being somewhat sensible though. 23 | /// 24 | /// - note: At present, this configuration is nowhere close to a real browser configuration but in case of disagreements we will choose values that match 25 | /// the default browser as closely as possible. 26 | /// 27 | /// Platform's default/prevalent browsers that we're trying to match (these might change over time): 28 | /// - macOS: Safari 29 | /// - iOS: Safari 30 | /// - Android: Google Chrome 31 | /// - Linux (non-Android): Google Chrome 32 | public static var singletonConfiguration: HTTPClient.Configuration { 33 | // To start with, let's go with these values. Obtained from Firefox's config. 34 | HTTPClient.Configuration( 35 | certificateVerification: .fullVerification, 36 | redirectConfiguration: .follow(max: 20, allowCycles: false), 37 | timeout: Timeout(connect: .seconds(90), read: .seconds(90)), 38 | connectionPool: .seconds(600), 39 | proxy: nil, 40 | ignoreUncleanSSLShutdown: false, 41 | decompression: .enabled(limit: .ratio(25)), 42 | backgroundActivityLogger: nil 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2019-2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import CNIOLinux 16 | import NIOCore 17 | import NIOSSL 18 | 19 | #if canImport(Darwin) 20 | import Darwin.C 21 | #elseif canImport(Musl) 22 | import Musl 23 | #elseif canImport(Android) 24 | import Android 25 | #elseif os(Linux) || os(FreeBSD) 26 | import Glibc 27 | #else 28 | #error("unsupported target operating system") 29 | #endif 30 | 31 | extension String { 32 | var isIPAddress: Bool { 33 | var ipv4Address = in_addr() 34 | var ipv6Address = in6_addr() 35 | return self.withCString { host in 36 | inet_pton(AF_INET, host, &ipv4Address) == 1 || inet_pton(AF_INET6, host, &ipv6Address) == 1 37 | } 38 | } 39 | } 40 | 41 | enum ConnectionPool { 42 | /// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s 43 | /// 44 | /// A key is initialized from a `Request`, it uses the components to derive a hashed value 45 | /// used by the `providers` dictionary to allow retrieving and creating 46 | /// connection providers associated to a certain request in constant time. 47 | struct Key: Hashable, CustomStringConvertible { 48 | var scheme: Scheme 49 | var connectionTarget: ConnectionTarget 50 | private var tlsConfiguration: BestEffortHashableTLSConfiguration? 51 | var serverNameIndicatorOverride: String? 52 | 53 | init( 54 | scheme: Scheme, 55 | connectionTarget: ConnectionTarget, 56 | tlsConfiguration: BestEffortHashableTLSConfiguration? = nil, 57 | serverNameIndicatorOverride: String? 58 | ) { 59 | self.scheme = scheme 60 | self.connectionTarget = connectionTarget 61 | self.tlsConfiguration = tlsConfiguration 62 | self.serverNameIndicatorOverride = serverNameIndicatorOverride 63 | } 64 | 65 | var description: String { 66 | var hasher = Hasher() 67 | self.tlsConfiguration?.hash(into: &hasher) 68 | let hash = hasher.finalize() 69 | let hostDescription: String 70 | switch self.connectionTarget { 71 | case .ipAddress(let serialization, let addr): 72 | hostDescription = "\(serialization):\(addr.port!)" 73 | case .domain(let domain, let port): 74 | hostDescription = "\(domain):\(port)" 75 | case .unixSocket(let socketPath): 76 | hostDescription = socketPath 77 | } 78 | return 79 | "\(self.scheme)://\(hostDescription)\(self.serverNameIndicatorOverride.map { " SNI: \($0)" } ?? "") TLS-hash: \(hash) " 80 | } 81 | } 82 | } 83 | 84 | extension DeconstructedURL { 85 | func applyDNSOverride(_ dnsOverride: [String: String]) -> (ConnectionTarget, serverNameIndicatorOverride: String?) { 86 | guard 87 | let originalHost = self.connectionTarget.host, 88 | let hostOverride = dnsOverride[originalHost] 89 | else { 90 | return (self.connectionTarget, nil) 91 | } 92 | return ( 93 | .init(remoteHost: hostOverride, port: self.connectionTarget.port ?? self.scheme.defaultPort), 94 | serverNameIndicatorOverride: originalHost.isIPAddress ? nil : originalHost 95 | ) 96 | } 97 | } 98 | 99 | extension ConnectionPool.Key { 100 | init(url: DeconstructedURL, tlsConfiguration: TLSConfiguration?, dnsOverride: [String: String]) { 101 | let (connectionTarget, serverNameIndicatorOverride) = url.applyDNSOverride(dnsOverride) 102 | self.init( 103 | scheme: url.scheme, 104 | connectionTarget: connectionTarget, 105 | tlsConfiguration: tlsConfiguration.map { 106 | BestEffortHashableTLSConfiguration(wrapping: $0) 107 | }, 108 | serverNameIndicatorOverride: serverNameIndicatorOverride 109 | ) 110 | } 111 | 112 | init(_ request: HTTPClient.Request, dnsOverride: [String: String] = [:]) { 113 | self.init( 114 | url: request.deconstructedURL, 115 | tlsConfiguration: request.tlsConfiguration, 116 | dnsOverride: dnsOverride 117 | ) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool/ChannelHandler/SOCKSEventsHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | import NIOSOCKS 17 | 18 | final class SOCKSEventsHandler: ChannelInboundHandler, RemovableChannelHandler { 19 | typealias InboundIn = NIOAny 20 | 21 | enum State { 22 | // transitions to channelActive or failed 23 | case initialized 24 | // transitions to socksEstablished or failed 25 | case channelActive(Scheduled) 26 | // final success state 27 | case socksEstablished 28 | // final success state 29 | case failed(Error) 30 | } 31 | 32 | private var socksEstablishedPromise: EventLoopPromise? 33 | var socksEstablishedFuture: EventLoopFuture? { 34 | self.socksEstablishedPromise?.futureResult 35 | } 36 | 37 | private let deadline: NIODeadline 38 | private var state: State = .initialized 39 | 40 | init(deadline: NIODeadline) { 41 | self.deadline = deadline 42 | } 43 | 44 | func handlerAdded(context: ChannelHandlerContext) { 45 | self.socksEstablishedPromise = context.eventLoop.makePromise(of: Void.self) 46 | 47 | if context.channel.isActive { 48 | self.connectionStarted(context: context) 49 | } 50 | } 51 | 52 | func handlerRemoved(context: ChannelHandlerContext) { 53 | struct NoResult: Error {} 54 | self.socksEstablishedPromise!.fail(NoResult()) 55 | } 56 | 57 | func channelActive(context: ChannelHandlerContext) { 58 | self.connectionStarted(context: context) 59 | } 60 | 61 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { 62 | guard event is SOCKSProxyEstablishedEvent else { 63 | return context.fireUserInboundEventTriggered(event) 64 | } 65 | 66 | switch self.state { 67 | case .initialized: 68 | preconditionFailure("How can we establish a SOCKS connection, if we are not connected?") 69 | case .socksEstablished: 70 | preconditionFailure("`SOCKSProxyEstablishedEvent` must only be fired once.") 71 | case .channelActive(let scheduled): 72 | self.state = .socksEstablished 73 | scheduled.cancel() 74 | self.socksEstablishedPromise?.succeed(()) 75 | context.fireUserInboundEventTriggered(event) 76 | case .failed: 77 | // potentially a race with the timeout... 78 | break 79 | } 80 | } 81 | 82 | func errorCaught(context: ChannelHandlerContext, error: Error) { 83 | switch self.state { 84 | case .initialized: 85 | self.state = .failed(error) 86 | self.socksEstablishedPromise?.fail(error) 87 | case .channelActive(let scheduled): 88 | scheduled.cancel() 89 | self.state = .failed(error) 90 | self.socksEstablishedPromise?.fail(error) 91 | case .socksEstablished, .failed: 92 | break 93 | } 94 | context.fireErrorCaught(error) 95 | } 96 | 97 | private func connectionStarted(context: ChannelHandlerContext) { 98 | guard case .initialized = self.state else { 99 | return 100 | } 101 | 102 | let scheduled = context.eventLoop.assumeIsolated().scheduleTask(deadline: self.deadline) { 103 | switch self.state { 104 | case .initialized, .channelActive: 105 | // close the connection, if the handshake timed out 106 | context.close(mode: .all, promise: nil) 107 | let error = HTTPClientError.socksHandshakeTimeout 108 | self.state = .failed(error) 109 | self.socksEstablishedPromise?.fail(error) 110 | case .failed, .socksEstablished: 111 | break 112 | } 113 | } 114 | 115 | self.state = .channelActive(scheduled) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool/ChannelHandler/TLSEventsHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | import NIOTLS 17 | 18 | final class TLSEventsHandler: ChannelInboundHandler, RemovableChannelHandler { 19 | typealias InboundIn = NIOAny 20 | 21 | enum State { 22 | // transitions to channelActive or failed 23 | case initialized 24 | // transitions to tlsEstablished or failed 25 | case channelActive(Scheduled?) 26 | // final success state 27 | case tlsEstablished 28 | // final success state 29 | case failed(Error) 30 | } 31 | 32 | private var tlsEstablishedPromise: EventLoopPromise? 33 | var tlsEstablishedFuture: EventLoopFuture? { 34 | self.tlsEstablishedPromise?.futureResult 35 | } 36 | 37 | private let deadline: NIODeadline? 38 | private var state: State = .initialized 39 | 40 | init(deadline: NIODeadline?) { 41 | self.deadline = deadline 42 | } 43 | 44 | func handlerAdded(context: ChannelHandlerContext) { 45 | self.tlsEstablishedPromise = context.eventLoop.makePromise(of: String?.self) 46 | 47 | if context.channel.isActive { 48 | self.connectionStarted(context: context) 49 | } 50 | } 51 | 52 | func handlerRemoved(context: ChannelHandlerContext) { 53 | struct NoResult: Error {} 54 | self.tlsEstablishedPromise!.fail(NoResult()) 55 | } 56 | 57 | func channelActive(context: ChannelHandlerContext) { 58 | self.connectionStarted(context: context) 59 | } 60 | 61 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { 62 | guard let tlsEvent = event as? TLSUserEvent else { 63 | return context.fireUserInboundEventTriggered(event) 64 | } 65 | 66 | switch tlsEvent { 67 | case .handshakeCompleted(negotiatedProtocol: let negotiated): 68 | switch self.state { 69 | case .initialized: 70 | preconditionFailure("How can we establish a TLS connection, if we are not connected?") 71 | case .channelActive(let scheduled): 72 | self.state = .tlsEstablished 73 | scheduled?.cancel() 74 | self.tlsEstablishedPromise?.succeed(negotiated) 75 | context.fireUserInboundEventTriggered(event) 76 | case .tlsEstablished, .failed: 77 | // potentially a race with the timeout... 78 | break 79 | } 80 | case .shutdownCompleted: 81 | break 82 | } 83 | } 84 | 85 | func errorCaught(context: ChannelHandlerContext, error: Error) { 86 | switch self.state { 87 | case .initialized: 88 | self.state = .failed(error) 89 | self.tlsEstablishedPromise?.fail(error) 90 | case .channelActive(let scheduled): 91 | scheduled?.cancel() 92 | self.state = .failed(error) 93 | self.tlsEstablishedPromise?.fail(error) 94 | case .tlsEstablished, .failed: 95 | break 96 | } 97 | context.fireErrorCaught(error) 98 | } 99 | 100 | private func connectionStarted(context: ChannelHandlerContext) { 101 | guard case .initialized = self.state else { 102 | return 103 | } 104 | 105 | var scheduled: Scheduled? 106 | if let deadline = deadline { 107 | scheduled = context.eventLoop.assumeIsolated().scheduleTask(deadline: deadline) { 108 | switch self.state { 109 | case .initialized, .channelActive: 110 | // close the connection, if the handshake timed out 111 | context.close(mode: .all, promise: nil) 112 | let error = HTTPClientError.tlsHandshakeTimeout 113 | self.state = .failed(error) 114 | self.tlsEstablishedPromise?.fail(error) 115 | case .failed, .tlsEstablished: 116 | break 117 | } 118 | } 119 | } 120 | 121 | self.state = .channelActive(scheduled) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool/HTTP1/HTTP1Connection.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | import NIOCore 17 | import NIOHTTP1 18 | import NIOHTTPCompression 19 | 20 | protocol HTTP1ConnectionDelegate: Sendable { 21 | func http1ConnectionReleased(_: HTTPConnectionPool.Connection.ID) 22 | func http1ConnectionClosed(_: HTTPConnectionPool.Connection.ID) 23 | } 24 | 25 | final class HTTP1Connection { 26 | let channel: Channel 27 | 28 | /// the connection's delegate, that will be informed about connection close and connection release 29 | /// (ready to run next request). 30 | let delegate: HTTP1ConnectionDelegate 31 | 32 | enum State { 33 | case initialized 34 | case active 35 | case closed 36 | } 37 | 38 | private var state: State = .initialized 39 | 40 | let id: HTTPConnectionPool.Connection.ID 41 | 42 | init( 43 | channel: Channel, 44 | connectionID: HTTPConnectionPool.Connection.ID, 45 | delegate: HTTP1ConnectionDelegate 46 | ) { 47 | self.channel = channel 48 | self.id = connectionID 49 | self.delegate = delegate 50 | } 51 | 52 | deinit { 53 | guard case .closed = self.state else { 54 | preconditionFailure("Connection must be closed, before we can deinit it") 55 | } 56 | } 57 | 58 | static func start( 59 | channel: Channel, 60 | connectionID: HTTPConnectionPool.Connection.ID, 61 | delegate: HTTP1ConnectionDelegate, 62 | decompression: HTTPClient.Decompression, 63 | logger: Logger 64 | ) throws -> HTTP1Connection { 65 | let connection = HTTP1Connection(channel: channel, connectionID: connectionID, delegate: delegate) 66 | try connection.start(decompression: decompression, logger: logger) 67 | return connection 68 | } 69 | 70 | var sendableView: SendableView { 71 | SendableView(self) 72 | } 73 | 74 | struct SendableView: Sendable { 75 | private let connection: NIOLoopBound 76 | let channel: Channel 77 | let id: HTTPConnectionPool.Connection.ID 78 | private var eventLoop: EventLoop { self.connection.eventLoop } 79 | 80 | init(_ connection: HTTP1Connection) { 81 | self.connection = NIOLoopBound(connection, eventLoop: connection.channel.eventLoop) 82 | self.id = connection.id 83 | self.channel = connection.channel 84 | } 85 | 86 | func executeRequest(_ request: HTTPExecutableRequest) { 87 | self.connection.execute { 88 | $0.execute0(request: request) 89 | } 90 | } 91 | 92 | func shutdown() { 93 | self.channel.triggerUserOutboundEvent(HTTPConnectionEvent.shutdownRequested, promise: nil) 94 | } 95 | 96 | func close(promise: EventLoopPromise?) { 97 | self.channel.close(mode: .all, promise: promise) 98 | } 99 | 100 | func close() -> EventLoopFuture { 101 | let promise = self.eventLoop.makePromise(of: Void.self) 102 | self.close(promise: promise) 103 | return promise.futureResult 104 | } 105 | } 106 | 107 | func taskCompleted() { 108 | self.delegate.http1ConnectionReleased(self.id) 109 | } 110 | 111 | private func execute0(request: HTTPExecutableRequest) { 112 | guard self.channel.isActive else { 113 | return request.fail(ChannelError.ioOnClosedChannel) 114 | } 115 | 116 | self.channel.pipeline.syncOperations.write(NIOAny(request), promise: nil) 117 | } 118 | 119 | private func start(decompression: HTTPClient.Decompression, logger: Logger) throws { 120 | self.channel.eventLoop.assertInEventLoop() 121 | 122 | guard case .initialized = self.state else { 123 | preconditionFailure("Connection must be initialized, to start it") 124 | } 125 | 126 | self.state = .active 127 | self.channel.closeFuture.assumeIsolated().whenComplete { _ in 128 | self.state = .closed 129 | self.delegate.http1ConnectionClosed(self.id) 130 | } 131 | 132 | do { 133 | let sync = self.channel.pipeline.syncOperations 134 | 135 | // We can not use `sync.addHTTPClientHandlers()`, as we want to explicitly set the 136 | // `.informationalResponseStrategy` for the decoder. 137 | let requestEncoder = HTTPRequestEncoder() 138 | let responseDecoder = HTTPResponseDecoder( 139 | leftOverBytesStrategy: .dropBytes, 140 | informationalResponseStrategy: .forward 141 | ) 142 | try sync.addHandler(requestEncoder) 143 | try sync.addHandler(ByteToMessageHandler(responseDecoder)) 144 | 145 | if case .enabled(let limit) = decompression { 146 | let decompressHandler = NIOHTTPResponseDecompressor(limit: limit) 147 | try sync.addHandler(decompressHandler) 148 | } 149 | 150 | let channelHandler = HTTP1ClientChannelHandler( 151 | eventLoop: channel.eventLoop, 152 | backgroundLogger: logger, 153 | connectionIdLoggerMetadata: "\(self.id)" 154 | ) 155 | channelHandler.onConnectionIdle = { 156 | self.taskCompleted() 157 | } 158 | 159 | try sync.addHandler(channelHandler) 160 | } catch { 161 | self.channel.close(mode: .all, promise: nil) 162 | throw error 163 | } 164 | } 165 | } 166 | 167 | @available(*, unavailable) 168 | extension HTTP1Connection: Sendable {} 169 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionEvent.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | enum HTTPConnectionEvent { 16 | case shutdownRequested 17 | } 18 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool/RequestBodyLength.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | 17 | /// - Note: use `HTTPClientRequest.Body.Length` if you want to expose `RequestBodyLength` publicly 18 | @usableFromInline 19 | internal enum RequestBodyLength: Hashable, Sendable { 20 | /// size of the request body is not known before starting the request 21 | case unknown 22 | /// size of the request body is fixed and exactly `count` bytes 23 | case known(_ count: Int64) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool/RequestFramingMetadata.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | struct RequestFramingMetadata: Hashable { 16 | enum Body: Hashable { 17 | case stream 18 | case fixedSize(Int64) 19 | } 20 | 21 | var connectionClose: Bool 22 | var body: Body 23 | } 24 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool/RequestOptions.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | 17 | struct RequestOptions { 18 | /// The maximal `TimeAmount` that is allowed to pass between `channelRead`s from the Channel. 19 | var idleReadTimeout: TimeAmount? 20 | /// The maximal `TimeAmount` that is allowed to pass between `write`s into the Channel. 21 | var idleWriteTimeout: TimeAmount? 22 | /// DNS overrides. 23 | var dnsOverride: [String: String] 24 | 25 | init( 26 | idleReadTimeout: TimeAmount?, 27 | idleWriteTimeout: TimeAmount?, 28 | dnsOverride: [String: String] 29 | ) { 30 | self.idleReadTimeout = idleReadTimeout 31 | self.idleWriteTimeout = idleWriteTimeout 32 | self.dnsOverride = dnsOverride 33 | } 34 | } 35 | 36 | extension RequestOptions { 37 | static func fromClientConfiguration(_ configuration: HTTPClient.Configuration) -> Self { 38 | RequestOptions( 39 | idleReadTimeout: configuration.timeout.read, 40 | idleWriteTimeout: configuration.timeout.write, 41 | dnsOverride: configuration.dnsOverride 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | 17 | #if canImport(Darwin) 18 | import func Darwin.pow 19 | #elseif canImport(Musl) 20 | import func Musl.pow 21 | #elseif canImport(Android) 22 | import func Android.pow 23 | #else 24 | import func Glibc.pow 25 | #endif 26 | 27 | extension HTTPConnectionPool { 28 | /// Calculates the delay for the next connection attempt after the given number of failed `attempts`. 29 | /// 30 | /// Our backoff formula is: 100ms * 1.25^(attempts - 1) that is capped of at 1 minute. 31 | /// This means for: 32 | /// - 1 failed attempt : 100ms 33 | /// - 5 failed attempts: ~300ms 34 | /// - 10 failed attempts: ~930ms 35 | /// - 15 failed attempts: ~2.84s 36 | /// - 20 failed attempts: ~8.67s 37 | /// - 25 failed attempts: ~26s 38 | /// - 29 failed attempts: ~60s (max out) 39 | /// 40 | /// - Parameter attempts: number of failed attempts in a row 41 | /// - Returns: time to wait until trying to establishing a new connection 42 | static func calculateBackoff(failedAttempt attempts: Int) -> TimeAmount { 43 | // Our backoff formula is: 100ms * 1.25^(attempts - 1) that is capped of at 1minute 44 | // This means for: 45 | // - 1 failed attempt : 100ms 46 | // - 5 failed attempts: ~300ms 47 | // - 10 failed attempts: ~930ms 48 | // - 15 failed attempts: ~2.84s 49 | // - 20 failed attempts: ~8.67s 50 | // - 25 failed attempts: ~26s 51 | // - 29 failed attempts: ~60s (max out) 52 | 53 | let start = Double(TimeAmount.milliseconds(100).nanoseconds) 54 | let backoffNanosecondsDouble = start * pow(1.25, Double(attempts - 1)) 55 | 56 | // Cap to 60s _before_ we convert to Int64, to avoid trapping in the Int64 initializer. 57 | let backoffNanoseconds = Int64(min(backoffNanosecondsDouble, Double(TimeAmount.seconds(60).nanoseconds))) 58 | 59 | let backoff = TimeAmount.nanoseconds(backoffNanoseconds) 60 | 61 | // Calculate a 3% jitter range 62 | let jitterRange = (backoff.nanoseconds / 100) * 3 63 | // Pick a random element from the range +/- jitter range. 64 | let jitter: TimeAmount = .nanoseconds((-jitterRange...jitterRange).randomElement()!) 65 | let jitteredBackoff = backoff + jitter 66 | return jitteredBackoff 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/ConnectionTarget.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2019-2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import enum NIOCore.SocketAddress 16 | 17 | enum ConnectionTarget: Equatable, Hashable { 18 | // We keep the IP address serialization precisely as it is in the URL. 19 | // Some platforms have quirks in their implementations of 'ntop', for example 20 | // writing IPv6 addresses as having embedded IPv4 sections (e.g. [::192.168.0.1] vs [::c0a8:1]). 21 | // This serialization includes square brackets, so it is safe to write next to a port number. 22 | // Note: `address` must have an explicit port. 23 | case ipAddress(serialization: String, address: SocketAddress) 24 | case domain(name: String, port: Int) 25 | case unixSocket(path: String) 26 | 27 | init(remoteHost: String, port: Int) { 28 | if let addr = try? SocketAddress(ipAddress: remoteHost, port: port) { 29 | switch addr { 30 | case .v6: 31 | self = .ipAddress(serialization: "[\(remoteHost)]", address: addr) 32 | case .v4: 33 | self = .ipAddress(serialization: remoteHost, address: addr) 34 | case .unixDomainSocket: 35 | fatalError("Expected a remote host") 36 | } 37 | } else { 38 | precondition(!remoteHost.isEmpty, "HTTPClient.Request should already reject empty remote hostnames") 39 | self = .domain(name: remoteHost, port: port) 40 | } 41 | } 42 | } 43 | 44 | extension ConnectionTarget { 45 | /// The host name which will be send as an HTTP `Host` header. 46 | /// Only returns nil if the `self` is a `unixSocket`. 47 | var host: String? { 48 | switch self { 49 | case .ipAddress(let serialization, _): return serialization 50 | case .domain(let name, _): return name 51 | case .unixSocket: return nil 52 | } 53 | } 54 | 55 | /// The host name which will be send as an HTTP host header. 56 | /// Only returns nil if the `self` is a `unixSocket`. 57 | var port: Int? { 58 | switch self { 59 | case .ipAddress(_, let address): return address.port! 60 | case .domain(_, let port): return port 61 | case .unixSocket: return nil 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/DeconstructedURL.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.URL 16 | 17 | struct DeconstructedURL { 18 | var scheme: Scheme 19 | var connectionTarget: ConnectionTarget 20 | var uri: String 21 | 22 | init( 23 | scheme: Scheme, 24 | connectionTarget: ConnectionTarget, 25 | uri: String 26 | ) { 27 | self.scheme = scheme 28 | self.connectionTarget = connectionTarget 29 | self.uri = uri 30 | } 31 | } 32 | 33 | extension DeconstructedURL { 34 | init(url: String) throws { 35 | guard let url = URL(string: url) else { 36 | throw HTTPClientError.invalidURL 37 | } 38 | try self.init(url: url) 39 | } 40 | 41 | init(url: URL) throws { 42 | guard let schemeString = url.scheme else { 43 | throw HTTPClientError.emptyScheme 44 | } 45 | guard let scheme = Scheme(rawValue: schemeString.lowercased()) else { 46 | throw HTTPClientError.unsupportedScheme(schemeString) 47 | } 48 | 49 | switch scheme { 50 | case .http, .https: 51 | #if !canImport(Darwin) && compiler(>=6.0) 52 | guard let urlHost = url.host, !urlHost.isEmpty else { 53 | throw HTTPClientError.emptyHost 54 | } 55 | let host = urlHost.trimIPv6Brackets() 56 | #else 57 | guard let host = url.host, !host.isEmpty else { 58 | throw HTTPClientError.emptyHost 59 | } 60 | #endif 61 | self.init( 62 | scheme: scheme, 63 | connectionTarget: .init(remoteHost: host, port: url.port ?? scheme.defaultPort), 64 | uri: url.uri 65 | ) 66 | 67 | case .httpUnix, .httpsUnix: 68 | guard let socketPath = url.host, !socketPath.isEmpty else { 69 | throw HTTPClientError.missingSocketPath 70 | } 71 | self.init( 72 | scheme: scheme, 73 | connectionTarget: .unixSocket(path: socketPath), 74 | uri: url.uri 75 | ) 76 | 77 | case .unix: 78 | let socketPath = url.baseURL?.path ?? url.path 79 | let uri = url.baseURL != nil ? url.uri : "/" 80 | guard !socketPath.isEmpty else { 81 | throw HTTPClientError.missingSocketPath 82 | } 83 | self.init( 84 | scheme: scheme, 85 | connectionTarget: .unixSocket(path: socketPath), 86 | uri: uri 87 | ) 88 | } 89 | } 90 | } 91 | 92 | #if !canImport(Darwin) && compiler(>=6.0) 93 | extension String { 94 | @inlinable internal func trimIPv6Brackets() -> String { 95 | var utf8View = self.utf8[...] 96 | 97 | var modified = false 98 | if utf8View.first == UInt8(ascii: "[") { 99 | utf8View = utf8View.dropFirst() 100 | modified = true 101 | } 102 | if utf8View.last == UInt8(ascii: "]") { 103 | utf8View = utf8View.dropLast() 104 | modified = true 105 | } 106 | 107 | if modified { 108 | return String(Substring(utf8View)) 109 | } 110 | return self 111 | } 112 | } 113 | #endif 114 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/FoundationExtensions.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2018-2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | // Extensions which provide better ergonomics when using Foundation types, 16 | // or by using Foundation APIs. 17 | 18 | import Foundation 19 | 20 | extension HTTPClient.Cookie { 21 | /// The cookie's expiration date. 22 | public var expires: Date? { 23 | get { 24 | expires_timestamp.map { Date(timeIntervalSince1970: TimeInterval($0)) } 25 | } 26 | set { 27 | expires_timestamp = newValue.map { Int64($0.timeIntervalSince1970) } 28 | } 29 | } 30 | 31 | /// Create HTTP cookie. 32 | /// 33 | /// - parameters: 34 | /// - name: The name of the cookie. 35 | /// - value: The cookie's string value. 36 | /// - path: The cookie's path. 37 | /// - domain: The domain of the cookie, defaults to nil. 38 | /// - expires: The cookie's expiration date, defaults to nil. 39 | /// - maxAge: The cookie's age in seconds, defaults to nil. 40 | /// - httpOnly: Whether this cookie should be used by HTTP servers only, defaults to false. 41 | /// - secure: Whether this cookie should only be sent using secure channels, defaults to false. 42 | public init( 43 | name: String, 44 | value: String, 45 | path: String = "/", 46 | domain: String? = nil, 47 | expires: Date? = nil, 48 | maxAge: Int? = nil, 49 | httpOnly: Bool = false, 50 | secure: Bool = false 51 | ) { 52 | // FIXME: This should be failable and validate the inputs 53 | // (for example, checking that the strings are ASCII, path begins with "/", domain is not empty, etc). 54 | self.init( 55 | name: name, 56 | value: value, 57 | path: path, 58 | domain: domain, 59 | expires_timestamp: expires.map { Int64($0.timeIntervalSince1970) }, 60 | maxAge: maxAge, 61 | httpOnly: httpOnly, 62 | secure: secure 63 | ) 64 | } 65 | } 66 | 67 | extension HTTPClient.Body { 68 | /// Create and stream body using `Data`. 69 | /// 70 | /// - parameters: 71 | /// - data: Body `Data` representation. 72 | public static func data(_ data: Data) -> HTTPClient.Body { 73 | self.bytes(data) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/HTTPClient+Proxy.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | 17 | extension HTTPClient.Configuration { 18 | /// Proxy server configuration 19 | /// Specifies the remote address of an HTTP proxy. 20 | /// 21 | /// Adding an `Proxy` to your client's `HTTPClient.Configuration` 22 | /// will cause requests to be passed through the specified proxy using the 23 | /// HTTP `CONNECT` method. 24 | /// 25 | /// If a `TLSConfiguration` is used in conjunction with `HTTPClient.Configuration.Proxy`, 26 | /// TLS will be established _after_ successful proxy, between your client 27 | /// and the destination server. 28 | public struct Proxy: Sendable, Hashable { 29 | enum ProxyType: Hashable { 30 | case http(HTTPClient.Authorization?) 31 | case socks 32 | } 33 | 34 | /// Specifies Proxy server host. 35 | public var host: String 36 | /// Specifies Proxy server port. 37 | public var port: Int 38 | /// Specifies Proxy server authorization. 39 | public var authorization: HTTPClient.Authorization? { 40 | set { 41 | precondition( 42 | self.type == .http(self.authorization), 43 | "SOCKS authorization support is not yet implemented." 44 | ) 45 | self.type = .http(newValue) 46 | } 47 | 48 | get { 49 | switch self.type { 50 | case .http(let authorization): 51 | return authorization 52 | case .socks: 53 | return nil 54 | } 55 | } 56 | } 57 | 58 | var type: ProxyType 59 | 60 | /// Create a HTTP proxy. 61 | /// 62 | /// - parameters: 63 | /// - host: proxy server host. 64 | /// - port: proxy server port. 65 | public static func server(host: String, port: Int) -> Proxy { 66 | .init(host: host, port: port, type: .http(nil)) 67 | } 68 | 69 | /// Create a HTTP proxy. 70 | /// 71 | /// - parameters: 72 | /// - host: proxy server host. 73 | /// - port: proxy server port. 74 | /// - authorization: proxy server authorization. 75 | public static func server(host: String, port: Int, authorization: HTTPClient.Authorization? = nil) -> Proxy { 76 | .init(host: host, port: port, type: .http(authorization)) 77 | } 78 | 79 | /// Create a SOCKSv5 proxy. 80 | /// - parameter host: The SOCKSv5 proxy address. 81 | /// - parameter port: The SOCKSv5 proxy port, defaults to 1080. 82 | /// - returns: A new instance of `Proxy` configured to connect to a `SOCKSv5` server. 83 | public static func socksServer(host: String, port: Int = 1080) -> Proxy { 84 | .init(host: host, port: port, type: .socks) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/HTTPClient+StructuredConcurrency.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | import NIO 17 | 18 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 19 | extension HTTPClient { 20 | #if compiler(>=6.0) 21 | /// Start & automatically shut down a new ``HTTPClient``. 22 | /// 23 | /// This method allows to start & automatically dispose of a ``HTTPClient`` following the principle of Structured Concurrency. 24 | /// The ``HTTPClient`` is guaranteed to be shut down upon return, whether `body` throws or not. 25 | /// 26 | /// This may be particularly useful if you cannot use the shared singleton (``HTTPClient/shared``). 27 | public static func withHTTPClient( 28 | eventLoopGroup: any EventLoopGroup = HTTPClient.defaultEventLoopGroup, 29 | configuration: Configuration = Configuration(), 30 | backgroundActivityLogger: Logger? = nil, 31 | isolation: isolated (any Actor)? = #isolation, 32 | _ body: (HTTPClient) async throws -> Return 33 | ) async throws -> Return { 34 | let logger = (backgroundActivityLogger ?? HTTPClient.loggingDisabled) 35 | let httpClient = HTTPClient( 36 | eventLoopGroup: eventLoopGroup, 37 | configuration: configuration, 38 | backgroundActivityLogger: logger 39 | ) 40 | return try await asyncDo { 41 | try await body(httpClient) 42 | } finally: { _ in 43 | try await httpClient.shutdown() 44 | } 45 | } 46 | #else 47 | /// Start & automatically shut down a new ``HTTPClient``. 48 | /// 49 | /// This method allows to start & automatically dispose of a ``HTTPClient`` following the principle of Structured Concurrency. 50 | /// The ``HTTPClient`` is guaranteed to be shut down upon return, whether `body` throws or not. 51 | /// 52 | /// This may be particularly useful if you cannot use the shared singleton (``HTTPClient/shared``). 53 | public static func withHTTPClient( 54 | eventLoopGroup: any EventLoopGroup = HTTPClient.defaultEventLoopGroup, 55 | configuration: Configuration = Configuration(), 56 | backgroundActivityLogger: Logger? = nil, 57 | _ body: (HTTPClient) async throws -> Return 58 | ) async throws -> Return { 59 | let logger = (backgroundActivityLogger ?? HTTPClient.loggingDisabled) 60 | let httpClient = HTTPClient( 61 | eventLoopGroup: eventLoopGroup, 62 | configuration: configuration, 63 | backgroundActivityLogger: logger 64 | ) 65 | return try await asyncDo { 66 | try await body(httpClient) 67 | } finally: { _ in 68 | try await httpClient.shutdown() 69 | } 70 | } 71 | #endif 72 | } 73 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/LRUCache.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | struct LRUCache { 16 | private typealias Generation = UInt64 17 | private struct Element { 18 | var generation: Generation 19 | var key: Key 20 | var value: Value 21 | } 22 | 23 | private let capacity: Int 24 | private var generation: Generation = 0 25 | private var elements: [Element] 26 | 27 | init(capacity: Int = 8) { 28 | precondition(capacity > 0, "capacity needs to be > 0") 29 | self.capacity = capacity 30 | self.elements = [] 31 | self.elements.reserveCapacity(capacity) 32 | } 33 | 34 | private mutating func bumpGenerationAndFindIndex(key: Key) -> Int? { 35 | self.generation += 1 36 | 37 | let found = self.elements.firstIndex { element in 38 | element.key == key 39 | } 40 | 41 | return found 42 | } 43 | 44 | mutating func find(key: Key) -> Value? { 45 | if let found = self.bumpGenerationAndFindIndex(key: key) { 46 | self.elements[found].generation = self.generation 47 | return self.elements[found].value 48 | } else { 49 | return nil 50 | } 51 | } 52 | 53 | @discardableResult 54 | mutating func append(key: Key, value: Value) -> Value { 55 | let newElement = Element( 56 | generation: self.generation, 57 | key: key, 58 | value: value 59 | ) 60 | if let found = self.bumpGenerationAndFindIndex(key: key) { 61 | self.elements[found] = newElement 62 | return value 63 | } 64 | 65 | if self.elements.count < self.capacity { 66 | self.elements.append(newElement) 67 | return value 68 | } 69 | assert(self.elements.count == self.capacity) 70 | assert(self.elements.count > 0) 71 | 72 | let minIndex = self.elements.minIndex { l, r in 73 | l.generation < r.generation 74 | }! 75 | 76 | self.elements.swapAt(minIndex, self.elements.endIndex - 1) 77 | self.elements.removeLast() 78 | self.elements.append(newElement) 79 | 80 | return value 81 | } 82 | 83 | mutating func findOrAppend(key: Key, _ valueGenerator: (Key) -> Value) -> Value { 84 | if let found = self.find(key: key) { 85 | return found 86 | } 87 | 88 | return self.append(key: key, value: valueGenerator(key)) 89 | } 90 | } 91 | 92 | extension Array { 93 | func minIndex(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> Index? { 94 | guard var minSoFar: (Index, Element) = self.first.map({ (0, $0) }) else { 95 | return nil 96 | } 97 | 98 | for indexElement in self.enumerated() { 99 | if try areInIncreasingOrder(indexElement.1, minSoFar.1) { 100 | minSoFar = indexElement 101 | } 102 | } 103 | 104 | return minSoFar.0 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/NIOLoopBound+Execute.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | 17 | extension NIOLoopBound { 18 | @inlinable 19 | func execute(_ body: @Sendable @escaping (Value) -> Void) { 20 | if self.eventLoop.inEventLoop { 21 | body(self.value) 22 | } else { 23 | self.eventLoop.execute { 24 | body(self.value) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2020 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | import NIOHTTP1 17 | import NIOTransportServices 18 | 19 | #if canImport(Network) 20 | import Network 21 | #endif 22 | 23 | extension HTTPClient { 24 | #if canImport(Network) 25 | /// A wrapper for `POSIX` errors thrown by `Network.framework`. 26 | public struct NWPOSIXError: Error, CustomStringConvertible { 27 | /// POSIX error code (enum) 28 | public let errorCode: POSIXErrorCode 29 | 30 | /// actual reason, in human readable form 31 | private let reason: String 32 | 33 | /// Initialise a NWPOSIXError 34 | /// - Parameters: 35 | /// - errorType: posix error type 36 | /// - reason: String describing reason for error 37 | public init(_ errorCode: POSIXErrorCode, reason: String) { 38 | self.errorCode = errorCode 39 | self.reason = reason 40 | } 41 | 42 | public var description: String { self.reason } 43 | } 44 | 45 | /// A wrapper for TLS errors thrown by `Network.framework`. 46 | public struct NWTLSError: Error, CustomStringConvertible { 47 | /// TLS error status. List of TLS errors can be found in `` 48 | public let status: OSStatus 49 | 50 | /// actual reason, in human readable form 51 | private let reason: String 52 | 53 | /// initialise a NWTLSError 54 | /// - Parameters: 55 | /// - status: TLS status 56 | /// - reason: String describing reason for error 57 | public init(_ status: OSStatus, reason: String) { 58 | self.status = status 59 | self.reason = reason 60 | } 61 | 62 | public var description: String { self.reason } 63 | } 64 | #endif 65 | 66 | final class NWErrorHandler: ChannelInboundHandler { 67 | typealias InboundIn = HTTPClientResponsePart 68 | 69 | func errorCaught(context: ChannelHandlerContext, error: Error) { 70 | context.fireErrorCaught(NWErrorHandler.translateError(error)) 71 | } 72 | 73 | static func translateError(_ error: Error) -> Error { 74 | #if canImport(Network) 75 | if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { 76 | if let error = error as? NWError { 77 | switch error { 78 | case .tls(let status): 79 | return NWTLSError(status, reason: String(describing: error)) 80 | case .posix(let errorCode): 81 | return NWPOSIXError(errorCode, reason: String(describing: error)) 82 | default: 83 | return error 84 | } 85 | } 86 | return error 87 | } else { 88 | preconditionFailure("\(self) used on a non-NIOTS Channel") 89 | } 90 | #else 91 | preconditionFailure("\(self) used on a non-NIOTS Channel") 92 | #endif 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/NIOTransportServices/NWWaitingHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if canImport(Network) 16 | import Network 17 | import NIOCore 18 | import NIOHTTP1 19 | import NIOTransportServices 20 | 21 | @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) 22 | final class NWWaitingHandler: ChannelInboundHandler { 23 | typealias InboundIn = Any 24 | typealias InboundOut = Any 25 | 26 | private var requester: Requester 27 | private let connectionID: HTTPConnectionPool.Connection.ID 28 | 29 | init(requester: Requester, connectionID: HTTPConnectionPool.Connection.ID) { 30 | self.requester = requester 31 | self.connectionID = connectionID 32 | } 33 | 34 | func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { 35 | if let waitingEvent = event as? NIOTSNetworkEvents.WaitingForConnectivity { 36 | self.requester.waitingForConnectivity( 37 | self.connectionID, 38 | error: HTTPClient.NWErrorHandler.translateError(waitingEvent.transientError) 39 | ) 40 | } 41 | context.fireUserInboundEventTriggered(event) 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/RedirectState.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOHTTP1 16 | 17 | import struct Foundation.URL 18 | 19 | typealias RedirectMode = HTTPClient.Configuration.RedirectConfiguration.Mode 20 | 21 | struct RedirectState { 22 | /// number of redirects we are allowed to follow. 23 | private var limit: Int 24 | 25 | /// All visited URLs. 26 | private var visited: [String] 27 | 28 | /// if true, `redirect(to:)` will throw an error if a cycle is detected. 29 | private let allowCycles: Bool 30 | } 31 | 32 | extension RedirectState { 33 | /// Creates a `RedirectState` from a configuration. 34 | /// Returns nil if the user disallowed redirects, 35 | /// otherwise an instance of `RedirectState` which respects the user defined settings. 36 | init?( 37 | _ configuration: RedirectMode, 38 | initialURL: String 39 | ) { 40 | switch configuration { 41 | case .disallow: 42 | return nil 43 | case .follow(let maxRedirects, let allowCycles): 44 | self.init(limit: maxRedirects, visited: [initialURL], allowCycles: allowCycles) 45 | } 46 | } 47 | } 48 | 49 | extension RedirectState { 50 | /// Call this method when you are about to do a redirect to the given `redirectURL`. 51 | /// This method records that URL into `self`. 52 | /// - Parameter redirectURL: the new URL to redirect the request to 53 | /// - Throws: if it reaches the redirect limit or detects a redirect cycle if and `allowCycles` is false 54 | mutating func redirect(to redirectURL: String) throws { 55 | guard self.visited.count <= limit else { 56 | throw HTTPClientError.redirectLimitReached 57 | } 58 | 59 | guard allowCycles || !self.visited.contains(redirectURL) else { 60 | throw HTTPClientError.redirectCycleDetected 61 | } 62 | self.visited.append(redirectURL) 63 | } 64 | } 65 | 66 | extension HTTPHeaders { 67 | /// Tries to extract a redirect URL from the `location` header if the `status` indicates it should do so. 68 | /// It also validates that we can redirect to the scheme of the extracted redirect URL from the `originalScheme`. 69 | /// - Parameters: 70 | /// - status: response status of the request 71 | /// - originalURL: url of the previous request 72 | /// - originalScheme: scheme of the previous request 73 | /// - Returns: redirect URL to follow 74 | func extractRedirectTarget( 75 | status: HTTPResponseStatus, 76 | originalURL: URL, 77 | originalScheme: Scheme 78 | ) -> URL? { 79 | switch status { 80 | case .movedPermanently, .found, .seeOther, .notModified, .useProxy, .temporaryRedirect, .permanentRedirect: 81 | break 82 | default: 83 | return nil 84 | } 85 | 86 | guard let location = self.first(name: "Location") else { 87 | return nil 88 | } 89 | 90 | guard let url = URL(string: location, relativeTo: originalURL) else { 91 | return nil 92 | } 93 | 94 | guard originalScheme.supportsRedirects(to: url.scheme) else { 95 | return nil 96 | } 97 | 98 | if url.isFileURL { 99 | return nil 100 | } 101 | 102 | return url.absoluteURL 103 | } 104 | } 105 | 106 | /// Transforms the original `requestMethod`, `requestHeaders` and `requestBody` to be ready to be send out as a new request to the `redirectURL`. 107 | /// - Returns: New `HTTPMethod`, `HTTPHeaders` and `Body` to be send as a new request to `redirectURL` 108 | func transformRequestForRedirect( 109 | from originalURL: URL, 110 | method requestMethod: HTTPMethod, 111 | headers requestHeaders: HTTPHeaders, 112 | body requestBody: Body?, 113 | to redirectURL: URL, 114 | status responseStatus: HTTPResponseStatus 115 | ) -> (HTTPMethod, HTTPHeaders, Body?) { 116 | let convertToGet: Bool 117 | if responseStatus == .seeOther, requestMethod != .HEAD { 118 | convertToGet = true 119 | } else if responseStatus == .movedPermanently || responseStatus == .found, requestMethod == .POST { 120 | convertToGet = true 121 | } else { 122 | convertToGet = false 123 | } 124 | 125 | var method = requestMethod 126 | var headers = requestHeaders 127 | var body = requestBody 128 | 129 | if convertToGet { 130 | method = .GET 131 | body = nil 132 | headers.remove(name: "Content-Length") 133 | headers.remove(name: "Content-Type") 134 | } 135 | 136 | if !originalURL.hasTheSameOrigin(as: redirectURL) { 137 | headers.remove(name: "Origin") 138 | headers.remove(name: "Cookie") 139 | headers.remove(name: "Authorization") 140 | headers.remove(name: "Proxy-Authorization") 141 | } 142 | return (method, headers, body) 143 | } 144 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/SSLContextCache.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Dispatch 16 | import Logging 17 | import NIOConcurrencyHelpers 18 | import NIOCore 19 | import NIOSSL 20 | 21 | final class SSLContextCache { 22 | private let lock = NIOLock() 23 | private var sslContextCache = LRUCache() 24 | private let offloadQueue = DispatchQueue(label: "io.github.swift-server.AsyncHTTPClient.SSLContextCache") 25 | } 26 | 27 | extension SSLContextCache { 28 | func sslContext( 29 | tlsConfiguration: TLSConfiguration, 30 | eventLoop: EventLoop, 31 | logger: Logger 32 | ) -> EventLoopFuture { 33 | let eqTLSConfiguration = BestEffortHashableTLSConfiguration(wrapping: tlsConfiguration) 34 | let sslContext = self.lock.withLock { 35 | self.sslContextCache.find(key: eqTLSConfiguration) 36 | } 37 | 38 | if let sslContext = sslContext { 39 | logger.trace( 40 | "found SSL context in cache", 41 | metadata: ["ahc-tls-config": "\(tlsConfiguration)"] 42 | ) 43 | return eventLoop.makeSucceededFuture(sslContext) 44 | } 45 | 46 | logger.trace( 47 | "creating new SSL context", 48 | metadata: ["ahc-tls-config": "\(tlsConfiguration)"] 49 | ) 50 | let newSSLContext = self.offloadQueue.asyncWithFuture(eventLoop: eventLoop) { 51 | try NIOSSLContext(configuration: tlsConfiguration) 52 | } 53 | 54 | newSSLContext.whenSuccess { (newSSLContext: NIOSSLContext) -> Void in 55 | self.lock.withLock { () -> Void in 56 | self.sslContextCache.append( 57 | key: eqTLSConfiguration, 58 | value: newSSLContext 59 | ) 60 | } 61 | } 62 | 63 | return newSSLContext 64 | } 65 | } 66 | 67 | extension SSLContextCache: @unchecked Sendable {} 68 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/Scheme.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// List of schemes `HTTPClient` currently supports 16 | enum Scheme: String { 17 | case http 18 | case https 19 | case unix 20 | case httpUnix = "http+unix" 21 | case httpsUnix = "https+unix" 22 | } 23 | 24 | extension Scheme { 25 | var usesTLS: Bool { 26 | switch self { 27 | case .http, .httpUnix, .unix: 28 | return false 29 | case .https, .httpsUnix: 30 | return true 31 | } 32 | } 33 | 34 | var defaultPort: Int { 35 | self.usesTLS ? 443 : 80 36 | } 37 | } 38 | 39 | extension Scheme { 40 | func supportsRedirects(to destinationScheme: String?) -> Bool { 41 | guard 42 | let destinationSchemeString = destinationScheme?.lowercased(), 43 | let destinationScheme = Self(rawValue: destinationSchemeString) 44 | else { 45 | return false 46 | } 47 | return self.supportsRedirects(to: destinationScheme) 48 | } 49 | 50 | func supportsRedirects(to destinationScheme: Self) -> Bool { 51 | switch self { 52 | case .http, .https: 53 | switch destinationScheme { 54 | case .http, .https: 55 | return true 56 | case .unix, .httpUnix, .httpsUnix: 57 | return false 58 | } 59 | case .unix, .httpUnix, .httpsUnix: 60 | return true 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/Singleton.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension HTTPClient { 16 | /// A globally shared, singleton ``HTTPClient``. 17 | /// 18 | /// The returned client uses the following settings: 19 | /// - configuration is ``HTTPClient/Configuration/singletonConfiguration`` (matching the platform's default/prevalent browser as well as possible) 20 | /// - `EventLoopGroup` is ``HTTPClient/defaultEventLoopGroup`` (matching the platform default) 21 | /// - logging is disabled 22 | public static var shared: HTTPClient { 23 | globallySharedHTTPClient 24 | } 25 | } 26 | 27 | private let globallySharedHTTPClient: HTTPClient = { 28 | let httpClient = HTTPClient( 29 | eventLoopGroup: HTTPClient.defaultEventLoopGroup, 30 | configuration: .singletonConfiguration, 31 | backgroundActivityLogger: HTTPClient.loggingDisabled, 32 | canBeShutDown: false 33 | ) 34 | return httpClient 35 | }() 36 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/StringConvertibleInstances.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2020-2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | extension HTTPClient.EventLoopPreference: CustomStringConvertible { 16 | public var description: String { 17 | "\(self.preference)" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/StructuredConcurrencyHelpers.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | // swift-format-ignore 15 | // Note: Whitespace changes are used to workaround compiler bug 16 | // https://github.com/swiftlang/swift/issues/79285 17 | 18 | #if compiler(>=6.0) 19 | @inlinable 20 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 21 | internal func asyncDo( 22 | isolation: isolated (any Actor)? = #isolation, 23 | // DO NOT FIX THE WHITESPACE IN THE NEXT LINE UNTIL 5.10 IS UNSUPPORTED 24 | // https://github.com/swiftlang/swift/issues/79285 25 | _ body: () async throws -> sending R, finally: sending @escaping ((any Error)?) async throws -> Void) async throws -> sending R { 26 | let result: R 27 | do { 28 | result = try await body() 29 | } catch { 30 | // `body` failed, we need to invoke `finally` with the `error`. 31 | 32 | // This _looks_ unstructured but isn't really because we unconditionally always await the return. 33 | // We need to have an uncancelled task here to assure this is actually running in case we hit a 34 | // cancellation error. 35 | try await Task { 36 | try await finally(error) 37 | }.value 38 | throw error 39 | } 40 | 41 | // `body` succeeded, we need to invoke `finally` with `nil` (no error). 42 | 43 | // This _looks_ unstructured but isn't really because we unconditionally always await the return. 44 | // We need to have an uncancelled task here to assure this is actually running in case we hit a 45 | // cancellation error. 46 | try await Task { 47 | try await finally(nil) 48 | }.value 49 | return result 50 | } 51 | #else 52 | @inlinable 53 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 54 | internal func asyncDo( 55 | _ body: () async throws -> R, 56 | finally: @escaping @Sendable ((any Error)?) async throws -> Void 57 | ) async throws -> R { 58 | let result: R 59 | do { 60 | result = try await body() 61 | } catch { 62 | // `body` failed, we need to invoke `finally` with the `error`. 63 | 64 | // This _looks_ unstructured but isn't really because we unconditionally always await the return. 65 | // We need to have an uncancelled task here to assure this is actually running in case we hit a 66 | // cancellation error. 67 | try await Task { 68 | try await finally(error) 69 | }.value 70 | throw error 71 | } 72 | 73 | // `body` succeeded, we need to invoke `finally` with `nil` (no error). 74 | 75 | // This _looks_ unstructured but isn't really because we unconditionally always await the return. 76 | // We need to have an uncancelled task here to assure this is actually running in case we hit a 77 | // cancellation error. 78 | try await Task { 79 | try await finally(nil) 80 | }.value 81 | return result 82 | } 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/AsyncHTTPClient/Utils.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2018-2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | 17 | /// An ``HTTPClientResponseDelegate`` that wraps a callback. 18 | /// 19 | /// ``HTTPClientCopyingDelegate`` discards most parts of a HTTP response, but streams the body 20 | /// to the `chunkHandler` provided on ``init(chunkHandler:)``. This is mostly useful for testing. 21 | public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate, Sendable { 22 | public typealias Response = Void 23 | 24 | let chunkHandler: @Sendable (ByteBuffer) -> EventLoopFuture 25 | 26 | @preconcurrency 27 | public init(chunkHandler: @Sendable @escaping (ByteBuffer) -> EventLoopFuture) { 28 | self.chunkHandler = chunkHandler 29 | } 30 | 31 | public func didReceiveBodyPart(task: HTTPClient.Task, _ buffer: ByteBuffer) -> EventLoopFuture { 32 | self.chunkHandler(buffer) 33 | } 34 | 35 | public func didFinishRequest(task: HTTPClient.Task) throws { 36 | () 37 | } 38 | } 39 | 40 | /// A utility function that runs the body code only in debug builds, without 41 | /// emitting compiler warnings. 42 | /// 43 | /// This is currently the only way to do this in Swift: see 44 | /// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. 45 | @inlinable 46 | internal func debugOnly(_ body: () -> Void) { 47 | assert( 48 | { 49 | body() 50 | return true 51 | }() 52 | ) 53 | } 54 | 55 | extension BidirectionalCollection where Element: Equatable { 56 | /// Returns a Boolean value indicating whether the collection ends with the specified suffix. 57 | /// 58 | /// If `suffix` is empty, this function returns `true`. 59 | /// If all elements of the collections are equal, this function also returns `true`. 60 | func hasSuffix(_ suffix: Suffix) -> Bool where Suffix: BidirectionalCollection, Suffix.Element == Element { 61 | var ourIdx = self.endIndex 62 | var suffixIdx = suffix.endIndex 63 | while ourIdx > self.startIndex, suffixIdx > suffix.startIndex { 64 | self.formIndex(before: &ourIdx) 65 | suffix.formIndex(before: &suffixIdx) 66 | guard self[ourIdx] == suffix[suffixIdx] else { return false } 67 | } 68 | guard suffixIdx == suffix.startIndex else { 69 | return false // Exhausted self, but 'suffix' has elements remaining. 70 | } 71 | return true // Exhausted 'other' without finding a mismatch. 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/CAsyncHTTPClient/CAsyncHTTPClient.c: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2018-2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if __APPLE__ 16 | #include 17 | #elif __linux__ 18 | #include 19 | #endif 20 | 21 | #include 22 | #include 23 | 24 | bool swiftahc_cshims_strptime(const char * string, const char * format, struct tm * result) { 25 | const char * firstNonProcessed = strptime(string, format, result); 26 | if (firstNonProcessed) { 27 | return *firstNonProcessed == 0; 28 | } 29 | return false; 30 | } 31 | 32 | bool swiftahc_cshims_strptime_l(const char * string, const char * format, struct tm * result, void * locale) { 33 | // The pointer cast is fine as long we make sure it really points to a locale_t. 34 | #if defined(__musl__) || defined(__ANDROID__) 35 | const char * firstNonProcessed = strptime(string, format, result); 36 | #else 37 | const char * firstNonProcessed = strptime_l(string, format, result, (locale_t)locale); 38 | #endif 39 | if (firstNonProcessed) { 40 | return *firstNonProcessed == 0; 41 | } 42 | return false; 43 | } 44 | -------------------------------------------------------------------------------- /Sources/CAsyncHTTPClient/include/CAsyncHTTPClient.h: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2018-2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #ifndef CASYNC_HTTP_CLIENT_H 16 | #define CASYNC_HTTP_CLIENT_H 17 | 18 | #include 19 | #include 20 | 21 | bool swiftahc_cshims_strptime( 22 | const char * _Nonnull input, 23 | const char * _Nonnull format, 24 | struct tm * _Nonnull result 25 | ); 26 | 27 | bool swiftahc_cshims_strptime_l( 28 | const char * _Nonnull input, 29 | const char * _Nonnull format, 30 | struct tm * _Nonnull result, 31 | void * _Nullable locale 32 | ); 33 | 34 | #endif // CASYNC_HTTP_CLIENT_H 35 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/AsyncTestHelpers.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOConcurrencyHelpers 16 | import NIOCore 17 | 18 | /// ``AsyncSequenceWriter`` is `Sendable` because its state is protected by a Lock 19 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 20 | final class AsyncSequenceWriter: AsyncSequence, @unchecked Sendable { 21 | typealias AsyncIterator = Iterator 22 | 23 | struct Iterator: AsyncIteratorProtocol { 24 | private let writer: AsyncSequenceWriter 25 | 26 | init(_ writer: AsyncSequenceWriter) { 27 | self.writer = writer 28 | } 29 | 30 | mutating func next() async throws -> Element? { 31 | try await self.writer.next() 32 | } 33 | } 34 | 35 | func makeAsyncIterator() -> Iterator { 36 | Iterator(self) 37 | } 38 | 39 | private enum State { 40 | case buffering(CircularBuffer, CheckedContinuation?) 41 | case finished 42 | case waiting(CheckedContinuation) 43 | case failed(Error, CheckedContinuation?) 44 | } 45 | 46 | private var _state = State.buffering(.init(), nil) 47 | private let lock = NIOLock() 48 | 49 | public var hasDemand: Bool { 50 | self.lock.withLock { 51 | switch self._state { 52 | case .failed, .finished, .buffering: 53 | return false 54 | case .waiting: 55 | return true 56 | } 57 | } 58 | } 59 | 60 | /// Wait until a downstream consumer has issued more demand by calling `next`. 61 | public func demand() async { 62 | self.lock.lock() 63 | 64 | switch self._state { 65 | case .buffering(let buffer, .none): 66 | await withCheckedContinuation { (continuation: CheckedContinuation) in 67 | self._state = .buffering(buffer, continuation) 68 | self.lock.unlock() 69 | } 70 | 71 | case .waiting: 72 | self.lock.unlock() 73 | return 74 | 75 | case .buffering(_, .some), .failed(_, .some): 76 | let state = self._state 77 | self.lock.unlock() 78 | preconditionFailure("Already waiting for demand. Invalid state: \(state)") 79 | 80 | case .finished, .failed: 81 | let state = self._state 82 | self.lock.unlock() 83 | preconditionFailure("Invalid state: \(state)") 84 | } 85 | } 86 | 87 | private func next() async throws -> Element? { 88 | self.lock.lock() 89 | switch self._state { 90 | case .buffering(let buffer, let demandContinuation) where buffer.isEmpty: 91 | return try await withCheckedThrowingContinuation { continuation in 92 | self._state = .waiting(continuation) 93 | self.lock.unlock() 94 | demandContinuation?.resume(returning: ()) 95 | } 96 | 97 | case .buffering(var buffer, let demandContinuation): 98 | let first = buffer.removeFirst() 99 | if first != nil { 100 | self._state = .buffering(buffer, demandContinuation) 101 | } else { 102 | self._state = .finished 103 | } 104 | self.lock.unlock() 105 | return first 106 | 107 | case .failed(let error, let demandContinuation): 108 | self._state = .finished 109 | self.lock.unlock() 110 | demandContinuation?.resume() 111 | throw error 112 | 113 | case .finished: 114 | self.lock.unlock() 115 | return nil 116 | 117 | case .waiting: 118 | let state = self._state 119 | self.lock.unlock() 120 | preconditionFailure( 121 | "Expected that there is always only one concurrent call to next. Invalid state: \(state)" 122 | ) 123 | } 124 | } 125 | 126 | public func write(_ element: Element) { 127 | self.writeBufferOrEnd(element) 128 | } 129 | 130 | public func end() { 131 | self.writeBufferOrEnd(nil) 132 | } 133 | 134 | private enum WriteAction { 135 | case succeedContinuation(CheckedContinuation, Element?) 136 | case none 137 | } 138 | 139 | private func writeBufferOrEnd(_ element: Element?) { 140 | let writeAction = self.lock.withLock { () -> WriteAction in 141 | switch self._state { 142 | case .buffering(var buffer, let continuation): 143 | buffer.append(element) 144 | self._state = .buffering(buffer, continuation) 145 | return .none 146 | 147 | case .waiting(let continuation): 148 | self._state = .buffering(.init(), nil) 149 | return .succeedContinuation(continuation, element) 150 | 151 | case .finished, .failed: 152 | preconditionFailure("Invalid state: \(self._state)") 153 | } 154 | } 155 | 156 | switch writeAction { 157 | case .succeedContinuation(let continuation, let element): 158 | continuation.resume(returning: element) 159 | 160 | case .none: 161 | break 162 | } 163 | } 164 | 165 | private enum ErrorAction { 166 | case failContinuation(CheckedContinuation, Error) 167 | case none 168 | } 169 | 170 | /// Drops all buffered writes and emits an error on the waiting `next`. If there is no call to `next` 171 | /// waiting, will emit the error on the next call to `next`. 172 | public func fail(_ error: Error) { 173 | let errorAction = self.lock.withLock { () -> ErrorAction in 174 | switch self._state { 175 | case .buffering(_, let demandContinuation): 176 | self._state = .failed(error, demandContinuation) 177 | return .none 178 | 179 | case .failed, .finished: 180 | return .none 181 | 182 | case .waiting(let continuation): 183 | self._state = .finished 184 | return .failContinuation(continuation, error) 185 | } 186 | } 187 | 188 | switch errorAction { 189 | case .failContinuation(let checkedContinuation, let error): 190 | checkedContinuation.resume(throwing: error) 191 | case .none: 192 | break 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/ConnectionPoolSizeConfigValueIsRespectedTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Atomics 17 | import Logging 18 | import NIOConcurrencyHelpers 19 | import NIOCore 20 | import NIOFoundationCompat 21 | import NIOHTTP1 22 | import NIOHTTPCompression 23 | import NIOPosix 24 | import NIOSSL 25 | import NIOTestUtils 26 | import NIOTransportServices 27 | import XCTest 28 | 29 | #if canImport(Network) 30 | import Network 31 | #endif 32 | 33 | final class ConnectionPoolSizeConfigValueIsRespectedTests: XCTestCaseHTTPClientTestsBaseClass { 34 | func testConnectionPoolSizeConfigValueIsRespected() { 35 | let numberOfRequestsPerThread = 1000 36 | let numberOfParallelWorkers = 16 37 | let poolSize = 12 38 | 39 | let httpBin = HTTPBin() 40 | defer { XCTAssertNoThrow(try httpBin.shutdown()) } 41 | 42 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 4) 43 | defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) } 44 | 45 | let configuration = HTTPClient.Configuration( 46 | connectionPool: .init( 47 | idleTimeout: .seconds(30), 48 | concurrentHTTP1ConnectionsPerHostSoftLimit: poolSize 49 | ) 50 | ) 51 | let client = HTTPClient(eventLoopGroupProvider: .shared(group), configuration: configuration) 52 | defer { XCTAssertNoThrow(try client.syncShutdown()) } 53 | 54 | let g = DispatchGroup() 55 | for workerID in 0.. Void = { _ in }) throws { 25 | let part = try self.readOutbound(as: HTTPClientRequestPart.self) 26 | switch part { 27 | case .head(let head): 28 | try verify(head) 29 | case .body, .end: 30 | throw HTTP1EmbeddedChannelError(reason: "Expected .head but got '\(part!)'") 31 | case .none: 32 | throw HTTP1EmbeddedChannelError(reason: "Nothing in buffer") 33 | } 34 | } 35 | 36 | public func receiveBodyAndVerify(_ verify: (IOData) throws -> Void = { _ in }) throws { 37 | let part = try self.readOutbound(as: HTTPClientRequestPart.self) 38 | switch part { 39 | case .body(let iodata): 40 | try verify(iodata) 41 | case .head, .end: 42 | throw HTTP1EmbeddedChannelError(reason: "Expected .head but got '\(part!)'") 43 | case .none: 44 | throw HTTP1EmbeddedChannelError(reason: "Nothing in buffer") 45 | } 46 | } 47 | 48 | public func receiveEnd() throws { 49 | let part = try self.readOutbound(as: HTTPClientRequestPart.self) 50 | switch part { 51 | case .end: 52 | break 53 | case .head, .body: 54 | throw HTTP1EmbeddedChannelError(reason: "Expected .head but got '\(part!)'") 55 | case .none: 56 | throw HTTP1EmbeddedChannelError(reason: "Nothing in buffer") 57 | } 58 | } 59 | } 60 | 61 | struct HTTP1TestTools { 62 | let connection: HTTP1Connection.SendableView 63 | let connectionDelegate: MockConnectionDelegate 64 | let readEventHandler: ReadEventHitHandler 65 | let logger: Logger 66 | } 67 | 68 | extension EmbeddedChannel { 69 | func setupHTTP1Connection() throws -> HTTP1TestTools { 70 | let logger = Logger(label: "test") 71 | let readEventHandler = ReadEventHitHandler() 72 | 73 | try self.pipeline.syncOperations.addHandler(readEventHandler) 74 | try self.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait() 75 | 76 | let connectionDelegate = MockConnectionDelegate() 77 | let connection = try HTTP1Connection.start( 78 | channel: self, 79 | connectionID: 1, 80 | delegate: connectionDelegate, 81 | decompression: .disabled, 82 | logger: logger 83 | ) 84 | 85 | // remove HTTP client encoder and decoder 86 | 87 | let decoder = try self.pipeline.syncOperations.handler(type: ByteToMessageHandler.self) 88 | let encoder = try self.pipeline.syncOperations.handler(type: HTTPRequestEncoder.self) 89 | 90 | let removeDecoderFuture = self.pipeline.syncOperations.removeHandler(decoder) 91 | let removeEncoderFuture = self.pipeline.syncOperations.removeHandler(encoder) 92 | 93 | self.embeddedEventLoop.run() 94 | 95 | try removeDecoderFuture.wait() 96 | try removeEncoderFuture.wait() 97 | 98 | return .init( 99 | connection: connection.sendableView, 100 | connectionDelegate: connectionDelegate, 101 | readEventHandler: readEventHandler, 102 | logger: logger 103 | ) 104 | } 105 | } 106 | 107 | public struct HTTP1EmbeddedChannelError: Error, Hashable, CustomStringConvertible { 108 | public var reason: String 109 | 110 | public init(reason: String) { 111 | self.reason = reason 112 | } 113 | 114 | public var description: String { 115 | self.reason 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/HTTPClient+StructuredConcurrencyTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2025 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import NIO 17 | import NIOFoundationCompat 18 | import NIOHTTP1 19 | import XCTest 20 | 21 | final class HTTPClientStructuredConcurrencyTests: XCTestCase { 22 | func testDoNothingWorks() async throws { 23 | let actual = try await HTTPClient.withHTTPClient { httpClient in 24 | "OK" 25 | } 26 | XCTAssertEqual("OK", actual) 27 | } 28 | 29 | func testShuttingDownTheClientInBodyLeadsToError() async { 30 | do { 31 | let actual = try await HTTPClient.withHTTPClient { httpClient in 32 | try await httpClient.shutdown() 33 | return "OK" 34 | } 35 | XCTFail("Expected error, got \(actual)") 36 | } catch let error as HTTPClientError where error == .alreadyShutdown { 37 | // OK 38 | } catch { 39 | XCTFail("unexpected error: \(error)") 40 | } 41 | } 42 | 43 | func testBasicRequest() async throws { 44 | let httpBin = HTTPBin() 45 | defer { XCTAssertNoThrow(try httpBin.shutdown()) } 46 | 47 | let actualBytes = try await HTTPClient.withHTTPClient { httpClient in 48 | let response = try await httpClient.get(url: httpBin.baseURL).get() 49 | XCTAssertEqual(response.status, .ok) 50 | return response.body ?? ByteBuffer(string: "n/a") 51 | } 52 | let actual = try JSONDecoder().decode(RequestInfo.self, from: actualBytes) 53 | 54 | XCTAssertGreaterThanOrEqual(actual.requestNumber, 0) 55 | XCTAssertGreaterThanOrEqual(actual.connectionNumber, 0) 56 | } 57 | 58 | func testClientIsShutDownAfterReturn() async throws { 59 | let leakedClient = try await HTTPClient.withHTTPClient { httpClient in 60 | httpClient 61 | } 62 | do { 63 | try await leakedClient.shutdown() 64 | XCTFail("unexpected, shutdown should have failed") 65 | } catch let error as HTTPClientError where error == .alreadyShutdown { 66 | // OK 67 | } catch { 68 | XCTFail("unexpected error: \(error)") 69 | } 70 | } 71 | 72 | func testClientIsShutDownOnThrowAlso() async throws { 73 | struct TestError: Error { 74 | var httpClient: HTTPClient 75 | } 76 | 77 | let leakedClient: HTTPClient 78 | do { 79 | try await HTTPClient.withHTTPClient { httpClient in 80 | throw TestError(httpClient: httpClient) 81 | } 82 | XCTFail("unexpected, shutdown should have failed") 83 | return 84 | } catch let error as TestError { 85 | // OK 86 | leakedClient = error.httpClient 87 | } catch { 88 | XCTFail("unexpected error: \(error)") 89 | return 90 | } 91 | 92 | do { 93 | try await leakedClient.shutdown() 94 | XCTFail("unexpected, shutdown should have failed") 95 | } catch let error as HTTPClientError where error == .alreadyShutdown { 96 | // OK 97 | } catch { 98 | XCTFail("unexpected error: \(error)") 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/HTTPClientBase.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Atomics 17 | import Logging 18 | import NIOConcurrencyHelpers 19 | import NIOCore 20 | import NIOFoundationCompat 21 | import NIOHTTP1 22 | import NIOHTTPCompression 23 | import NIOPosix 24 | import NIOSSL 25 | import NIOTestUtils 26 | import NIOTransportServices 27 | import XCTest 28 | 29 | #if canImport(Network) 30 | import Network 31 | #endif 32 | 33 | class XCTestCaseHTTPClientTestsBaseClass: XCTestCase { 34 | typealias Request = HTTPClient.Request 35 | 36 | var clientGroup: EventLoopGroup! 37 | var serverGroup: EventLoopGroup! 38 | var defaultHTTPBin: HTTPBin! 39 | var defaultClient: HTTPClient! 40 | var backgroundLogStore: CollectEverythingLogHandler.LogStore! 41 | 42 | var defaultHTTPBinURLPrefix: String { 43 | self.defaultHTTPBin.baseURL 44 | } 45 | 46 | override func setUp() { 47 | XCTAssertNil(self.clientGroup) 48 | XCTAssertNil(self.serverGroup) 49 | XCTAssertNil(self.defaultHTTPBin) 50 | XCTAssertNil(self.defaultClient) 51 | XCTAssertNil(self.backgroundLogStore) 52 | 53 | self.clientGroup = getDefaultEventLoopGroup(numberOfThreads: 1) 54 | self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 55 | self.defaultHTTPBin = HTTPBin() 56 | self.backgroundLogStore = CollectEverythingLogHandler.LogStore() 57 | var backgroundLogger = Logger( 58 | label: "\(#function)", 59 | factory: { _ in 60 | CollectEverythingLogHandler(logStore: self.backgroundLogStore!) 61 | } 62 | ) 63 | backgroundLogger.logLevel = .trace 64 | self.defaultClient = HTTPClient( 65 | eventLoopGroupProvider: .shared(self.clientGroup), 66 | configuration: HTTPClient.Configuration().enableFastFailureModeForTesting(), 67 | backgroundActivityLogger: backgroundLogger 68 | ) 69 | } 70 | 71 | override func tearDown() { 72 | if let defaultClient = self.defaultClient { 73 | XCTAssertNoThrow(try defaultClient.syncShutdown()) 74 | self.defaultClient = nil 75 | } 76 | 77 | XCTAssertNotNil(self.defaultHTTPBin) 78 | XCTAssertNoThrow(try self.defaultHTTPBin.shutdown()) 79 | self.defaultHTTPBin = nil 80 | 81 | XCTAssertNotNil(self.clientGroup) 82 | XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully()) 83 | self.clientGroup = nil 84 | 85 | XCTAssertNotNil(self.serverGroup) 86 | XCTAssertNoThrow(try self.serverGroup.syncShutdownGracefully()) 87 | self.serverGroup = nil 88 | 89 | XCTAssertNotNil(self.backgroundLogStore) 90 | self.backgroundLogStore = nil 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/HTTPClientInformationalResponsesTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Logging 17 | import NIOCore 18 | import NIOHTTP1 19 | import XCTest 20 | 21 | final class HTTPClientReproTests: XCTestCase { 22 | func testServerSends100ContinueFirst() { 23 | final class HTTPInformationalResponseHandler: ChannelInboundHandler { 24 | typealias InboundIn = HTTPServerRequestPart 25 | typealias OutboundOut = HTTPServerResponsePart 26 | 27 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 28 | switch self.unwrapInboundIn(data) { 29 | case .head: 30 | context.writeAndFlush( 31 | self.wrapOutboundOut(.head(.init(version: .http1_1, status: .continue))), 32 | promise: nil 33 | ) 34 | case .body: 35 | break 36 | case .end: 37 | context.write(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok))), promise: nil) 38 | context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) 39 | } 40 | } 41 | } 42 | 43 | let client = HTTPClient(eventLoopGroupProvider: .singleton) 44 | defer { XCTAssertNoThrow(try client.syncShutdown()) } 45 | 46 | let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in 47 | HTTPInformationalResponseHandler() 48 | } 49 | 50 | let body = #"{"foo": "bar"}"# 51 | 52 | var maybeRequest: HTTPClient.Request? 53 | XCTAssertNoThrow( 54 | maybeRequest = try HTTPClient.Request( 55 | url: "http://localhost:\(httpBin.port)/", 56 | method: .POST, 57 | headers: [ 58 | "Content-Type": "application/json" 59 | ], 60 | body: .string(body) 61 | ) 62 | ) 63 | guard let request = maybeRequest else { return XCTFail("Expected to have a request here") } 64 | 65 | var logger = Logger(label: "test") 66 | logger.logLevel = .trace 67 | 68 | var response: HTTPClient.Response? 69 | XCTAssertNoThrow(response = try client.execute(request: request, logger: logger).wait()) 70 | XCTAssertEqual(response?.status, .ok) 71 | } 72 | 73 | func testServerSendsSwitchingProtocols() { 74 | final class HTTPInformationalResponseHandler: ChannelInboundHandler { 75 | typealias InboundIn = HTTPServerRequestPart 76 | typealias OutboundOut = HTTPServerResponsePart 77 | 78 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 79 | switch self.unwrapInboundIn(data) { 80 | case .head: 81 | let head = HTTPResponseHead( 82 | version: .http1_1, 83 | status: .switchingProtocols, 84 | headers: [ 85 | "Connection": "Upgrade", 86 | "Upgrade": "Websocket", 87 | ] 88 | ) 89 | let body = context.channel.allocator.buffer(string: "foo bar") 90 | 91 | context.write(self.wrapOutboundOut(.head(head)), promise: nil) 92 | context.write(self.wrapOutboundOut(.body(.byteBuffer(body))), promise: nil) 93 | // we purposefully don't send an `.end` here. 94 | context.flush() 95 | case .body: 96 | break 97 | case .end: 98 | break 99 | } 100 | } 101 | } 102 | 103 | let client = HTTPClient(eventLoopGroupProvider: .singleton) 104 | defer { XCTAssertNoThrow(try client.syncShutdown()) } 105 | 106 | let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in 107 | HTTPInformationalResponseHandler() 108 | } 109 | 110 | let body = #"{"foo": "bar"}"# 111 | 112 | var maybeRequest: HTTPClient.Request? 113 | XCTAssertNoThrow( 114 | maybeRequest = try HTTPClient.Request( 115 | url: "http://localhost:\(httpBin.port)/", 116 | method: .POST, 117 | headers: [ 118 | "Content-Type": "application/json" 119 | ], 120 | body: .string(body) 121 | ) 122 | ) 123 | guard let request = maybeRequest else { return XCTFail("Expected to have a request here") } 124 | 125 | var logger = Logger(label: "test") 126 | logger.logLevel = .trace 127 | 128 | var response: HTTPClient.Response? 129 | XCTAssertNoThrow(response = try client.execute(request: request, logger: logger).wait()) 130 | XCTAssertEqual(response?.status, .switchingProtocols) 131 | XCTAssertNil(response?.body) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/HTTPClientResponseTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | import NIOCore 17 | import NIOHTTP1 18 | import XCTest 19 | 20 | @testable import AsyncHTTPClient 21 | 22 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 23 | final class HTTPClientResponseTests: XCTestCase { 24 | func testSimpleResponse() { 25 | let response = HTTPClientResponse.expectedContentLength( 26 | requestMethod: .GET, 27 | headers: ["content-length": "1025"], 28 | status: .ok 29 | ) 30 | XCTAssertEqual(response, 1025) 31 | } 32 | 33 | func testSimpleResponseNotModified() { 34 | let response = HTTPClientResponse.expectedContentLength( 35 | requestMethod: .GET, 36 | headers: ["content-length": "1025"], 37 | status: .notModified 38 | ) 39 | XCTAssertEqual(response, 0) 40 | } 41 | 42 | func testSimpleResponseHeadRequestMethod() { 43 | let response = HTTPClientResponse.expectedContentLength( 44 | requestMethod: .HEAD, 45 | headers: ["content-length": "1025"], 46 | status: .ok 47 | ) 48 | XCTAssertEqual(response, 0) 49 | } 50 | 51 | func testResponseNoContentLengthHeader() { 52 | let response = HTTPClientResponse.expectedContentLength(requestMethod: .GET, headers: [:], status: .ok) 53 | XCTAssertEqual(response, nil) 54 | } 55 | 56 | func testResponseInvalidInteger() { 57 | let response = HTTPClientResponse.expectedContentLength( 58 | requestMethod: .GET, 59 | headers: ["content-length": "none"], 60 | status: .ok 61 | ) 62 | XCTAssertEqual(response, nil) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/HTTPConnectionPool+ManagerTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient 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 | import XCTest 20 | 21 | @testable import AsyncHTTPClient 22 | 23 | class HTTPConnectionPool_ManagerTests: XCTestCase { 24 | func testManagerHappyPath() { 25 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 4) 26 | defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } 27 | 28 | let httpBin1 = HTTPBin() 29 | defer { XCTAssertNoThrow(try httpBin1.shutdown()) } 30 | 31 | let httpBin2 = HTTPBin() 32 | defer { XCTAssertNoThrow(try httpBin2.shutdown()) } 33 | 34 | let server = [httpBin1, httpBin2] 35 | 36 | let poolManager = HTTPConnectionPool.Manager( 37 | eventLoopGroup: eventLoopGroup, 38 | configuration: .init(), 39 | backgroundActivityLogger: .init(label: "test") 40 | ) 41 | 42 | defer { 43 | let promise = eventLoopGroup.next().makePromise(of: Bool.self) 44 | poolManager.shutdown(promise: promise) 45 | XCTAssertNoThrow(try promise.futureResult.wait()) 46 | } 47 | 48 | for i in 0..<9 { 49 | let httpBin = server[i % 2] 50 | 51 | var maybeRequest: HTTPClient.Request? 52 | var maybeRequestBag: RequestBag? 53 | XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)")) 54 | XCTAssertNoThrow( 55 | maybeRequestBag = try RequestBag( 56 | request: XCTUnwrap(maybeRequest), 57 | eventLoopPreference: .indifferent, 58 | task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")), 59 | redirectHandler: nil, 60 | connectionDeadline: .now() + .seconds(5), 61 | requestOptions: .forTests(), 62 | delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest)) 63 | ) 64 | ) 65 | 66 | guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") } 67 | 68 | poolManager.executeRequest(requestBag) 69 | 70 | XCTAssertNoThrow(try requestBag.task.futureResult.wait()) 71 | XCTAssertEqual(httpBin.activeConnections, 1) 72 | } 73 | } 74 | 75 | func testShutdownManagerThatHasSeenNoConnections() { 76 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 77 | defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } 78 | 79 | let poolManager = HTTPConnectionPool.Manager( 80 | eventLoopGroup: eventLoopGroup, 81 | configuration: .init(), 82 | backgroundActivityLogger: .init(label: "test") 83 | ) 84 | 85 | let eventLoop = eventLoopGroup.next() 86 | let promise = eventLoop.makePromise(of: Bool.self) 87 | poolManager.shutdown(promise: promise) 88 | XCTAssertFalse(try promise.futureResult.wait()) 89 | } 90 | 91 | func testExecutingARequestOnAShutdownPoolManager() { 92 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 93 | defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } 94 | 95 | let httpBin = HTTPBin() 96 | defer { XCTAssertNoThrow(try httpBin.shutdown()) } 97 | 98 | let poolManager = HTTPConnectionPool.Manager( 99 | eventLoopGroup: eventLoopGroup, 100 | configuration: .init(), 101 | backgroundActivityLogger: .init(label: "test") 102 | ) 103 | 104 | let eventLoop = eventLoopGroup.next() 105 | let promise = eventLoop.makePromise(of: Bool.self) 106 | poolManager.shutdown(promise: promise) 107 | XCTAssertFalse(try promise.futureResult.wait()) 108 | 109 | var maybeRequest: HTTPClient.Request? 110 | var maybeRequestBag: RequestBag? 111 | XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)")) 112 | XCTAssertNoThrow( 113 | maybeRequestBag = try RequestBag( 114 | request: XCTUnwrap(maybeRequest), 115 | eventLoopPreference: .indifferent, 116 | task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")), 117 | redirectHandler: nil, 118 | connectionDeadline: .now() + .seconds(5), 119 | requestOptions: .forTests(), 120 | delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest)) 121 | ) 122 | ) 123 | 124 | guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") } 125 | 126 | poolManager.executeRequest(requestBag) 127 | 128 | XCTAssertThrowsError(try requestBag.task.futureResult.wait()) { 129 | XCTAssertEqual($0 as? HTTPClientError, .alreadyShutdown) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/HTTPConnectionPool+RequestQueueTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | import NIOCore 17 | import NIOEmbedded 18 | import NIOHTTP1 19 | import NIOSSL 20 | import XCTest 21 | 22 | @testable import AsyncHTTPClient 23 | 24 | class HTTPConnectionPool_RequestQueueTests: XCTestCase { 25 | func testCountAndIsEmptyWorks() { 26 | var queue = HTTPConnectionPool.RequestQueue() 27 | XCTAssertTrue(queue.isEmpty) 28 | XCTAssertEqual(queue.count, 0) 29 | let req1 = MockScheduledRequest(requiredEventLoop: nil) 30 | let req1ID = queue.push(.init(req1)) 31 | XCTAssertFalse(queue.isEmpty) 32 | XCTAssertFalse(queue.isEmpty(for: nil)) 33 | XCTAssertEqual(queue.count, 1) 34 | XCTAssertEqual(queue.generalPurposeCount, 1) 35 | 36 | let req2 = MockScheduledRequest(requiredEventLoop: nil) 37 | let req2ID = queue.push(.init(req2)) 38 | XCTAssertEqual(queue.count, 2) 39 | 40 | XCTAssert(queue.popFirst()?.__testOnly_wrapped_request() === req1) 41 | XCTAssertEqual(queue.count, 1) 42 | XCTAssertFalse(queue.isEmpty) 43 | XCTAssert(queue.remove(req2ID)?.__testOnly_wrapped_request() === req2) 44 | XCTAssertNil(queue.remove(req1ID)) 45 | XCTAssertEqual(queue.count, 0) 46 | XCTAssertTrue(queue.isEmpty) 47 | 48 | let eventLoop = EmbeddedEventLoop() 49 | 50 | XCTAssertTrue(queue.isEmpty(for: eventLoop)) 51 | XCTAssertEqual(queue.count(for: eventLoop), 0) 52 | let req3 = MockScheduledRequest(requiredEventLoop: eventLoop) 53 | let req3ID = queue.push(.init(req3)) 54 | XCTAssertFalse(queue.isEmpty(for: eventLoop)) 55 | XCTAssertEqual(queue.count(for: eventLoop), 1) 56 | XCTAssertFalse(queue.isEmpty) 57 | XCTAssertEqual(queue.count, 1) 58 | XCTAssert(queue.popFirst(for: eventLoop)?.__testOnly_wrapped_request() === req3) 59 | XCTAssertNil(queue.remove(req3ID)) 60 | XCTAssertTrue(queue.isEmpty(for: eventLoop)) 61 | XCTAssertEqual(queue.count(for: eventLoop), 0) 62 | XCTAssertTrue(queue.isEmpty) 63 | XCTAssertEqual(queue.count, 0) 64 | 65 | let req4 = MockScheduledRequest(requiredEventLoop: eventLoop) 66 | let req4ID = queue.push(.init(req4)) 67 | XCTAssert(queue.remove(req4ID)?.__testOnly_wrapped_request() === req4) 68 | 69 | let req5 = MockScheduledRequest(requiredEventLoop: nil) 70 | queue.push(.init(req5)) 71 | let req6 = MockScheduledRequest(requiredEventLoop: eventLoop) 72 | queue.push(.init(req6)) 73 | let all = queue.removeAll() 74 | let testSet = all.map { $0.__testOnly_wrapped_request() } 75 | XCTAssertEqual(testSet.count, 2) 76 | XCTAssertTrue(testSet.contains(where: { $0 === req5 })) 77 | XCTAssertTrue(testSet.contains(where: { $0 === req6 })) 78 | XCTAssertFalse(testSet.contains(where: { $0 === req4 })) 79 | XCTAssertTrue(queue.isEmpty(for: eventLoop)) 80 | XCTAssertEqual(queue.count(for: eventLoop), 0) 81 | XCTAssertTrue(queue.isEmpty) 82 | XCTAssertEqual(queue.count, 0) 83 | } 84 | } 85 | 86 | final private class MockScheduledRequest: HTTPSchedulableRequest { 87 | let requiredEventLoop: EventLoop? 88 | 89 | init(requiredEventLoop: EventLoop?) { 90 | self.requiredEventLoop = requiredEventLoop 91 | } 92 | 93 | var poolKey: ConnectionPool.Key { preconditionFailure("Unimplemented") } 94 | var tlsConfiguration: TLSConfiguration? { nil } 95 | var logger: Logger { preconditionFailure("Unimplemented") } 96 | var connectionDeadline: NIODeadline { preconditionFailure("Unimplemented") } 97 | var preferredEventLoop: EventLoop { preconditionFailure("Unimplemented") } 98 | 99 | func requestWasQueued(_: HTTPRequestScheduler) { 100 | preconditionFailure("Unimplemented") 101 | } 102 | 103 | func fail(_: Error) { 104 | preconditionFailure("Unimplemented") 105 | } 106 | 107 | // MARK: HTTPExecutableRequest 108 | 109 | var requestHead: HTTPRequestHead { preconditionFailure("Unimplemented") } 110 | var requestFramingMetadata: RequestFramingMetadata { preconditionFailure("Unimplemented") } 111 | var requestOptions: RequestOptions { preconditionFailure("Unimplemented") } 112 | 113 | func willExecuteRequest(_: HTTPRequestExecutor) { 114 | preconditionFailure("Unimplemented") 115 | } 116 | 117 | func requestHeadSent() { 118 | preconditionFailure("Unimplemented") 119 | } 120 | 121 | func resumeRequestBodyStream() { 122 | preconditionFailure("Unimplemented") 123 | } 124 | 125 | func pauseRequestBodyStream() { 126 | preconditionFailure("Unimplemented") 127 | } 128 | 129 | func receiveResponseHead(_: HTTPResponseHead) { 130 | preconditionFailure("Unimplemented") 131 | } 132 | 133 | func receiveResponseBodyParts(_: CircularBuffer) { 134 | preconditionFailure("Unimplemented") 135 | } 136 | 137 | func succeedRequest(_: CircularBuffer?) { 138 | preconditionFailure("Unimplemented") 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/IdleTimeoutNoReuseTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Atomics 17 | import Logging 18 | import NIOConcurrencyHelpers 19 | import NIOCore 20 | import NIOFoundationCompat 21 | import NIOHTTP1 22 | import NIOHTTPCompression 23 | import NIOPosix 24 | import NIOSSL 25 | import NIOTestUtils 26 | import NIOTransportServices 27 | import XCTest 28 | 29 | #if canImport(Network) 30 | import Network 31 | #endif 32 | 33 | final class TestIdleTimeoutNoReuse: XCTestCaseHTTPClientTestsBaseClass { 34 | func testIdleTimeoutNoReuse() throws { 35 | var req = try HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "get", method: .GET) 36 | XCTAssertNoThrow(try self.defaultClient.execute(request: req, deadline: .now() + .seconds(2)).wait()) 37 | req.headers.add(name: "X-internal-delay", value: "2500") 38 | try self.defaultClient.eventLoopGroup.next().scheduleTask(in: .milliseconds(250)) {}.futureResult.wait() 39 | XCTAssertNoThrow(try self.defaultClient.execute(request: req).timeout(after: .seconds(10)).wait()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/LRUCacheTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | 17 | @testable import AsyncHTTPClient 18 | 19 | class LRUCacheTests: XCTestCase { 20 | func testBasicsWork() { 21 | var cache = LRUCache(capacity: 1) 22 | var requestedValueGens = 0 23 | for i in 0..<10 { 24 | let actual = cache.findOrAppend(key: i) { i in 25 | requestedValueGens += 1 26 | return i 27 | } 28 | XCTAssertEqual(i, actual) 29 | } 30 | XCTAssertEqual(10, requestedValueGens) 31 | 32 | let nine = cache.findOrAppend(key: 9) { i in 33 | XCTAssertEqual(9, i) 34 | XCTFail("9 should be in the cache") 35 | return -1 36 | } 37 | XCTAssertEqual(9, nine) 38 | } 39 | 40 | func testCachesTheRightThings() { 41 | var cache = LRUCache(capacity: 3) 42 | 43 | for i in 0..<10 { 44 | let actual = cache.findOrAppend(key: i) { i in 45 | i 46 | } 47 | XCTAssertEqual(i, actual) 48 | 49 | let zero = cache.find(key: 0) 50 | XCTAssertEqual(0, zero, "at \(i), couldn't find 0") 51 | 52 | cache.append(key: -1, value: -1) 53 | XCTAssertEqual(-1, cache.find(key: -1)) 54 | } 55 | 56 | XCTAssertEqual(0, cache.find(key: 0)) 57 | XCTAssertEqual(9, cache.find(key: 9)) 58 | 59 | for i in 1..<9 { 60 | XCTAssertNil(cache.find(key: i)) 61 | } 62 | } 63 | 64 | func testAppendingTheSameDoesNotEvictButUpdates() { 65 | var cache = LRUCache(capacity: 3) 66 | 67 | cache.append(key: 1, value: 1) 68 | cache.append(key: 3, value: 3) 69 | for i in (2...100).reversed() { 70 | cache.append(key: 2, value: i) 71 | XCTAssertEqual(i, cache.find(key: 2)) 72 | } 73 | 74 | for i in 1...3 { 75 | XCTAssertEqual(i, cache.find(key: i)) 76 | } 77 | 78 | cache.append(key: 4, value: 4) 79 | XCTAssertNil(cache.find(key: 1)) 80 | for i in 2...4 { 81 | XCTAssertEqual(i, cache.find(key: i)) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/Mocks/MockRequestQueuer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Logging 16 | import NIOCore 17 | import NIOHTTP1 18 | 19 | @testable import AsyncHTTPClient 20 | 21 | /// A mock request queue (not creating any timers) that is used to validate 22 | /// request actions returned by the `HTTPConnectionPool.StateMachine`. 23 | struct MockRequestQueuer { 24 | enum Errors: Error { 25 | case requestIDNotFound 26 | case requestIDAlreadyUsed 27 | case requestIDDoesNotMatchTask 28 | } 29 | 30 | typealias RequestID = HTTPConnectionPool.Request.ID 31 | 32 | private struct QueuedRequest { 33 | let id: RequestID 34 | let request: HTTPSchedulableRequest 35 | } 36 | 37 | init() { 38 | self.waiters = [:] 39 | } 40 | 41 | private var waiters: [RequestID: QueuedRequest] 42 | 43 | var count: Int { 44 | self.waiters.count 45 | } 46 | 47 | var isEmpty: Bool { 48 | self.waiters.isEmpty 49 | } 50 | 51 | mutating func queue(_ request: HTTPSchedulableRequest, id: RequestID) throws { 52 | guard self.waiters[id] == nil else { 53 | throw Errors.requestIDAlreadyUsed 54 | } 55 | 56 | self.waiters[id] = QueuedRequest(id: id, request: request) 57 | } 58 | 59 | mutating func fail(_ id: RequestID, request: HTTPSchedulableRequest) throws { 60 | guard let waiter = self.waiters.removeValue(forKey: id) else { 61 | throw Errors.requestIDNotFound 62 | } 63 | guard waiter.request === request else { 64 | throw Errors.requestIDDoesNotMatchTask 65 | } 66 | } 67 | 68 | mutating func get(_ id: RequestID, request: HTTPSchedulableRequest) throws -> HTTPSchedulableRequest { 69 | guard let waiter = self.waiters.removeValue(forKey: id) else { 70 | throw Errors.requestIDNotFound 71 | } 72 | guard waiter.request === request else { 73 | throw Errors.requestIDDoesNotMatchTask 74 | } 75 | return waiter.request 76 | } 77 | 78 | @discardableResult 79 | mutating func cancel(_ id: RequestID) throws -> HTTPSchedulableRequest { 80 | guard let waiter = self.waiters.removeValue(forKey: id) else { 81 | throw Errors.requestIDNotFound 82 | } 83 | return waiter.request 84 | } 85 | 86 | mutating func timeoutRandomRequest() -> (RequestID, HTTPSchedulableRequest)? { 87 | guard let waiter = self.waiters.randomElement() else { 88 | return nil 89 | } 90 | self.waiters.removeValue(forKey: waiter.key) 91 | return (waiter.key, waiter.value.request) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/NWWaitingHandlerTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if canImport(Network) 16 | @testable import AsyncHTTPClient 17 | import Network 18 | import NIOCore 19 | import NIOConcurrencyHelpers 20 | import NIOEmbedded 21 | import NIOSSL 22 | import NIOTransportServices 23 | import XCTest 24 | 25 | @available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *) 26 | class NWWaitingHandlerTests: XCTestCase { 27 | final class MockRequester: HTTPConnectionRequester { 28 | private struct State: Sendable { 29 | var waitingForConnectivityCalled = false 30 | var connectionID: AsyncHTTPClient.HTTPConnectionPool.Connection.ID? 31 | var transientError: NWError? 32 | } 33 | 34 | private let state = NIOLockedValueBox(State()) 35 | 36 | var waitingForConnectivityCalled: Bool { 37 | self.state.withLockedValue { $0.waitingForConnectivityCalled } 38 | } 39 | 40 | var connectionID: AsyncHTTPClient.HTTPConnectionPool.Connection.ID? { 41 | self.state.withLockedValue { $0.connectionID } 42 | } 43 | 44 | var transientError: NWError? { 45 | self.state.withLockedValue { 46 | $0.transientError 47 | } 48 | } 49 | 50 | func http1ConnectionCreated(_: AsyncHTTPClient.HTTP1Connection.SendableView) {} 51 | 52 | func http2ConnectionCreated(_: AsyncHTTPClient.HTTP2Connection.SendableView, maximumStreams: Int) {} 53 | 54 | func failedToCreateHTTPConnection(_: AsyncHTTPClient.HTTPConnectionPool.Connection.ID, error: Error) {} 55 | 56 | func waitingForConnectivity(_ connectionID: AsyncHTTPClient.HTTPConnectionPool.Connection.ID, error: Error) { 57 | self.state.withLockedValue { 58 | $0.waitingForConnectivityCalled = true 59 | $0.connectionID = connectionID 60 | $0.transientError = error as? NWError 61 | } 62 | } 63 | } 64 | 65 | func testWaitingHandlerInvokesWaitingForConnectivity() { 66 | let requester = MockRequester() 67 | let connectionID: AsyncHTTPClient.HTTPConnectionPool.Connection.ID = 1 68 | let waitingEventHandler = NWWaitingHandler(requester: requester, connectionID: connectionID) 69 | let embedded = EmbeddedChannel(handlers: [waitingEventHandler]) 70 | 71 | embedded.pipeline.fireUserInboundEventTriggered( 72 | NIOTSNetworkEvents.WaitingForConnectivity(transientError: .dns(1)) 73 | ) 74 | 75 | XCTAssertTrue( 76 | requester.waitingForConnectivityCalled, 77 | "Expected the handler to invoke .waitingForConnectivity on the requester" 78 | ) 79 | XCTAssertEqual(requester.connectionID, connectionID, "Expected the handler to pass connectionID to requester") 80 | XCTAssertEqual(requester.transientError, NWError.dns(1)) 81 | } 82 | 83 | func testWaitingHandlerDoesNotInvokeWaitingForConnectionOnUnrelatedErrors() { 84 | let requester = MockRequester() 85 | let waitingEventHandler = NWWaitingHandler(requester: requester, connectionID: 1) 86 | let embedded = EmbeddedChannel(handlers: [waitingEventHandler]) 87 | embedded.pipeline.fireUserInboundEventTriggered(NIOTSNetworkEvents.BetterPathAvailable()) 88 | 89 | XCTAssertFalse( 90 | requester.waitingForConnectivityCalled, 91 | "Should not call .waitingForConnectivity on unrelated events" 92 | ) 93 | } 94 | 95 | func testWaitingHandlerPassesTheEventDownTheContext() { 96 | let requester = MockRequester() 97 | let waitingEventHandler = NWWaitingHandler(requester: requester, connectionID: 1) 98 | let tlsEventsHandler = TLSEventsHandler(deadline: nil) 99 | let embedded = EmbeddedChannel(handlers: [waitingEventHandler, tlsEventsHandler]) 100 | 101 | embedded.pipeline.fireErrorCaught(NIOSSLError.handshakeFailed(BoringSSLError.wantConnect)) 102 | XCTAssertThrowsError(try XCTUnwrap(tlsEventsHandler.tlsEstablishedFuture).wait()) { 103 | XCTAssertEqualTypeAndValue($0, NIOSSLError.handshakeFailed(BoringSSLError.wantConnect)) 104 | } 105 | } 106 | } 107 | 108 | #endif 109 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/NoBytesSentOverBodyLimitTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Atomics 17 | import Logging 18 | import NIOConcurrencyHelpers 19 | import NIOCore 20 | import NIOFoundationCompat 21 | import NIOHTTP1 22 | import NIOHTTPCompression 23 | import NIOPosix 24 | import NIOSSL 25 | import NIOTestUtils 26 | import NIOTransportServices 27 | import XCTest 28 | 29 | #if canImport(Network) 30 | import Network 31 | #endif 32 | 33 | final class NoBytesSentOverBodyLimitTests: XCTestCaseHTTPClientTestsBaseClass { 34 | func testNoBytesSentOverBodyLimit() throws { 35 | let server = NIOHTTP1TestServer(group: self.serverGroup) 36 | defer { 37 | XCTAssertNoThrow(try server.stop()) 38 | } 39 | 40 | let tooLong = "XBAD BAD BAD NOT HTTP/1.1\r\n\r\n" 41 | 42 | let request = try Request( 43 | url: "http://localhost:\(server.serverPort)", 44 | body: .stream(contentLength: 1) { streamWriter in 45 | streamWriter.write(.byteBuffer(ByteBuffer(string: tooLong))) 46 | } 47 | ) 48 | 49 | let future = self.defaultClient.execute(request: request) 50 | 51 | // Okay, what happens here needs an explanation: 52 | // 53 | // In the request state machine, we should start the request, which will lead to an 54 | // invocation of `context.write(HTTPRequestHead)`. Since we will receive a streamed request 55 | // body a `context.flush()` will be issued. Further the request stream will be started. 56 | // Since the request stream immediately produces to much data, the request will be failed 57 | // and the connection will be closed. 58 | // 59 | // Even though a flush was issued after the request head, there is no guarantee that the 60 | // request head was written to the network. For this reason we must accept not receiving a 61 | // request and receiving a request head. 62 | 63 | do { 64 | _ = try server.receiveHead() 65 | 66 | // A request head was sent. We expect the request now to fail with a parsing error, 67 | // since the client ended the connection to early (from the server's point of view.) 68 | XCTAssertThrowsError(try server.readInbound()) { 69 | XCTAssertEqual($0 as? HTTPParserError, HTTPParserError.invalidEOFState) 70 | } 71 | } catch { 72 | // TBD: We sadly can't verify the error type, since it is private in `NIOTestUtils`: 73 | // NIOTestUtils.BlockingQueue.TimeoutError 74 | } 75 | 76 | // request must always be failed with this error 77 | XCTAssertThrowsError(try future.wait()) { 78 | XCTAssertEqual($0 as? HTTPClientError, .bodyLengthMismatch) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/RacePoolIdleConnectionsAndGetTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Atomics 17 | import Logging 18 | import NIOConcurrencyHelpers 19 | import NIOCore 20 | import NIOFoundationCompat 21 | import NIOHTTP1 22 | import NIOHTTPCompression 23 | import NIOPosix 24 | import NIOSSL 25 | import NIOTestUtils 26 | import NIOTransportServices 27 | import XCTest 28 | 29 | #if canImport(Network) 30 | import Network 31 | #endif 32 | 33 | final class RacePoolIdleConnectionsAndGetTests: XCTestCaseHTTPClientTestsBaseClass { 34 | func testRacePoolIdleConnectionsAndGet() { 35 | let localClient = HTTPClient( 36 | eventLoopGroupProvider: .shared(self.clientGroup), 37 | configuration: .init(connectionPool: .init(idleTimeout: .milliseconds(10))) 38 | ) 39 | defer { 40 | XCTAssertNoThrow(try localClient.syncShutdown()) 41 | } 42 | for _ in 1...200 { 43 | XCTAssertNoThrow(try localClient.get(url: self.defaultHTTPBinURLPrefix + "get").wait()) 44 | Thread.sleep(forTimeInterval: 0.01 + .random(in: -0.01...0.01)) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/Resources/example.com.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBwTCCAUigAwIBAgIUX7f9BABxGdAqG5EvLpQScFt9lOkwCgYIKoZIzj0EAwMw 3 | KjEUMBIGA1UECgwLU2VsZiBTaWduZWQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0y 4 | NTA0MDExNDMwMTFaFw0yNjA0MDExNDMwMTFaMCoxFDASBgNVBAoMC1NlbGYgU2ln 5 | bmVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQW 6 | szfO5HCWIWgKUqyXUU0pFpYgaq01RRL69XZz1CkV6XTrxMfIvvwez2886EQDL8QX 7 | i5NpKg3qvPgWuDjVHaj4WEJe5XMNqcujxcTufBlmaQ6o4vtoK7CIHDIDldF/HRij 8 | LzAtMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMBMGA1UdJQQMMAoGCCsGAQUFBwMB 9 | MAoGCCqGSM49BAMDA2cAMGQCMBJ8Dxg0qX2bEZ3r6dI3UCGAUYxJDVk+XhiIY1Fm 10 | 5FJeQqhaVayCRPrPXXGZUJGY/wIwXej70FwkxHKLq+XxfHTC5CzmoOK469C9Rk9Y 11 | ucddXM83ebFxVNgRCWetH9tDdXJ9 12 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/Resources/example.com.private-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDD9v51MTOcgFIbiHbok 3 | U+QOubosGF1u1q+D3fEUb1U2cgjCofKmPHekXTz0xu9MJi2hZANiAAQWszfO5HCW 4 | IWgKUqyXUU0pFpYgaq01RRL69XZz1CkV6XTrxMfIvvwez2886EQDL8QXi5NpKg3q 5 | vPgWuDjVHaj4WEJe5XMNqcujxcTufBlmaQ6o4vtoK7CIHDIDldF/HRg= 6 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/Resources/self_signed_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEpjCCAo4CCQCeTmfiTQcJrzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls 3 | b2NhbGhvc3QwIBcNMjIwNjE0MTI1NDQ4WhgPMjI5NjAzMjgxMjU0NDhaMBQxEjAQ 4 | BgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB 5 | AK16gPDwP/Xbaf36x5BNd6yHDxCPIIJP4JLfMEuozwLE0YRqwmZOuklb4jUbAXf7 6 | u9u24ANrC4XS6VVWkfPdugokAUkaKPpwkV4GOiMCXeSDjDiLt1dYxlbp+MLV78a5 7 | oUDbCAqfFKebIgv1oiK+L6/p818eAHSBWEXXMhTeBDEQAIpJLTG88iVu6r3fMJeH 8 | FbMWuPmAajmx2AEGmwD1x6+NHZLJv1zaufa7j0sHADraagXnfKn6rkLn1is6QFu4 9 | v7xaNlEwsRCYbh0nrtCtEdJIqnEHc0GCu/gnw5GE3CuRG3FYBTZStIF7d9h+XZQB 10 | ky/YEWSGw9DXFBbebOZugopvl91qaZLqo6Wg0J8qCodgFtJHOSVMq/SAOBmKyw+b 11 | 7FYZbj4tQKpuuhwCN+gwEveTy+BK+zGY/sVzPwR8PNjpCgT/HiOBM7dNt4+2r9pY 12 | Ld/mcMvakgRzM4Iqqntem9ltuckZev0TRjdrIylVWsAlNYVXm4ncMLkbzxFkv5Gb 13 | AlhAuTwxyFkIo0M7+GS4lXCZ2bX2umJ0DTl3/NGJserFdkOhvHZSHHC9BzDBysmc 14 | SejX/cGOFQ8O3sFeJdVMGlO64dU482O0FbBcLHmTLXWR4t8dlhrzJuXZ4X6WtHqY 15 | 83RwyD1gacYRZnT0eL+Z7XGrO1/qypji1RNaFIaGUt7DAgMBAAEwDQYJKoZIhvcN 16 | AQELBQADggIBAIigOuEVirgqXoUMStTwYObs/DcNIPEugn9gAq9Lt1cr6fm7CvhG 17 | AupxoJTbKLHQX6FegvFSA+4Kt3KYXX9Qi9SJF3Vr4zOhV0q203d4Aui6Lamo5Yye 18 | nhbzzXuDSIyxpaWPFRC2RqCA6+hV8/Ar9Bx0TCI4NQxWxQEPerwqzqWCuTbViccw 19 | WzlwRD2AHibaQaCbpzXg9lOX0fRJHoSM3exYQd91pDoSoL3f/EV3I/czssq+10M8 20 | F4GhE4bQjaKD7jL5U59dlvfy73nLAzzxzsxsFuYTAgzZwDg586sdbrqqFjzjoZ9A 21 | dF8NuVYkHyFDQkpe66e1isNZi7eFdSjeVmj8llp4b6in59ik7ZS7arzGOxhZZzmv 22 | Jf3nfE4hJzMS/4GJsKMdtcI+6K+hMi6Yt9OoPh82SQ2q8gK4QSWWrwAKuQ4F4UeO 23 | pgiWBryKrkOXlGARBbsR/ZDhlqyAskeGuhIpEY5NLCByFfQ5KlcrX+n4TVLRZMvb 24 | /7PZqboGgU+CUVawm/suPAs8jOlFQOzrxWQPRfWVvFII62ABgozS8N/xZ/WbgTVj 25 | kOtWj85NpaBSCUliIY/7z1FkjpMZO8Kds45WQzAq4YChDLZGbgV0MkyXqO/LEYFJ 26 | zqGOP1yGxVcKxu6t8Xh0hL6JPFmKWiMEWVrd1wut6NAIu6WNftmWZX6J 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/Resources/self_signed_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCteoDw8D/122n9 3 | +seQTXeshw8QjyCCT+CS3zBLqM8CxNGEasJmTrpJW+I1GwF3+7vbtuADawuF0ulV 4 | VpHz3boKJAFJGij6cJFeBjojAl3kg4w4i7dXWMZW6fjC1e/GuaFA2wgKnxSnmyIL 5 | 9aIivi+v6fNfHgB0gVhF1zIU3gQxEACKSS0xvPIlbuq93zCXhxWzFrj5gGo5sdgB 6 | BpsA9cevjR2Syb9c2rn2u49LBwA62moF53yp+q5C59YrOkBbuL+8WjZRMLEQmG4d 7 | J67QrRHSSKpxB3NBgrv4J8ORhNwrkRtxWAU2UrSBe3fYfl2UAZMv2BFkhsPQ1xQW 8 | 3mzmboKKb5fdammS6qOloNCfKgqHYBbSRzklTKv0gDgZissPm+xWGW4+LUCqbroc 9 | AjfoMBL3k8vgSvsxmP7Fcz8EfDzY6QoE/x4jgTO3TbePtq/aWC3f5nDL2pIEczOC 10 | Kqp7XpvZbbnJGXr9E0Y3ayMpVVrAJTWFV5uJ3DC5G88RZL+RmwJYQLk8MchZCKND 11 | O/hkuJVwmdm19rpidA05d/zRibHqxXZDobx2UhxwvQcwwcrJnEno1/3BjhUPDt7B 12 | XiXVTBpTuuHVOPNjtBWwXCx5ky11keLfHZYa8ybl2eF+lrR6mPN0cMg9YGnGEWZ0 13 | 9Hi/me1xqztf6sqY4tUTWhSGhlLewwIDAQABAoICAApRcP3jrEo5RLKgieIhWW7f 14 | kZvQh4R4r8jMkZjOb5Gglz2jA/EF2bqnRmsWMh4q0N+envBVG5hYFRzIS2IP3BLi 15 | VVk9vxY2P88x259dcqw2zs5GMR923kUpIWylQN+3BspOvMm08IuPhJTlhUE/wqJZ 16 | 7enIZQqI7vEofYgUNHeelgmjlJaSwGxNjpTAg6lflYDTZykf5DGOTGSzOeDyvW/J 17 | muqyKTmioND2Eu3JetAFUa0MObP6fwbntytXCaDq+ix/yR9HICD2kAYX6CPtR1QU 18 | kl6qrMZGultmMhGjr1zAArvZGmZCwQ26hERSL8qv1UtRNKegBGGViVJa5GtIQ2dT 19 | UmTWmWu/5gyxKvvjuqYl8Dub2/ZT0iGAsA6hGyUr+vpgjcNEZqsYhiEiQPi0g1sM 20 | XyszytqG1F7JzXYgVzcdFA9L+eLD+i4nKD18TYTYHFGRmxwQ+HzHnetgDQ2gqRbB 21 | XwT4lp643oNLMGyL+T0cQ7i1Hpq7Ko0S2FeXzzFe9B33uXbDvc0usier5qx2tgxc 22 | zfgSqJjahfo4LCxhxvBWOup3U/sXNgyMCctr1qjpwGwLek+H1keOyv7FO9O6OgI1 23 | v5ZPFsJV7mK1fDLM/8QLDpUcUNnhPUfzsBdxKrjLfnZ8MPNczgv1GPzb4jsLvewf 24 | g6ps8oBwnZDQVa6dMuyRAoIBAQDnTKRUsTMmFo01o0k90C8SwwE2x7Wry8r6vIIf 25 | PMni3ZAS+zWFnu1zg82+83QpdvskntWM2iXS7nimmkXClCCFMDU/hYA9EsZtGIv6 26 | +xA6gYF0Xd3Qf9QrvhixOxHj3ixNyCeee3/9XUYln3ZfEx8cgCwHjPSIm3rOKI2M 27 | PFnuG9xJ513sy6YCDrCdtb661E6bmsaMcIhu6S7At0njwnoL9aB617TSds5tFEr8 28 | 74EW3D9epN01uUQ9MgZSXbzdQ82IswLps4a/k4wfDFp4qKpx7zOsoTSjA9il3fgW 29 | QLhBXxnzTYYTvwxIgaW//fyqEL3p6t9zuYcjbORcrj7v8xIvAoIBAQDAASGjsSCA 30 | hn03DXrI/atoXEC0htVwPwp4HTI0Z1/rOS0IrFBcX3CWx90Dr/clePHQGPk1yOO7 31 | oM83zumwggIOymtDhlTcCa77yN9x9AZMW3qPMF+mvAouUzItnlMrOjvfEnIWziWC 32 | UsylBiV4/I6tf0zpH8zFYPNXq98fpv+UXyJDTW+YGBc2b2BwZZA6RdtFalqvunM7 33 | M8FIH8vSYEMR0YC47L2ceBJY/U9EQpsc6vuS7+CoXOH/WRb5v1z+a5O9sHWp8Rdc 34 | Oh67B6v2feUT9TwhGUVF0L+ktW389e3N+VzPvbvICvRsOvo6+bceCJTszhNno00s 35 | 87bPyelaHXutAoIBAFtJ6onqri9YMz96RMv6wLl88Zu3UsKNWn1/rTO7AEtj+xsi 36 | vssQINO4r5mv6Kb86L5ZWhuPdeI8cK4AsYvMftFSZ5G8lRKFuH8Scx0Jviv5NSjC 37 | a2uBKDJjgsdgcv0mkQHZ/5kTUT6kc60htMxtdZgAFmCch17rTprTcppor23E3Trl 38 | 8DInZkvllFuKgc6nQKc1fSustoxfyC4TqTwVY6oYtdAGFr4CWhK/MaGGvcJSB0jJ 39 | dO1hQ8eLWOdlS8dgnVxYmsu2KXavO1x9ua9pkmwJZrG5pla4i+dbJjFSNebHLCzU 40 | 6hgdDTIIyWxvSCuvE+Wg57R7AxU+Qxs5Qmnd280CggEAex4+m+BwnvmeQTb7jPZc 41 | e0bsltX+90L1S6AtGT1QXF0Fa5JS1Wi9oXH3Xu3u5LBxHqdk5gAzR5UOSxL69pvn 42 | BeT2cw4oTBBJjFp6LW/0ufHO3RJ/w0LApIPkoSvs2MM2sQv67HSzyKWfZBJU5QfN 43 | 1aLTholFnStV3tnu8TT8nf+C0PVOoZCREe7JQElf+n3g5NoV3KkKSuQdBEqfP/9K 44 | Apr8l5f23eaAnV+Q/IxZOmnTd50pycwFft95xBvZXatNyUzlpltaR2FdY0DAHAcO 45 | ZYXTUMYLjYEV4mAUbyijnHhR80QOrW+Y2+3VlwuZSEDofhCGkOY+Dp0YlJU8dPSC 46 | 4QKCAQEA3qlwsjJT8Vv+Sx2sZySDNtey/00yjxe4TckdH8dWTC22G38/ppbazlp/ 47 | YVqFUgteoo5FI60HKa0+jLKQZpCIH6JgpL3yq81Obl0wRkrSNePRTL1Ikiff8P2j 48 | bowFpbIdJLgDco1opJpDgTOz2mB7HlHu6RyoKjiVrNA/EOoks1Uljxdth6h/5ctr 49 | rLn8dnz2sTtwxcUsOpyFcFQ2qaWJvSg+bF7JPPzMrpQfCR1qVWa43Kl8KlcWSKaq 50 | ITpglIBY+h3F2GygAAcnpfkXde381Iw89y7TFd2LxWQR98zhnbJWF2JmuuPDtVRv 51 | +HYZkcyQcpDwfC+2NOWOU7NQj+IDIA== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/ResponseDelayGetTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Atomics 17 | import Logging 18 | import NIOConcurrencyHelpers 19 | import NIOCore 20 | import NIOFoundationCompat 21 | import NIOHTTP1 22 | import NIOHTTPCompression 23 | import NIOPosix 24 | import NIOSSL 25 | import NIOTestUtils 26 | import NIOTransportServices 27 | import XCTest 28 | 29 | #if canImport(Network) 30 | import Network 31 | #endif 32 | 33 | final class ResponseDelayGetTests: XCTestCaseHTTPClientTestsBaseClass { 34 | func testResponseDelayGet() throws { 35 | let req = try HTTPClient.Request( 36 | url: self.defaultHTTPBinURLPrefix + "get", 37 | method: .GET, 38 | headers: ["X-internal-delay": "2000"], 39 | body: nil 40 | ) 41 | let start = NIODeadline.now() 42 | let response = try self.defaultClient.execute(request: req).wait() 43 | XCTAssertGreaterThanOrEqual(.now() - start, .milliseconds(1_900)) 44 | XCTAssertEqual(response.status, .ok) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/SOCKSEventsHandlerTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | import NIOEmbedded 17 | import NIOSOCKS 18 | import XCTest 19 | 20 | @testable import AsyncHTTPClient 21 | 22 | class SOCKSEventsHandlerTests: XCTestCase { 23 | func testHandlerHappyPath() { 24 | let socksEventsHandler = SOCKSEventsHandler(deadline: .now() + .seconds(10)) 25 | XCTAssertNil(socksEventsHandler.socksEstablishedFuture) 26 | let embedded = EmbeddedChannel(handlers: [socksEventsHandler]) 27 | XCTAssertNotNil(socksEventsHandler.socksEstablishedFuture) 28 | 29 | XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait()) 30 | 31 | embedded.pipeline.fireUserInboundEventTriggered(SOCKSProxyEstablishedEvent()) 32 | XCTAssertNoThrow(try XCTUnwrap(socksEventsHandler.socksEstablishedFuture).wait()) 33 | } 34 | 35 | func testHandlerFailsFutureWhenRemovedWithoutEvent() { 36 | let socksEventsHandler = SOCKSEventsHandler(deadline: .now() + .seconds(10)) 37 | XCTAssertNil(socksEventsHandler.socksEstablishedFuture) 38 | let embedded = EmbeddedChannel(handlers: [socksEventsHandler]) 39 | XCTAssertNotNil(socksEventsHandler.socksEstablishedFuture) 40 | 41 | XCTAssertNoThrow(try embedded.pipeline.syncOperations.removeHandler(socksEventsHandler).wait()) 42 | XCTAssertThrowsError(try XCTUnwrap(socksEventsHandler.socksEstablishedFuture).wait()) 43 | } 44 | 45 | func testHandlerFailsFutureWhenHandshakeFails() { 46 | let socksEventsHandler = SOCKSEventsHandler(deadline: .now() + .seconds(10)) 47 | XCTAssertNil(socksEventsHandler.socksEstablishedFuture) 48 | let embedded = EmbeddedChannel(handlers: [socksEventsHandler]) 49 | XCTAssertNotNil(socksEventsHandler.socksEstablishedFuture) 50 | 51 | let error = SOCKSError.InvalidReservedByte(actual: 19) 52 | embedded.pipeline.fireErrorCaught(error) 53 | XCTAssertThrowsError(try XCTUnwrap(socksEventsHandler.socksEstablishedFuture).wait()) { 54 | XCTAssertEqual($0 as? SOCKSError.InvalidReservedByte, error) 55 | } 56 | } 57 | 58 | func testHandlerClosesConnectionIfHandshakeTimesout() { 59 | // .uptimeNanoseconds(0) => .now() for EmbeddedEventLoops 60 | let socksEventsHandler = SOCKSEventsHandler(deadline: .uptimeNanoseconds(0) + .milliseconds(10)) 61 | XCTAssertNil(socksEventsHandler.socksEstablishedFuture) 62 | let embedded = EmbeddedChannel(handlers: [socksEventsHandler]) 63 | XCTAssertNotNil(socksEventsHandler.socksEstablishedFuture) 64 | 65 | XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait()) 66 | 67 | embedded.embeddedEventLoop.advanceTime(by: .milliseconds(20)) 68 | 69 | XCTAssertThrowsError(try XCTUnwrap(socksEventsHandler.socksEstablishedFuture).wait()) { 70 | XCTAssertEqual($0 as? HTTPClientError, .socksHandshakeTimeout) 71 | } 72 | XCTAssertFalse(embedded.isActive, "The timeout shall close the connection") 73 | } 74 | 75 | func testHandlerWorksIfDeadlineIsInPast() { 76 | // .uptimeNanoseconds(0) => .now() for EmbeddedEventLoops 77 | let socksEventsHandler = SOCKSEventsHandler(deadline: .uptimeNanoseconds(0)) 78 | XCTAssertNil(socksEventsHandler.socksEstablishedFuture) 79 | let embedded = EmbeddedChannel(handlers: [socksEventsHandler]) 80 | embedded.embeddedEventLoop.advanceTime(by: .milliseconds(10)) 81 | 82 | XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait()) 83 | 84 | // schedules execute only on the next tick 85 | embedded.embeddedEventLoop.run() 86 | XCTAssertThrowsError(try XCTUnwrap(socksEventsHandler.socksEstablishedFuture).wait()) { 87 | XCTAssertEqual($0 as? HTTPClientError, .socksHandshakeTimeout) 88 | } 89 | XCTAssertFalse(embedded.isActive, "The timeout shall close the connection") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/SOCKSTestUtils.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import NIOCore 17 | import NIOHTTP1 18 | import NIOPosix 19 | import NIOSOCKS 20 | import XCTest 21 | 22 | struct MockSOCKSError: Error, Hashable { 23 | var description: String 24 | } 25 | 26 | class TestSOCKSBadServerHandler: ChannelInboundHandler { 27 | typealias InboundIn = ByteBuffer 28 | 29 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 30 | // just write some nonsense bytes 31 | let buffer = context.channel.allocator.buffer(bytes: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]) 32 | context.writeAndFlush(.init(buffer), promise: nil) 33 | } 34 | } 35 | 36 | class MockSOCKSServer { 37 | let channel: Channel 38 | 39 | var port: Int { 40 | self.channel.localAddress!.port! 41 | } 42 | 43 | init( 44 | expectedURL: String, 45 | expectedResponse: String, 46 | misbehave: Bool = false, 47 | file: String = #filePath, 48 | line: UInt = #line 49 | ) throws { 50 | let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) 51 | let bootstrap: ServerBootstrap 52 | if misbehave { 53 | bootstrap = ServerBootstrap(group: elg) 54 | .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) 55 | .childChannelInitializer { channel in 56 | channel.eventLoop.makeCompletedFuture { 57 | try channel.pipeline.syncOperations.addHandler(TestSOCKSBadServerHandler()) 58 | } 59 | } 60 | } else { 61 | bootstrap = ServerBootstrap(group: elg) 62 | .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) 63 | .childChannelInitializer { channel in 64 | channel.eventLoop.makeCompletedFuture { 65 | let handshakeHandler = SOCKSServerHandshakeHandler() 66 | try channel.pipeline.syncOperations.addHandlers([ 67 | handshakeHandler, 68 | SOCKSTestHandler(handshakeHandler: handshakeHandler), 69 | TestHTTPServer( 70 | expectedURL: expectedURL, 71 | expectedResponse: expectedResponse, 72 | file: file, 73 | line: line 74 | ), 75 | ]) 76 | } 77 | } 78 | } 79 | self.channel = try bootstrap.bind(host: "localhost", port: 0).wait() 80 | } 81 | 82 | func shutdown() throws { 83 | try self.channel.close().wait() 84 | } 85 | } 86 | 87 | class SOCKSTestHandler: ChannelInboundHandler, RemovableChannelHandler { 88 | typealias InboundIn = ClientMessage 89 | 90 | let handshakeHandler: SOCKSServerHandshakeHandler 91 | 92 | init(handshakeHandler: SOCKSServerHandshakeHandler) { 93 | self.handshakeHandler = handshakeHandler 94 | } 95 | 96 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 97 | guard context.channel.isActive else { 98 | return 99 | } 100 | 101 | let message = self.unwrapInboundIn(data) 102 | switch message { 103 | case .greeting: 104 | context.writeAndFlush( 105 | .init( 106 | ServerMessage.selectedAuthenticationMethod(.init(method: .noneRequired)) 107 | ), 108 | promise: nil 109 | ) 110 | case .authenticationData: 111 | context.fireErrorCaught(MockSOCKSError(description: "Received authentication data but didn't receive any.")) 112 | case .request(let request): 113 | context.writeAndFlush( 114 | .init( 115 | ServerMessage.response(.init(reply: .succeeded, boundAddress: request.addressType)) 116 | ), 117 | promise: nil 118 | ) 119 | 120 | do { 121 | try context.channel.pipeline.syncOperations.addHandlers( 122 | [ 123 | ByteToMessageHandler(HTTPRequestDecoder()), 124 | HTTPResponseEncoder(), 125 | ], 126 | position: .after(self) 127 | ) 128 | context.channel.pipeline.syncOperations.removeHandler(self, promise: nil) 129 | context.channel.pipeline.syncOperations.removeHandler(self.handshakeHandler, promise: nil) 130 | } catch { 131 | context.fireErrorCaught(error) 132 | } 133 | } 134 | } 135 | } 136 | 137 | class TestHTTPServer: ChannelInboundHandler { 138 | typealias InboundIn = HTTPServerRequestPart 139 | typealias OutboundOut = HTTPServerResponsePart 140 | 141 | let expectedURL: String 142 | let expectedResponse: String 143 | let file: String 144 | let line: UInt 145 | var requestCount = 0 146 | 147 | init(expectedURL: String, expectedResponse: String, file: String, line: UInt) { 148 | self.expectedURL = expectedURL 149 | self.expectedResponse = expectedResponse 150 | self.file = file 151 | self.line = line 152 | } 153 | 154 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 155 | let message = self.unwrapInboundIn(data) 156 | switch message { 157 | case .head(let head): 158 | guard self.requestCount == 0 else { 159 | return 160 | } 161 | XCTAssertEqual(head.uri, self.expectedURL) 162 | self.requestCount += 1 163 | case .body: 164 | break 165 | case .end: 166 | context.write(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok))), promise: nil) 167 | context.write( 168 | self.wrapOutboundOut( 169 | .body(.byteBuffer(context.channel.allocator.buffer(string: self.expectedResponse))) 170 | ), 171 | promise: nil 172 | ) 173 | context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) 174 | } 175 | } 176 | 177 | func errorCaught(context: ChannelHandlerContext, error: Error) { 178 | context.fireErrorCaught(error) 179 | context.close(promise: nil) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/SSLContextCacheTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | import NIOPosix 17 | import NIOSSL 18 | import XCTest 19 | 20 | @testable import AsyncHTTPClient 21 | 22 | final class SSLContextCacheTests: XCTestCase { 23 | func testRequestingSSLContextWorks() { 24 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 25 | let eventLoop = group.next() 26 | let cache = SSLContextCache() 27 | defer { 28 | XCTAssertNoThrow(try group.syncShutdownGracefully()) 29 | } 30 | 31 | XCTAssertNoThrow( 32 | try cache.sslContext( 33 | tlsConfiguration: .makeClientConfiguration(), 34 | eventLoop: eventLoop, 35 | logger: HTTPClient.loggingDisabled 36 | ).wait() 37 | ) 38 | } 39 | 40 | func testCacheWorks() { 41 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 42 | let eventLoop = group.next() 43 | let cache = SSLContextCache() 44 | defer { 45 | XCTAssertNoThrow(try group.syncShutdownGracefully()) 46 | } 47 | 48 | var firstContext: NIOSSLContext? 49 | var secondContext: NIOSSLContext? 50 | 51 | XCTAssertNoThrow( 52 | firstContext = try cache.sslContext( 53 | tlsConfiguration: .makeClientConfiguration(), 54 | eventLoop: eventLoop, 55 | logger: HTTPClient.loggingDisabled 56 | ).wait() 57 | ) 58 | XCTAssertNoThrow( 59 | secondContext = try cache.sslContext( 60 | tlsConfiguration: .makeClientConfiguration(), 61 | eventLoop: eventLoop, 62 | logger: HTTPClient.loggingDisabled 63 | ).wait() 64 | ) 65 | XCTAssertNotNil(firstContext) 66 | XCTAssertNotNil(secondContext) 67 | XCTAssert(firstContext === secondContext) 68 | } 69 | 70 | func testCacheDoesNotReturnWrongEntry() { 71 | let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 72 | let eventLoop = group.next() 73 | let cache = SSLContextCache() 74 | defer { 75 | XCTAssertNoThrow(try group.syncShutdownGracefully()) 76 | } 77 | 78 | var firstContext: NIOSSLContext? 79 | var secondContext: NIOSSLContext? 80 | 81 | XCTAssertNoThrow( 82 | firstContext = try cache.sslContext( 83 | tlsConfiguration: .makeClientConfiguration(), 84 | eventLoop: eventLoop, 85 | logger: HTTPClient.loggingDisabled 86 | ).wait() 87 | ) 88 | 89 | // Second one has a _different_ TLSConfiguration. 90 | var testTLSConfig = TLSConfiguration.makeClientConfiguration() 91 | testTLSConfig.certificateVerification = .none 92 | XCTAssertNoThrow( 93 | secondContext = try cache.sslContext( 94 | tlsConfiguration: testTLSConfig, 95 | eventLoop: eventLoop, 96 | logger: HTTPClient.loggingDisabled 97 | ).wait() 98 | ) 99 | XCTAssertNotNil(firstContext) 100 | XCTAssertNotNil(secondContext) 101 | XCTAssert(firstContext !== secondContext) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/StressGetHttpsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import AsyncHTTPClient 16 | import Atomics 17 | import Logging 18 | import NIOConcurrencyHelpers 19 | import NIOCore 20 | import NIOFoundationCompat 21 | import NIOHTTP1 22 | import NIOHTTPCompression 23 | import NIOPosix 24 | import NIOSSL 25 | import NIOTestUtils 26 | import NIOTransportServices 27 | import XCTest 28 | 29 | #if canImport(Network) 30 | import Network 31 | #endif 32 | 33 | final class StressGetHttpsTests: XCTestCaseHTTPClientTestsBaseClass { 34 | func testStressGetHttps() throws { 35 | let localHTTPBin = HTTPBin(.http1_1(ssl: true)) 36 | let localClient = HTTPClient( 37 | eventLoopGroupProvider: .shared(self.clientGroup), 38 | configuration: HTTPClient.Configuration(certificateVerification: .none) 39 | ) 40 | defer { 41 | XCTAssertNoThrow(try localClient.syncShutdown()) 42 | XCTAssertNoThrow(try localHTTPBin.shutdown()) 43 | } 44 | 45 | let eventLoop = localClient.eventLoopGroup.next() 46 | let requestCount = 200 47 | var futureResults = [EventLoopFuture]() 48 | for _ in 1...requestCount { 49 | let req = try HTTPClient.Request( 50 | url: "https://localhost:\(localHTTPBin.port)/get", 51 | method: .GET, 52 | headers: ["X-internal-delay": "100"] 53 | ) 54 | futureResults.append(localClient.execute(request: req)) 55 | } 56 | XCTAssertNoThrow(try EventLoopFuture.andAllSucceed(futureResults, on: eventLoop).wait()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/TLSEventsHandlerTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIOCore 16 | import NIOEmbedded 17 | import NIOSSL 18 | import NIOTLS 19 | import XCTest 20 | 21 | @testable import AsyncHTTPClient 22 | 23 | class TLSEventsHandlerTests: XCTestCase { 24 | func testHandlerHappyPath() { 25 | let tlsEventsHandler = TLSEventsHandler(deadline: nil) 26 | XCTAssertNil(tlsEventsHandler.tlsEstablishedFuture) 27 | let embedded = EmbeddedChannel(handlers: [tlsEventsHandler]) 28 | XCTAssertNotNil(tlsEventsHandler.tlsEstablishedFuture) 29 | 30 | XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait()) 31 | 32 | embedded.pipeline.fireUserInboundEventTriggered(TLSUserEvent.handshakeCompleted(negotiatedProtocol: "abcd1234")) 33 | XCTAssertEqual(try XCTUnwrap(tlsEventsHandler.tlsEstablishedFuture).wait(), "abcd1234") 34 | } 35 | 36 | func testHandlerFailsFutureWhenRemovedWithoutEvent() { 37 | let tlsEventsHandler = TLSEventsHandler(deadline: nil) 38 | XCTAssertNil(tlsEventsHandler.tlsEstablishedFuture) 39 | let embedded = EmbeddedChannel(handlers: [tlsEventsHandler]) 40 | XCTAssertNotNil(tlsEventsHandler.tlsEstablishedFuture) 41 | 42 | XCTAssertNoThrow(try embedded.pipeline.syncOperations.removeHandler(tlsEventsHandler).wait()) 43 | XCTAssertThrowsError(try XCTUnwrap(tlsEventsHandler.tlsEstablishedFuture).wait()) 44 | } 45 | 46 | func testHandlerFailsFutureWhenHandshakeFails() { 47 | let tlsEventsHandler = TLSEventsHandler(deadline: nil) 48 | XCTAssertNil(tlsEventsHandler.tlsEstablishedFuture) 49 | let embedded = EmbeddedChannel(handlers: [tlsEventsHandler]) 50 | XCTAssertNotNil(tlsEventsHandler.tlsEstablishedFuture) 51 | 52 | embedded.pipeline.fireErrorCaught(NIOSSLError.handshakeFailed(BoringSSLError.wantConnect)) 53 | XCTAssertThrowsError(try XCTUnwrap(tlsEventsHandler.tlsEstablishedFuture).wait()) { 54 | XCTAssertEqual($0 as? NIOSSLError, .handshakeFailed(BoringSSLError.wantConnect)) 55 | } 56 | } 57 | 58 | func testHandlerIgnoresShutdownCompletedEvent() { 59 | let tlsEventsHandler = TLSEventsHandler(deadline: nil) 60 | XCTAssertNil(tlsEventsHandler.tlsEstablishedFuture) 61 | let embedded = EmbeddedChannel(handlers: [tlsEventsHandler]) 62 | XCTAssertNotNil(tlsEventsHandler.tlsEstablishedFuture) 63 | 64 | XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait()) 65 | 66 | // ignore event 67 | embedded.pipeline.fireUserInboundEventTriggered(TLSUserEvent.shutdownCompleted) 68 | 69 | embedded.pipeline.fireUserInboundEventTriggered(TLSUserEvent.handshakeCompleted(negotiatedProtocol: "alpn")) 70 | XCTAssertEqual(try XCTUnwrap(tlsEventsHandler.tlsEstablishedFuture).wait(), "alpn") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/AsyncHTTPClientTests/XCTest+AsyncAwait.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the AsyncHTTPClient open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | // 15 | // Copyright 2021, gRPC Authors All rights reserved. 16 | // 17 | // Licensed under the Apache License, Version 2.0 (the "License"); 18 | // you may not use this file except in compliance with the License. 19 | // You may obtain a copy of the License at 20 | // 21 | // http://www.apache.org/licenses/LICENSE-2.0 22 | // 23 | // Unless required by applicable law or agreed to in writing, software 24 | // distributed under the License is distributed on an "AS IS" BASIS, 25 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | // See the License for the specific language governing permissions and 27 | // limitations under the License. 28 | // 29 | 30 | import XCTest 31 | 32 | extension XCTestCase { 33 | /// Cross-platform XCTest support for async-await tests. 34 | /// 35 | /// Currently the Linux implementation of XCTest doesn't have async-await support. 36 | /// Until it does, we make use of this shim which uses a detached `Task` along with 37 | /// `XCTest.wait(for:timeout:)` to wrap the operation. 38 | /// 39 | /// - NOTE: Support for Linux is tracked by https://bugs.swift.org/browse/SR-14403. 40 | /// - NOTE: Implementation currently in progress: https://github.com/apple/swift-corelibs-xctest/pull/326 41 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 42 | func XCTAsyncTest( 43 | expectationDescription: String = "Async operation", 44 | timeout: TimeInterval = 30, 45 | file: StaticString = #filePath, 46 | line: UInt = #line, 47 | function: StaticString = #function, 48 | operation: @escaping @Sendable () async throws -> Void 49 | ) { 50 | let expectation = self.expectation(description: expectationDescription) 51 | Task { 52 | do { 53 | try await operation() 54 | } catch { 55 | XCTFail("Error thrown while executing \(function): \(error)", file: file, line: line) 56 | for symbol in Thread.callStackSymbols { print(symbol) } 57 | } 58 | expectation.fulfill() 59 | } 60 | self.wait(for: [expectation], timeout: timeout) 61 | } 62 | } 63 | 64 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 65 | internal func XCTAssertThrowsError( 66 | _ expression: @autoclosure () async throws -> T, 67 | verify: (Error) -> Void = { _ in }, 68 | file: StaticString = #filePath, 69 | line: UInt = #line 70 | ) async { 71 | do { 72 | _ = try await expression() 73 | XCTFail("Expression did not throw error", file: file, line: line) 74 | } catch { 75 | verify(error) 76 | } 77 | } 78 | 79 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 80 | internal func XCTAssertNoThrowWithResult( 81 | _ expression: @autoclosure () async throws -> Result, 82 | file: StaticString = #filePath, 83 | line: UInt = #line 84 | ) async -> Result? { 85 | do { 86 | return try await expression() 87 | } catch { 88 | XCTFail("Expression did throw: \(error)", file: file, line: line) 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------