| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors |
| // Licensed under Apache License v2.0 with Runtime Library Exception |
| // |
| // See http://swift.org/LICENSE.txt for license information |
| // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| // |
| |
| #if !os(Windows) |
| //This is a very rudimentary FTP server written plainly for testing URLSession FTP Implementation. |
| import Dispatch |
| |
| #if canImport(Glibc) |
| import Glibc |
| #elseif canImport(Darwin) |
| import Darwin |
| #endif |
| |
| |
| class _FTPSocket { |
| |
| private var listenSocket: Int32! |
| private var socketAddress = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: 1) |
| private var socketAddress1 = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: 1) |
| private var connectionSocket: Int32! |
| var dataSocket: Int32! // data socket for communication |
| var dataSocketPort: UInt16! // data socket port, should be sent as part of header |
| private func isNotMinusOne(r: CInt) -> Bool { |
| return r != -1 |
| } |
| |
| private func isZero(r: CInt) -> Bool { |
| return r == 0 |
| } |
| |
| private func attempt(_ name: String, file: String = #file, line: UInt = #line, valid: (CInt) -> Bool, _ b: @autoclosure () -> CInt) throws -> CInt { |
| let r = b() |
| guard valid(r) else { throw ServerError(operation: name, errno: r, file: file, line: line) } |
| return r |
| } |
| |
| init(port: UInt16) throws { |
| #if os(Linux) |
| let SOCKSTREAM = Int32(SOCK_STREAM.rawValue) |
| #else |
| let SOCKSTREAM = SOCK_STREAM |
| #endif |
| listenSocket = try attempt("socket", valid: isNotMinusOne, socket(AF_INET, SOCKSTREAM, Int32(IPPROTO_TCP))) |
| var on: Int = 1 |
| _ = try attempt("setsockopt", valid: isZero, setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &on, socklen_t(MemoryLayout<Int>.size))) |
| let sa = createSockaddr(port) |
| socketAddress.initialize(to: sa) |
| try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, { |
| let addr = UnsafePointer<sockaddr>($0) |
| _ = try attempt("bind", valid: isZero, bind(listenSocket, addr, socklen_t(MemoryLayout<sockaddr>.size))) |
| }) |
| |
| dataSocket = try attempt("socket", valid: isNotMinusOne, |
| socket(AF_INET, SOCKSTREAM, Int32(IPPROTO_TCP))) |
| var on1: Int = 1 |
| _ = try attempt("setsockopt", valid: isZero, |
| setsockopt(dataSocket, SOL_SOCKET, SO_REUSEADDR, &on1, socklen_t(MemoryLayout<Int>.size))) |
| let sa1 = createSockaddr(port+1) |
| socketAddress1.initialize(to: sa1) |
| try socketAddress1.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, { |
| let addr = UnsafeMutablePointer<sockaddr>($0) |
| _ = try attempt("bind", valid: isZero, bind(dataSocket, addr, socklen_t(MemoryLayout<sockaddr>.size))) |
| var sockLen = socklen_t(MemoryLayout<sockaddr>.size) |
| _ = try attempt("listen", valid: isZero, listen(dataSocket, SOMAXCONN)) |
| // Open the data port asynchronously. Port should be opened before ESPV header communication. |
| DispatchQueue(label: "delay").async { |
| do { |
| self.dataSocket = try self.attempt("accept", valid: self.isNotMinusOne, accept(self.dataSocket, addr, &sockLen)) |
| self.dataSocketPort = sa1.sin_port |
| } catch { |
| NSLog("Could not open data port.") |
| } |
| } |
| }) |
| } |
| |
| private func createSockaddr(_ port: UInt16) -> sockaddr_in { |
| // Listen on the loopback address so that OSX doesnt pop up a dialog |
| // asking to accept incoming connections if the firewall is enabled. |
| let addr = UInt32(INADDR_LOOPBACK).bigEndian |
| let netPort = port.bigEndian |
| #if os(Linux) |
| return sockaddr_in(sin_family: sa_family_t(AF_INET), sin_port: netPort, sin_addr: in_addr(s_addr: addr), sin_zero: (0,0,0,0,0,0,0,0)) |
| #elseif os(Android) |
| return sockaddr_in(sin_family: sa_family_t(AF_INET), sin_port: netPort, sin_addr: in_addr(s_addr: addr), __pad: (0,0,0,0,0,0,0,0)) |
| #else |
| return sockaddr_in(sin_len: 0, sin_family: sa_family_t(AF_INET), sin_port: netPort, sin_addr: in_addr(s_addr: addr), sin_zero: (0,0,0,0,0,0,0,0)) |
| #endif |
| } |
| |
| func acceptConnection(notify: ServerSemaphore) throws { |
| _ = try attempt("listen", valid: isZero, listen(listenSocket, SOMAXCONN)) |
| try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, { |
| let addr = UnsafeMutablePointer<sockaddr>($0) |
| var sockLen = socklen_t(MemoryLayout<sockaddr>.size) |
| notify.signal() |
| connectionSocket = try attempt("accept", valid: isNotMinusOne, accept(listenSocket, addr, &sockLen)) |
| }) |
| } |
| |
| func readData() throws -> String { |
| var buffer = [UInt8](repeating: 0, count: 4096) |
| _ = try attempt("read", valid: isNotMinusOne, CInt(read(connectionSocket, &buffer, 4096))) |
| return String(cString: &buffer) |
| } |
| |
| func readDataOnDataSocket() throws -> String { |
| var buffer = [UInt8](repeating: 0, count: 4096) |
| _ = try attempt("read", valid: isNotMinusOne, CInt(read(dataSocket, &buffer, 4096))) |
| return String(cString: &buffer) |
| } |
| |
| func writeRawData(_ data: Data) throws { |
| _ = try data.withUnsafeBytes { ptr in |
| try attempt("write", valid: isNotMinusOne, CInt(write(connectionSocket, ptr, data.count))) |
| } |
| } |
| |
| func writeRawData(socket data: Data) throws -> Int32 { |
| var bytesWritten: Int32 = 0 |
| _ = try data.withUnsafeBytes { ptr in |
| bytesWritten = try attempt("write", valid: isNotMinusOne, CInt(write(dataSocket, ptr, data.count))) |
| } |
| return bytesWritten |
| } |
| |
| func shutdown() { |
| close(connectionSocket) |
| close(listenSocket) |
| close(dataSocket) |
| } |
| } |
| |
| class _FTPServer { |
| |
| let socket: _FTPSocket |
| let commandPort: UInt16 |
| |
| init(port: UInt16) throws { |
| commandPort = port |
| socket = try _FTPSocket(port: port) |
| } |
| |
| public class func create(port: UInt16) throws -> _FTPServer { |
| return try _FTPServer(port: port) |
| } |
| |
| public func listen(notify: ServerSemaphore) throws { |
| try socket.acceptConnection(notify: notify) |
| } |
| |
| public func stop() { |
| socket.shutdown() |
| } |
| |
| // parse header information and respond accordingly |
| public func parseHeaderData() throws { |
| let saveData = """ |
| FTP implementation to test FTP |
| upload, download and data tasks. Instead of sending a file, |
| we are sending the hardcoded data.We are going to test FTP |
| data, download and upload tasks with delegates & completion handlers. |
| Creating the data here as we need to pass the count |
| as part of the header.\r\n |
| """.data(using: String.Encoding.utf8) |
| |
| let dataCount = saveData?.count |
| let read = try socket.readData() |
| if read.contains("anonymous") { |
| try respondWithRawData(with: "331 Please specify the password.\r\n") |
| } else if read.contains("PASS") { |
| try respondWithRawData(with: "230 Login successful.\r\n") |
| } else if read.contains("PWD") { |
| try respondWithRawData(with: "257 \"/\"\r\n") |
| } else if read.contains("EPSV") { |
| try respondWithRawData(with: "229 Entering Extended Passive Mode (|||\(commandPort+1)|).\r\n") |
| } else if read.contains("TYPE I") { |
| try respondWithRawData(with: "200 Switching to Binary mode.\r\n") |
| } else if read.contains("SIZE") { |
| try respondWithRawData(with: "213 \(dataCount!)\r\n") |
| } else if read.contains("RETR") { |
| try respondWithRawData(with: "150 Opening BINARY mode data, connection for test.txt (\(dataCount!) bytes).\r\n") |
| // Send data here through data port |
| do { |
| let dataWritten = try respondWithData(with: saveData!) |
| if dataWritten != -1 { |
| // Send the end header on command port |
| try respondWithRawData(with: "226 Transfer complete.\r\n") |
| } |
| } catch { |
| NSLog("Transfer failed.") |
| } |
| } else if read.contains("STOR") { |
| // Request is for upload. As we are only dealing with data, just read the data and ignore |
| try respondWithRawData(with: "150 Ok to send data.\r\n") |
| // Read data from the data socket and respond with completion header after the transfer |
| do { |
| _ = try readDataOnDataSocket() |
| try respondWithRawData(with: "226 Transfer complete.\r\n") |
| } catch { |
| NSLog("Transfer failed.") |
| } |
| } |
| } |
| |
| public func respondWithRawData(with string: String) throws { |
| try self.socket.writeRawData(string.data(using: String.Encoding.utf8)!) |
| } |
| |
| public func respondWithData(with data: Data) throws -> Int32 { |
| return try self.socket.writeRawData(socket: data) |
| } |
| public func readDataOnDataSocket() throws -> String { |
| return try self.socket.readDataOnDataSocket() |
| } |
| } |
| |
| public class TestFTPURLSessionServer { |
| let ftpServer: _FTPServer |
| |
| public init (port: UInt16) throws { |
| ftpServer = try _FTPServer.create(port: port) |
| } |
| public func start(started: ServerSemaphore) throws { |
| started.signal() |
| try ftpServer.listen(notify: started) |
| } |
| public func parseHeaderAndRespond() throws { |
| try ftpServer.parseHeaderData() |
| } |
| |
| func writeStartHeaderData() throws { |
| try ftpServer.respondWithRawData(with: "220 (vsFTPd 2.3.5)\r\n") |
| } |
| |
| func stop() { |
| ftpServer.stop() |
| } |
| } |
| |
| class LoopbackFTPServerTest: XCTestCase { |
| static var serverPort: Int = -1 |
| |
| override class func setUp() { |
| super.setUp() |
| func runServer(with condition: ServerSemaphore, |
| startDelay: TimeInterval? = nil, |
| sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws { |
| let start = 21961 // 21961 |
| for port in start...(start+100) { //we must find at least one port to bind |
| do { |
| serverPort = port |
| let test = try TestFTPURLSessionServer(port: UInt16(port)) |
| try test.start(started: condition) |
| try test.writeStartHeaderData() // Welcome message to start the transfer |
| for _ in 1...7 { |
| try test.parseHeaderAndRespond() |
| } |
| test.stop() |
| } catch let err as ServerError { |
| if err.operation == "bind" { continue } |
| throw err |
| } |
| } |
| } |
| |
| let serverReady = ServerSemaphore() |
| globalDispatchQueue.async { |
| do { |
| try runServer(with: serverReady) |
| } catch { |
| XCTAssertTrue(true) |
| return |
| } |
| } |
| let timeout = DispatchTime(uptimeNanoseconds: DispatchTime.now().uptimeNanoseconds + 2_000_000_000) |
| |
| serverReady.wait(timeout: timeout) |
| } |
| } |
| #endif |