├── .gitignore ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Omnibus │ └── SSH │ │ └── SSHClient.swift └── TypedChannels │ ├── TCP │ ├── TCPClient.swift │ └── TCPServer.swift │ └── TypedChannels.swift └── Tests └── OmnibusTests └── OmnibusTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-nio", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-nio.git", 7 | "state" : { 8 | "revision" : "124119f0bb12384cef35aa041d7c3a686108722d", 9 | "version" : "2.40.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Omnibus", 8 | platforms: [ 9 | .macOS(.v12), 10 | .iOS(.v15), 11 | .tvOS(.v13), 12 | .watchOS(.v6) 13 | ], 14 | products: [ 15 | // Products define the executables and libraries a package produces, and make them visible to other packages. 16 | .library( 17 | name: "Omnibus", 18 | targets: ["Omnibus"]), 19 | .library( 20 | name: "TypedChannels", 21 | targets: ["TypedChannels"]), 22 | ], 23 | dependencies: [ 24 | // Dependencies declare other packages that this package depends on. 25 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), 26 | // .package(url: "https://github.com/orlandos-nl/Citadel.git", branch: "async-await"), 27 | ], 28 | targets: [ 29 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 30 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 31 | .target( 32 | name: "TypedChannels", 33 | dependencies: [ 34 | .product(name: "NIO", package: "swift-nio"), 35 | ]), 36 | .target( 37 | name: "Omnibus", 38 | dependencies: [ 39 | "TypedChannels", 40 | .product(name: "NIO", package: "swift-nio"), 41 | // .product(name: "Citadel", package: "Citadel"), 42 | ]), 43 | .testTarget( 44 | name: "OmnibusTests", 45 | dependencies: [ 46 | "TypedChannels", 47 | "Omnibus", 48 | .product(name: "NIOHTTP1", package: "swift-nio"), 49 | ]), 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # We're working on adding this to SwiftNIO itself 2 | 3 | Omnibus is a set of helpers for SwiftNIO that allow you to leverage Swift's generics type system to create NIO Channels. 4 | 5 | It depends on Swift 5.7's new ResultBuilder feature, [Partial Blocks](https://github.com/apple/swift-evolution/blob/main/proposals/0348-buildpartialblock.md), and leverages this to enable type-checked channel building. 6 | 7 | Channels are written in the same order as they're used in NIO normally. Inbound data comes in from the top, using the channel's read (Inbound) types. For official NIO channels that's IOData, and official channels also write (Outbound) IOData. 8 | 9 | Each handler transforms either the Inboud, Outbound or both types using `ChannelInboundHandler`, `ChannelOutboundHandler` or `ChannelDuplexHandler` respectively. 10 | 11 | Omnibus' channel builder type checks each handler, so that the InboundOut of one handler must match the InboundIn of the next. Likewise, it checks if the OutboundIn and OutboundOut match up as well. 12 | 13 | When writing channels using this system, or NIO in general, the input (read data) comes in at the first handler. However, outbound data comes in at the bottom of the chain, and works its way back to the front. 14 | 15 | ### Example 16 | 17 | ```swift 18 | let myPrivateKey: Insecure.RSA.PrivateKey = .. 19 | 20 | // Create a TCP Server that accepts clients on localhost:8082 21 | let server = try await TCPServer.buildServer(host: "127.0.0.1", port: 8082) { 22 | // InboundHandler that maps NIO's IOData to ByteBuffer 23 | IODataInboundDecoder() 24 | // OutboundHandler that encodes HTTPServerResponsePart to IOData 25 | HTTPResponseEncoder() 26 | // InboundHandler that decodes ByteBuffer 27 | ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .dropBytes)) 28 | // InboundHandler that maps HTTPServerRequestPart to HTTPClientRequestPart 29 | HTTPProxyServerRequestMapper() 30 | // OutboundHandler that maps HTTPClientResponsePart to HTTPServerResponsePart 31 | HTTPProxyServerResponseMapper() 32 | 33 | // DuplexHandler that consumes all Inbound data 34 | ProxyChannelHandler { baseChannel in 35 | // baseChannel is the client connected to TCPServer 36 | // 1. Connect to an SSHServer and start an SSH tunnel 37 | // 2. Configure that tunnel's channel 38 | // 3. Return the configured (typed) channel 39 | return try await SSHClient.connect( 40 | host: "orlandos.nl", 41 | authenticationMethod: .rsa(username: "joannis", privateKey: myPrivateKey), 42 | hostKeyValidator: .acceptAnything() 43 | ).buildTunnel(host: "example.com", port: 80) { 44 | // OutboundHandler that maps IOData to ByteBuffer. SSHTunnel reads/writes ByteBuffer, not IOData. 45 | IODataOutboundDecoder() 46 | // OutboundHandler that Encodes Ooutbound HTTPClientRequestPart to IOData 47 | HTTPRequestEncoder() 48 | // InboundHandler that decodes incoming ByteBuffer into HTTPClientResponsePart 49 | HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes) 50 | // InboundHandler that consumes HTTPClientResponsePart, and writes it to the TCPServer's client 51 | OutputToChannelHandler(channel: baseChannel, payloadType: HTTPClientResponsePart.self) 52 | } 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /Sources/Omnibus/SSH/SSHClient.swift: -------------------------------------------------------------------------------- 1 | //import NIO 2 | //import Citadel 3 | //import NIOSSH 4 | // 5 | //public struct SSHClient { 6 | // let client: Citadel.SSHClient 7 | // 8 | // public static func connect( 9 | // host: String, 10 | // port: Int = 22, 11 | // authenticationMethod: SSHAuthenticationMethod, 12 | // hostKeyValidator: SSHHostKeyValidator, 13 | // group: MultiThreadedEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 14 | // ) async throws -> SSHClient { 15 | // return try await SSHClient( 16 | // client: .connect( 17 | // host: host, 18 | // port: port, 19 | // authenticationMethod: authenticationMethod, 20 | // hostKeyValidator: hostKeyValidator, 21 | // reconnect: .never, 22 | // group: group 23 | // ) 24 | // ) 25 | // } 26 | // 27 | // public func buildTunnel( 28 | // host: String, 29 | // port: Int, 30 | // @ChannelBuilder buildHandlers: @escaping () -> ConfiguredChannel 31 | // ) async throws -> SSHTunnel { 32 | // let channel = try await client.createDirectTCPIPChannel( 33 | // using: .init( 34 | // targetHost: host, 35 | // targetPort: port, 36 | // originatorAddress: SocketAddress(ipAddress: "fe80::1", port: port) 37 | // ) 38 | // ) { channel in 39 | // channel.pipeline.addHandlers(buildHandlers().handlers) 40 | // } 41 | // 42 | // return SSHTunnel<_>(channel: channel) 43 | // } 44 | //} 45 | // 46 | //public struct SSHTunnel: ReadableChannel, WritableChannel, ClosableChannel { 47 | // public typealias InboundIn = ByteBuffer 48 | // public typealias OutboundOut = ByteBuffer 49 | // 50 | // let channel: Channel 51 | // 52 | // public func write(_ data: OutboundIn) async throws { 53 | // try await channel.writeAndFlush(data) 54 | // } 55 | // 56 | // public func close() async throws { 57 | // try await channel.close() 58 | // } 59 | //} 60 | -------------------------------------------------------------------------------- /Sources/TypedChannels/TCP/TCPClient.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | // TODO: import NIOTransportServices 3 | 4 | public struct TCPClient: WritableChannel, ClosableChannel { 5 | public typealias InboundIn = IOData 6 | public typealias OutboundOut = IOData 7 | 8 | private let channel: Channel 9 | 10 | public static func buildPosixClient( 11 | host: String, 12 | port: Int, 13 | group: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1), 14 | @ChannelBuilder buildHandlers: @escaping () -> ConfiguredChannel 15 | ) async throws -> TCPClient { 16 | let channel = try await ClientBootstrap(group: group) 17 | .channelInitializer { channel in 18 | channel.pipeline.addHandlers(buildHandlers().handlers) 19 | } 20 | .connect(host: host, port: port) 21 | .get() 22 | 23 | return TCPClient<_>(channel: channel) 24 | } 25 | 26 | // TODO: NIOTS 27 | 28 | public func write(_ data: OutboundIn) async throws { 29 | try await channel.writeAndFlush(data) 30 | } 31 | 32 | public func close() async throws { 33 | try await channel.close() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/TypedChannels/TCP/TCPServer.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | // TODO: import NIOTransportServices 3 | 4 | public struct TCPServer: ClosableChannel { 5 | public typealias InboundIn = IOData 6 | public typealias OutboundOut = IOData 7 | 8 | private let channel: Channel 9 | 10 | public static func buildServer( 11 | host: String, 12 | port: Int, 13 | group: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1), 14 | @ChannelBuilder buildHandlers: @escaping () -> ConfiguredChannel 15 | ) async throws -> TCPServer { 16 | let channel = try await ServerBootstrap(group: group) 17 | .childChannelInitializer { channel in 18 | channel.pipeline.addHandlers(buildHandlers().handlers) 19 | } 20 | .bind(host: host, port: port) 21 | .get() 22 | 23 | return TCPServer(channel: channel) 24 | } 25 | 26 | public func close() async throws { 27 | try await channel.close() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/TypedChannels/TypedChannels.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | @resultBuilder public struct ChannelBuilder { 4 | public static func buildPartialBlock( 5 | first handler: Handler 6 | ) -> ModifiedTypedChannel where InboundOut == Handler.InboundIn, OutboundIn == Handler.OutboundOut { 7 | ModifiedTypedChannel<_, _>(handlers: [ handler ]) 8 | } 9 | 10 | public static func buildPartialBlock< 11 | PartialIn, PartialOut, 12 | Handler: ChannelDuplexHandler 13 | >( 14 | accumulated base: ModifiedTypedChannel, 15 | next handler: Handler 16 | ) -> ModifiedTypedChannel where PartialIn == Handler.InboundIn, OutboundIn == Handler.OutboundOut 17 | { 18 | ModifiedTypedChannel<_, _>(handlers: base.handlers + [handler]) 19 | } 20 | 21 | @_disfavoredOverload 22 | public static func buildPartialBlock( 23 | first handler: Handler 24 | ) -> ModifiedTypedChannel where InboundOut == Handler.InboundIn { 25 | ModifiedTypedChannel<_, _>(handlers: [ handler ]) 26 | } 27 | 28 | @_disfavoredOverload 29 | public static func buildPartialBlock( 30 | first handler: Handler 31 | ) -> ModifiedTypedChannel where OutboundIn == Handler.OutboundOut { 32 | ModifiedTypedChannel<_, _>(handlers: [ handler ]) 33 | } 34 | 35 | @_disfavoredOverload 36 | public static func buildPartialBlock< 37 | PartialIn, PartialOut, 38 | Handler: ChannelInboundHandler 39 | >( 40 | accumulated base: ModifiedTypedChannel, 41 | next handler: Handler 42 | ) -> ModifiedTypedChannel where PartialIn == Handler.InboundIn 43 | { 44 | ModifiedTypedChannel<_, _>(handlers: base.handlers + [handler]) 45 | } 46 | 47 | @_disfavoredOverload 48 | public static func buildPartialBlock( 49 | accumulated base: ModifiedTypedChannel, 50 | next decoder: Decoder 51 | ) -> ModifiedTypedChannel { 52 | ModifiedTypedChannel<_, _>( 53 | handlers: base.handlers + [ByteToMessageHandler(decoder)] 54 | ) 55 | } 56 | 57 | @_disfavoredOverload 58 | public static func buildPartialBlock( 59 | accumulated base: ModifiedTypedChannel, 60 | next encoder: Encoder 61 | ) -> ModifiedTypedChannel { 62 | ModifiedTypedChannel<_, _>( 63 | handlers: base.handlers + [MessageToByteHandler(encoder)] 64 | ) 65 | } 66 | 67 | @_disfavoredOverload 68 | public static func buildPartialBlock< 69 | PartialIn, PartialOut, 70 | Handler: ChannelOutboundHandler 71 | >( 72 | accumulated base: ModifiedTypedChannel, 73 | next handler: Handler 74 | ) -> ModifiedTypedChannel where PartialOut == Handler.OutboundOut 75 | { 76 | ModifiedTypedChannel<_, _>(handlers: base.handlers + [handler]) 77 | } 78 | 79 | // public static func buildPartialBlock( 80 | // accumulated base: ModifiedTypedChannel, 81 | // next handler: GlueHandler 82 | // ) -> ConfiguredChannel where C.OutboundIn == PartialOut { 83 | // ConfiguredChannel<_, _, _, _>( 84 | // handlers: base.handlers + [handler.makeHandler()] 85 | // ) 86 | // } 87 | 88 | @_disfavoredOverload 89 | public static func buildFinalResult( 90 | _ component: ConfiguredChannel 91 | ) -> ConfiguredChannel { 92 | component 93 | } 94 | 95 | public static func buildFinalResult( 96 | _ component: ModifiedTypedChannel 97 | ) -> ConfiguredChannel { 98 | ConfiguredChannel<_, _, _, _>(handlers: component.handlers) 99 | } 100 | } 101 | 102 | //public struct GlueHandler { 103 | // let channel: C 104 | // 105 | // public init(to channel: C) { 106 | // self.channel = channel 107 | // } 108 | // 109 | // func makeHandler() -> OutputToChannelHandler { 110 | // OutputToChannelHandler(channel: channel) 111 | // } 112 | //} 113 | 114 | public struct ConfiguredChannel { 115 | let handlers: [ChannelHandler] 116 | 117 | func addHandlers(to channel: Channel) async throws { 118 | try await channel.pipeline.addHandlers(handlers) 119 | } 120 | 121 | // public func glue( 122 | // to channel: C 123 | // ) -> ConfiguredChannel where C.OutboundIn == InboundOut { 124 | // let outputHandler = OutputToChannelHandler(channel: channel) 125 | // return .init(handlers: handlers + [outputHandler]) 126 | // } 127 | } 128 | 129 | public struct ModifiedTypedChannel { 130 | let handlers: [ChannelHandler] 131 | } 132 | 133 | extension ClientBootstrap { 134 | public typealias InboundIn = ByteBuffer 135 | public typealias OutboundOut = ByteBuffer 136 | 137 | public func buildHandlers( 138 | @ChannelBuilder build: @escaping () -> ConfiguredChannel 139 | ) -> ClientBootstrap { 140 | return channelInitializer { channel in 141 | channel.pipeline.addHandlers(build().handlers) 142 | } 143 | } 144 | } 145 | 146 | public protocol ReadableChannel { 147 | associatedtype OutboundOut 148 | } 149 | 150 | public protocol WritableChannel { 151 | associatedtype OutboundIn 152 | 153 | func write(_ data: OutboundIn) async throws 154 | } 155 | 156 | public protocol ClosableChannel { 157 | func close() async throws 158 | } 159 | 160 | public final class IODataOutboundEncoder: ChannelOutboundHandler { 161 | public typealias OutboundIn = ByteBuffer 162 | public typealias OutboundOut = IOData 163 | 164 | public init() {} 165 | 166 | public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 167 | context.write(data, promise: nil) 168 | } 169 | } 170 | 171 | public final class IODataOutboundDecoder: ChannelOutboundHandler { 172 | public typealias OutboundIn = IOData 173 | public typealias OutboundOut = ByteBuffer 174 | 175 | public init() {} 176 | 177 | public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 178 | context.write(data, promise: nil) 179 | } 180 | } 181 | 182 | public final class IODataInboundEncoder: ChannelInboundHandler { 183 | public typealias InboundIn = ByteBuffer 184 | public typealias InboundOut = IOData 185 | 186 | public init() {} 187 | 188 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 189 | context.fireChannelRead(data) 190 | } 191 | } 192 | 193 | public final class IODataInboundDecoder: ChannelInboundHandler { 194 | public typealias InboundIn = IOData 195 | public typealias InboundOut = ByteBuffer 196 | 197 | public init() {} 198 | 199 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 200 | context.fireChannelRead(data) 201 | } 202 | } 203 | 204 | public final class IODataDuplexHandler: ChannelDuplexHandler { 205 | public typealias InboundIn = IOData 206 | public typealias InboundOut = ByteBuffer 207 | public typealias OutboundIn = ByteBuffer 208 | public typealias OutboundOut = IOData 209 | 210 | public init() {} 211 | 212 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 213 | context.fireChannelRead(data) 214 | } 215 | 216 | public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 217 | context.write(data, promise: nil) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Tests/OmnibusTests/OmnibusTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIO 3 | import NIOHTTP1 4 | import TypedChannels 5 | import Omnibus 6 | 7 | public final class StringReader: ChannelInboundHandler { 8 | public typealias InboundIn = ByteBuffer 9 | public typealias InboundOut = String 10 | 11 | public init() {} 12 | 13 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 14 | var data = unwrapInboundIn(data) 15 | let string = data.readString(length: data.readableBytes)! 16 | context.fireChannelRead(wrapInboundOut(string)) 17 | } 18 | } 19 | 20 | public final class StringEncoder: ChannelOutboundHandler { 21 | public typealias OutboundIn = String 22 | public typealias OutboundOut = ByteBuffer 23 | 24 | public init() {} 25 | 26 | public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 27 | let string = unwrapOutboundIn(data) 28 | let buffer = ByteBuffer(string: string) 29 | context.write(wrapOutboundOut(buffer), promise: promise) 30 | } 31 | } 32 | 33 | public final class StringPrinter: ChannelInboundHandler { 34 | public typealias InboundIn = String 35 | public typealias InboundOut = Never 36 | 37 | public init() {} 38 | 39 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 40 | let string = unwrapInboundIn(data) 41 | print(string) 42 | } 43 | } 44 | 45 | public final class HTTPRequestLogger: ChannelInboundHandler { 46 | public typealias InboundIn = HTTPClientRequestPart 47 | public typealias InboundOut = Never 48 | 49 | public init() {} 50 | 51 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 52 | let part = unwrapInboundIn(data) 53 | print(part) 54 | } 55 | } 56 | 57 | public final class HTTPResponseLogger: ChannelInboundHandler { 58 | public typealias InboundIn = HTTPClientResponsePart 59 | public typealias InboundOut = HTTPClientResponsePart 60 | 61 | public init() {} 62 | 63 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 64 | let part = unwrapInboundIn(data) 65 | print(part) 66 | context.fireChannelRead(data) 67 | } 68 | } 69 | 70 | final class HTTPProxyServerResponseMapper: ChannelOutboundHandler { 71 | typealias OutboundIn = HTTPClientResponsePart 72 | typealias OutboundOut = HTTPServerResponsePart 73 | 74 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 75 | let out: OutboundOut 76 | 77 | switch unwrapOutboundIn(data) { 78 | case .head(let head): 79 | out = .head(head) 80 | case .body(let body): 81 | out = .body(.byteBuffer(body)) 82 | case .end(let end): 83 | out = .end(end) 84 | } 85 | 86 | context.write(wrapOutboundOut(out), promise: nil) 87 | } 88 | } 89 | 90 | public final class OutputToChannelHandler: ChannelInboundHandler { 91 | public typealias InboundIn = Payload 92 | public typealias InboundOut = Never 93 | 94 | let write: (Payload) async throws -> () 95 | 96 | public init(channel: C) where C.OutboundIn == Payload { 97 | self.write = channel.write 98 | } 99 | 100 | public init( 101 | channel: Channel, 102 | payloadType: Payload.Type = Payload.self 103 | ) { 104 | self.write = { payload in 105 | try await channel.writeAndFlush(payload) 106 | } 107 | } 108 | 109 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 110 | let data = unwrapInboundIn(data) 111 | Task { 112 | try await write(data) 113 | } 114 | } 115 | } 116 | 117 | public final class ProxyChannelHandler< 118 | Proxy: ReadableChannel & WritableChannel & ClosableChannel 119 | >: ChannelDuplexHandler { 120 | public typealias OutboundIn = Proxy.OutboundOut 121 | public typealias OutboundOut = Proxy.OutboundOut 122 | public typealias InboundIn = Proxy.OutboundIn 123 | public typealias InboundOut = Never 124 | public typealias BuildProxyChannel = (Channel) async throws -> Proxy 125 | 126 | // TODO: Real backpressure 127 | private var queue = [InboundIn]() 128 | private let buildChannel: BuildProxyChannel 129 | private var otherChannel: Proxy? 130 | 131 | public init(buildChannel: @escaping BuildProxyChannel) { 132 | self.buildChannel = buildChannel 133 | } 134 | 135 | public func channelActive(context: ChannelHandlerContext) { 136 | let channel = context.channel 137 | Task { 138 | do { 139 | let otherChannel = try await buildChannel(channel) 140 | self.otherChannel = otherChannel 141 | for item in queue { 142 | try await otherChannel.write(item) 143 | } 144 | } catch { 145 | context.fireErrorCaught(error) 146 | } 147 | } 148 | } 149 | 150 | public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 151 | context.write(data, promise: promise) 152 | } 153 | 154 | public func channelRead(context: ChannelHandlerContext, data: NIOAny) { 155 | let data = unwrapInboundIn(data) 156 | 157 | if let otherChannel = otherChannel { 158 | Task { 159 | try await otherChannel.write(data) 160 | } 161 | } else { 162 | queue.append(data) 163 | } 164 | } 165 | } 166 | 167 | final class HTTPProxyServerRequestMapper: ChannelInboundHandler { 168 | typealias InboundIn = HTTPServerRequestPart 169 | typealias InboundOut = HTTPClientRequestPart 170 | 171 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 172 | let out: InboundOut 173 | 174 | switch unwrapInboundIn(data) { 175 | case .head(let head): 176 | out = .head(head) 177 | case .body(let body): 178 | out = .body(.byteBuffer(body)) 179 | case .end(let headers): 180 | out = .end(headers) 181 | } 182 | 183 | context.fireChannelRead(wrapInboundOut(out)) 184 | } 185 | } 186 | 187 | final class DeclarativeTestTests: XCTestCase { 188 | func testExample() async throws { 189 | let httpSpoof = try await TCPClient.buildPosixClient( 190 | host: "example.com", 191 | port: 80 192 | ) { 193 | IODataDuplexHandler() 194 | StringEncoder() 195 | StringReader() 196 | StringPrinter() 197 | } 198 | 199 | try await httpSpoof.write(""" 200 | GET / HTTP/1.1\r 201 | Host: example.com\r 202 | Accept: text/html\r 203 | \r\n 204 | """) 205 | 206 | try await Task.sleep(nanoseconds: NSEC_PER_SEC * 10) 207 | try await httpSpoof.close() 208 | } 209 | 210 | // func testSSHProxiedHTTP() async throws { 211 | //// let string = try String(contentsOfFile: "/Users/joannisorlandos/.ssh/...") 212 | // 213 | // let server = try await TCPServer.buildServer(host: "127.0.0.1", port: 8082) { 214 | // IODataInboundDecoder() 215 | // HTTPResponseEncoder() 216 | // ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .dropBytes)) 217 | // HTTPProxyServerRequestMapper() 218 | // HTTPProxyServerResponseMapper() 219 | // ProxyChannelHandler { baseChannel in 220 | // return try await SSHClient.connect( 221 | // host: "orlandos.nl", 222 | // authenticationMethod: .rsa(username: "joannis", privateKey: .init(sshRsa: string)), 223 | // hostKeyValidator: .acceptAnything() 224 | // ).buildTunnel(host: "example.com", port: 80) { 225 | // IODataOutboundDecoder() 226 | // HTTPRequestEncoder() 227 | // HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes) 228 | // HTTPResponseLogger() 229 | // OutputToChannelHandler(channel: baseChannel, payloadType: HTTPClientResponsePart.self) 230 | // } 231 | // } 232 | // } 233 | // 234 | // try await Task.sleep(nanoseconds: NSEC_PER_SEC * 100) 235 | // try await server.close() 236 | // } 237 | } 238 | --------------------------------------------------------------------------------