@@ -92,11 +92,89 @@ internal final class RecordingHandler<Input, Output>: ChannelDuplexHandler {
9292 }
9393}
9494
95+ enum TemporaryFileHelpers {
96+ private static var temporaryDirectory : String {
97+ get {
98+ #if targetEnvironment(simulator)
99+ // Simulator temp directories are so long (and contain the user name) that they're not usable
100+ // for UNIX Domain Socket paths (which are limited to 103 bytes).
101+ return " /tmp "
102+ #else
103+ #if os(Android)
104+ return " /data/local/tmp "
105+ #elseif os(Linux)
106+ return " /tmp "
107+ #else
108+ if #available( macOS 10 . 12 , iOS 10 , tvOS 10 , watchOS 3 , * ) {
109+ return FileManager . default. temporaryDirectory. path
110+ } else {
111+ return " /tmp "
112+ }
113+ #endif // os
114+ #endif // targetEnvironment
115+ }
116+ }
117+
118+ private static func openTemporaryFile( ) -> ( CInt , String ) {
119+ let template = " \( temporaryDirectory) /ahc_XXXXXX "
120+ var templateBytes = template. utf8 + [ 0 ]
121+ let templateBytesCount = templateBytes. count
122+ let fd = templateBytes. withUnsafeMutableBufferPointer { ptr in
123+ ptr. baseAddress!. withMemoryRebound ( to: Int8 . self, capacity: templateBytesCount) { ptr in
124+ return mkstemp ( ptr)
125+ }
126+ }
127+ templateBytes. removeLast ( )
128+ return ( fd, String ( decoding: templateBytes, as: Unicode . UTF8. self) )
129+ }
130+
131+ /// This function creates a filename that can be used for a temporary UNIX domain socket path.
132+ ///
133+ /// If the temporary directory is too long to store a UNIX domain socket path, it will `chdir` into the temporary
134+ /// directory and return a short-enough path. The iOS simulator is known to have too long paths.
135+ internal static func withTemporaryUnixDomainSocketPathName< T> ( directory: String = temporaryDirectory,
136+ _ body: ( String ) throws -> T ) throws -> T {
137+ // this is racy but we're trying to create the shortest possible path so we can't add a directory...
138+ let ( fd, path) = openTemporaryFile ( )
139+ close ( fd)
140+ try ! FileManager . default. removeItem ( atPath: path)
141+
142+ let saveCurrentDirectory = FileManager . default. currentDirectoryPath
143+ let restoreSavedCWD : Bool
144+ let shortEnoughPath : String
145+ do {
146+ _ = try SocketAddress ( unixDomainSocketPath: path)
147+ // this seems to be short enough for a UDS
148+ shortEnoughPath = path
149+ restoreSavedCWD = false
150+ } catch SocketAddressError . unixDomainSocketPathTooLong {
151+ FileManager . default. changeCurrentDirectoryPath ( URL ( fileURLWithPath: path) . deletingLastPathComponent ( ) . absoluteString)
152+ shortEnoughPath = URL ( fileURLWithPath: path) . lastPathComponent
153+ restoreSavedCWD = true
154+ print ( " WARNING: Path ' \( path) ' could not be used as UNIX domain socket path, using chdir & ' \( shortEnoughPath) ' " )
155+ }
156+ defer {
157+ if FileManager . default. fileExists ( atPath: path) {
158+ try ? FileManager . default. removeItem ( atPath: path)
159+ }
160+ if restoreSavedCWD {
161+ FileManager . default. changeCurrentDirectoryPath ( saveCurrentDirectory)
162+ }
163+ }
164+ return try body ( shortEnoughPath)
165+ }
166+ }
167+
95168internal final class HTTPBin {
96169 let group = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
97170 let serverChannel : Channel
98171 let isShutdown : NIOAtomic < Bool > = . makeAtomic( value: false )
99172
173+ enum BindTarget {
174+ case unixDomainSocket( String )
175+ case localhostIPv4RandomPort
176+ }
177+
100178 var port : Int {
101179 return Int ( self . serverChannel. localAddress!. port!)
102180 }
@@ -112,7 +190,19 @@ internal final class HTTPBin {
112190 return channel. pipeline. addHandler ( try ! NIOSSLServerHandler ( context: context) , position: . first)
113191 }
114192
115- init ( ssl: Bool = false , compress: Bool = false , simulateProxy: HTTPProxySimulator . Option ? = nil , channelPromise: EventLoopPromise < Channel > ? = nil ) {
193+ init ( ssl: Bool = false ,
194+ compress: Bool = false ,
195+ bindTarget: BindTarget = . localhostIPv4RandomPort,
196+ simulateProxy: HTTPProxySimulator . Option ? = nil ,
197+ channelPromise: EventLoopPromise < Channel > ? = nil ) {
198+
199+ let socketAddress : SocketAddress
200+ switch bindTarget {
201+ case . localhostIPv4RandomPort:
202+ socketAddress = try ! SocketAddress ( ipAddress: " 127.0.0.1 " , port: 0 )
203+ case . unixDomainSocket( let path) :
204+ socketAddress = try ! SocketAddress . init ( unixDomainSocketPath: path)
205+ }
116206 self . serverChannel = try ! ServerBootstrap ( group: self . group)
117207 . serverChannelOption ( ChannelOptions . socket ( SocketOptionLevel ( SOL_SOCKET) , SO_REUSEADDR) , value: 1 )
118208 . childChannelOption ( ChannelOptions . socket ( IPPROTO_TCP, TCP_NODELAY) , value: 1 )
@@ -145,7 +235,7 @@ internal final class HTTPBin {
145235 }
146236 }
147237 }
148- . bind ( host : " 127.0.0.1 " , port : 0 ) . wait ( )
238+ . bind ( to : socketAddress ) . wait ( )
149239 }
150240
151241 func shutdown( ) throws {
@@ -250,6 +340,16 @@ internal final class HttpBinHandler: ChannelInboundHandler {
250340 case . head( let req) :
251341 let url = URL ( string: req. uri) !
252342 switch url. path {
343+ case " / " :
344+ var headers = HTTPHeaders ( )
345+ headers. add ( name: " X-Is-This-Slash " , value: " Yes " )
346+ self . resps. append ( HTTPResponseBuilder ( status: . ok, headers: headers) )
347+ return
348+ case " /echo-uri " :
349+ var headers = HTTPHeaders ( )
350+ headers. add ( name: " X-Calling-URI " , value: req. uri)
351+ self . resps. append ( HTTPResponseBuilder ( status: . ok, headers: headers) )
352+ return
253353 case " /ok " :
254354 self . resps. append ( HTTPResponseBuilder ( status: . ok) )
255355 return
0 commit comments