blob: 251e351d5dc9f29c5da46650197842c669dccfcb [file] [log] [blame]
// 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