From 717d1c3bba995c303c8cced9dedc6885199ad3cb Mon Sep 17 00:00:00 2001 From: sdpopov Date: Thu, 21 Jan 2021 01:27:42 +0300 Subject: [PATCH] Initial update. --- .gitignore | 7 + Package.resolved | 43 ++ Package.swift | 42 ++ README.md | 42 ++ Sources/kvHttp2Kit/KvHttpServer.swift | 431 ++++++++++++++++++++ Tests/LinuxMain.swift | 30 ++ Tests/kvHttp2KitTests/KvHttp2KitTests.swift | 67 +++ Tests/kvHttp2KitTests/XCTestManifests.swift | 32 ++ 8 files changed, 694 insertions(+) create mode 100644 .gitignore create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/kvHttp2Kit/KvHttpServer.swift create mode 100644 Tests/LinuxMain.swift create mode 100644 Tests/kvHttp2KitTests/KvHttp2KitTests.swift create mode 100644 Tests/kvHttp2KitTests/XCTestManifests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ccf559 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +.swiftpm/xcode +Ignored/ diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..a655c49 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,43 @@ +{ + "object": { + "pins": [ + { + "package": "kvKit-Swift", + "repositoryURL": "https://github.com/keyvariable/kvKit-Swift.git", + "state": { + "branch": null, + "revision": "f0c4253c7214ce090feb6adf6247fe5b918b70e0", + "version": "1.0.0" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "43931b7a7daf8120a487601530c8bc03ce711992", + "version": "2.25.1" + } + }, + { + "package": "swift-nio-http2", + "repositoryURL": "https://github.com/apple/swift-nio-http2.git", + "state": { + "branch": null, + "revision": "d4060ac4d056a48d946298f04968f6f6080cc618", + "version": "1.16.2" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "62bf5083df970e67c886210fa5b857eacf044b7c", + "version": "2.10.2" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..bc689b6 --- /dev/null +++ b/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version:5.2 +// +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2021 Svyatoslav Popov. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +let package = Package( + name: "kvHttp2Kit-Swift", + products: [ + .library(name: "kvHttp2Kit", targets: [ "kvHttp2Kit" ]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.13.0"), + .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.9.0"), + .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.6.0"), + .package(url: "https://github.com/keyvariable/kvKit-Swift.git", from: "1.0.0"), + ], + targets: [ + .target(name: "kvHttp2Kit", + dependencies: [ .product(name: "kvKit", package: "kvKit-Swift"), + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSSL", package: "swift-nio-ssl") ]), + .testTarget(name: "kvHttp2KitTests", dependencies: [ "kvHttp2Kit" ]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..586e420 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# kvHttp2Kit-Swift + +![Swift 5.2](https://img.shields.io/badge/swift-5.2-green.svg) +![Linux](https://img.shields.io/badge/os-linux-green.svg) +![macOS](https://img.shields.io/badge/os-macOS-green.svg) + +A collection of auxiliaries for HTTP and HTTP/2 on Swift. It's based on [SwiftNIO](https://github.com/apple/swift-nio). + + +## Supported Platforms + +The same as [SwiftNIO](https://github.com/apple/swift-nio). + + +## Getting Started + +### Swift Tools 5.2+ + +#### Package Dependencies: + +```swift +dependencies: [ + .package(url: "https://github.com/keyvariable/kvHttp2Kit-Swift", from: "0.1.0"), +] +``` + +#### Target Dependencies: + +```swift +dependencies: [ + .product(name: "kvHttp2Kit", package: "kvHttp2Kit-Swift"), +] +``` + +### Xcode + +Documentation: [Adding Package Dependencies to Your App](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app). + + +## Authors + +- Svyatoslav Popov ([@sdpopov-keyvariable](https://github.com/sdpopov-keyvariable), [info@keyvar.com](mailto:info@keyvar.com)). diff --git a/Sources/kvHttp2Kit/KvHttpServer.swift b/Sources/kvHttp2Kit/KvHttpServer.swift new file mode 100644 index 0000000..f832dbd --- /dev/null +++ b/Sources/kvHttp2Kit/KvHttpServer.swift @@ -0,0 +1,431 @@ +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2021 Svyatoslav Popov. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// KvHttpServer.swift +// kvHttp2Kit +// +// Created by Svyatoslav Popov on 15.04.2020. +// + +import Foundation + + + +import Foundation +import kvKit +import NIO +import NIOHTTP1 +import NIOHTTP2 +import NIOSSL + + + +public protocol KvHttpServerDelegate : class { + + func httpServerDidStart(_ httpServer: KvHttpServer) + + func httpServer(_ httpServer: KvHttpServer, didStopWith result: Result) + + func httpServer(_ httpServer: KvHttpServer, didStart httpChannelhandler: KvHttpServer.ChannelHandler) + + func httpServer(_ httpServer: KvHttpServer, didCatch error: Error) + +} + + + +public protocol KvHttpChannelHandlerDelegate : class { + + func httpChannelHandler(_ httpChannelHandler: KvHttpServer.ChannelHandler, didReceive requestPart: KvHttpServer.ChannelHandler.RequestPart) + + func httpChannelHandler(_ httpChannelHandler: KvHttpServer.ChannelHandler, didCatch error: Error) + +} + + + +/// An HTTP/2 server handling requests in HTTP1 style. +public class KvHttpServer { + + public let configuration: Configuration + + + public weak var delegate: KvHttpServerDelegate? + + + + public init(with configuration: Configuration) { + self.configuration = configuration + } + + + + deinit { + KvThreadKit.locking(mutationLock) { + channel = nil + } + } + + + + private let mutationLock = NSRecursiveLock() + + + private var channel: Channel? { + didSet { + guard channel !== oldValue else { return } + + try! oldValue?.close().wait() + + if let channel = channel { + delegate?.httpServerDidStart(self) + + channel.closeFuture.whenComplete({ [weak self] (result) in + guard let server = self else { return } + + KvThreadKit.locking(server.mutationLock) { + server.channel = nil + } + + server.delegate?.httpServer(server, didStopWith: result) + }) + + } else { + eventLoopGroup = nil + } + } + } + + private var eventLoopGroup: MultiThreadedEventLoopGroup? { + didSet { + guard eventLoopGroup !== oldValue else { return } + + try! oldValue?.syncShutdownGracefully() + } + } + +} + + + +// MARK: Configuration + +extension KvHttpServer { + + public struct Configuration { + + public var host: String + public var port: Int + + public var ssl: SSL + + + + public init(host: String = Defaults.host, port: Int, ssl: SSL) { + self.host = host + self.port = port + self.ssl = ssl + } + + + + // MARK: Defaults + + public struct Defaults { + + public static let host: String = "::1" + + } + + + + // MARK: SSL + + public struct SSL { + + public var privateKey: NIOSSLPrivateKey + public var certificateChain: [NIOSSLCertificate] + + + + public init(privateKey: NIOSSLPrivateKey, certificateChain: [NIOSSLCertificate]) { + self.privateKey = privateKey + self.certificateChain = certificateChain + } + + } + + } + +} + + + +// MARK: Context + +extension KvHttpServer { + + public typealias Context = ChannelHandlerContext + +} + + + +extension KvHttpServer.Context : Hashable { + + // MARK: : Equatable + + public static func ==(lhs: KvHttpServer.Context, rhs: KvHttpServer.Context) -> Bool { lhs === rhs } + + + + // MARK: : Hashable + + public func hash(into hasher: inout Hasher) { ObjectIdentifier(self).hash(into: &hasher) } + +} + + + +// MARK: Status + +extension KvHttpServer { + + public var isStarted: Bool { + KvThreadKit.locking(mutationLock) { channel != nil } + } + + + public var localAddress: SocketAddress? { + KvThreadKit.locking(mutationLock) { channel?.localAddress } + } + + + + public func start(synchronous isSynchronous: Bool = false) throws { + + final class ErrorHandler : ChannelInboundHandler { + + init(_ server: KvHttpServer?) { + self.server = server + } + + + private weak var server: KvHttpServer? + + + typealias InboundIn = Never + + + func errorCaught(context: ChannelHandlerContext, error: Error) { + guard let server = server else { + return NSLog("[KvHttpServer] Error: \(error)") + } + + server.delegate?.httpServer(server, didCatch: error) + } + + } + + + try KvThreadKit.locking(mutationLock) { + guard !isStarted else { return } + + let tlsConfiguration = TLSConfiguration.forServer(certificateChain: configuration.ssl.certificateChain.map { .certificate($0) }, + privateKey: .privateKey(configuration.ssl.privateKey), + applicationProtocols: NIOHTTP2SupportedALPNProtocols) + // Configure the SSL context that is used by all SSL handlers. + let sslContext = try NIOSSLContext(configuration: tlsConfiguration) + + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + + let bootstrap = ServerBootstrap(group: eventLoopGroup) + .serverChannelOption(ChannelOptions.backlog, value: 256) + .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) + + .childChannelInitializer({ [weak self] channel in + channel.pipeline.addHandler(NIOSSLServerHandler(context: sslContext)).flatMap { [weak self] in + channel.configureHTTP2Pipeline(mode: .server) { [weak self] streamChannel in + streamChannel.pipeline.addHandler(HTTP2FramePayloadToHTTP1ServerCodec()).flatMap { [weak self] in + streamChannel.pipeline.addHandler(InternalChannelHandler(self)) + }.flatMap { [weak self] in + streamChannel.pipeline.addHandler(ErrorHandler(self)) + } + } + }.flatMap { [weak self] (_: HTTP2StreamMultiplexer) in + channel.pipeline.addHandler(ErrorHandler(self)) + } + }) + + // Enable TCP_NODELAY and SO_REUSEADDR for the accepted Channels + .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) + .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) + .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1) + + channel = try bootstrap.bind(host: configuration.host, port: configuration.port).wait() + + self.eventLoopGroup = eventLoopGroup + + if isSynchronous { + try channel?.closeFuture.wait() + } + } + } + + + + public func stop() { + KvThreadKit.locking(mutationLock) { + channel = nil + } + } + +} + + + +// MARK: Response + +extension KvHttpServer { + + public enum Response { + case json(Data) + } + +} + + + +// MARK: : .ChannelHandler + +extension KvHttpServer { + + public class ChannelHandler { + + public typealias RequestPart = HTTPServerRequestPart + + + + public weak var delegate: KvHttpChannelHandlerDelegate? + + public fileprivate(set) weak var httpServer: KvHttpServer! + + + public var userInfo: Any? + + + + fileprivate init(_ httpServer: KvHttpServer?) { + self.httpServer = httpServer + + httpServer?.delegate?.httpServer(httpServer!, didStart: self) + } + + + + fileprivate weak var context: ChannelHandlerContext? + + + + public func submit(_ response: Response) throws { throw KvError.inconsistency("implementation for \(#function) is missing") } + + } + + + + fileprivate class InternalChannelHandler : ChannelHandler, ChannelInboundHandler { + + override func submit(_ response: Response) throws { + guard let context = context else { throw KvError.inconsistency("channel handler has no context") } + + context.eventLoop.execute { [weak self] in + context.channel.getOption(HTTP2StreamChannelOptions.streamID).flatMap({ [weak self] (streamID) -> EventLoopFuture in + + func DataBuffer(_ data: Data) -> ByteBuffer { + var buffer = context.channel.allocator.buffer(capacity: data.count) + + buffer.writeBytes(data) + + return buffer + } + + + guard let channelHandler = self else { return context.close() } + + let (contentType, contentLength, buffer): (String, UInt64, ByteBuffer) = { + switch response { + case .json(let data): + return ("application/json; charset=utf-8", numericCast(data.count), DataBuffer(data)) + } + }() + + + let headers: HTTPHeaders = [ "Content-Type": contentType, + "Content-Length": String(contentLength), + "x-stream-id": String(Int(streamID)), ] + + context.channel.write(channelHandler.wrapOutboundOut(HTTPServerResponsePart.head(HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok, headers: headers))), promise: nil) + context.channel.write(channelHandler.wrapOutboundOut(HTTPServerResponsePart.body(.byteBuffer(buffer))), promise: nil) + + return context.channel.writeAndFlush(channelHandler.wrapOutboundOut(HTTPServerResponsePart.end(nil))) + + }).whenComplete { _ in + context.close(promise: nil) + } + } + } + + + + // MARK: : ChannelInboundHandler + + typealias InboundIn = RequestPart + typealias OutboundOut = HTTPServerResponsePart + + + + func handlerAdded(context: ChannelHandlerContext) { + self.context = context + } + + + + func handlerRemoved(context: ChannelHandlerContext) { + assert(self.context == context) + + self.context = nil + } + + + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + assert(self.context == context) + + delegate?.httpChannelHandler(self, didReceive: unwrapInboundIn(data)) + } + + + + func errorCaught(context: ChannelHandlerContext, error: Error) { + assert(self.context == context) + + delegate?.httpChannelHandler(self, didCatch: error) + } + + } + +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..3951943 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2021 Svyatoslav Popov. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// LinuxMain.swift +// kvHttp2Kit +// +// Created by Svyatoslav Popov on 15.04.2020. +// + +import XCTest + +import kvKitTests + +var tests = [XCTestCaseEntry]() +tests += kvKitTests.allTests() +XCTMain(tests) diff --git a/Tests/kvHttp2KitTests/KvHttp2KitTests.swift b/Tests/kvHttp2KitTests/KvHttp2KitTests.swift new file mode 100644 index 0000000..ca75760 --- /dev/null +++ b/Tests/kvHttp2KitTests/KvHttp2KitTests.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2021 Svyatoslav Popov. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// KvHttp2KitTests.swift +// kvHttp2Kit +// +// Created by Svyatoslav Popov on 01.05.2020. +// + +import XCTest + +@testable import kvHttp2Kit + + + +final class KvHttp2KitTests : XCTestCase { + + static var allTests = [ + ("default", testApplication), + ] + + + + func testApplication() { + #if false + do { + let db = try KvPostgreSQL(with: .init(user: NSUserName())) + + let expectation = XCTestExpectation(description: "PostgreSQL version") + defer { wait(for: [expectation], timeout: 10.0) } + + let statement = try db.prepared("SELECT version()") + + statement.execute { (result) in + do { + try IteratorSequence(result.get()).forEach { row in + print(try row.get()) + } + + } catch { + XCTFail(error.localizedDescription) + } + + expectation.fulfill() + } + + } catch { + XCTFail(error.localizedDescription) + } + #endif + } + +} diff --git a/Tests/kvHttp2KitTests/XCTestManifests.swift b/Tests/kvHttp2KitTests/XCTestManifests.swift new file mode 100644 index 0000000..07df890 --- /dev/null +++ b/Tests/kvHttp2KitTests/XCTestManifests.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2021 Svyatoslav Popov. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +// +// XCTestManifests.swift +// kvHttp2Kit +// +// Created by Svyatoslav Popov on 16.04.2020. +// + +import XCTest + +#if !canImport(ObjectiveC) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(kvHttp2KitTests.allTests), + ] +} +#endif