Skip to content

Commit

Permalink
Initial implementation of FTP Protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
saiHemak committed May 15, 2019
1 parent 8043670 commit b6e298a
Show file tree
Hide file tree
Showing 12 changed files with 614 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ add_swift_library(Foundation
Foundation/URLSession/libcurl/MultiHandle.swift
Foundation/URLSession/Message.swift
Foundation/URLSession/NativeProtocol.swift
Foundation/URLSession/ftp/FTPURLProtocol.swift
Foundation/URLSession/TaskRegistry.swift
Foundation/URLSession/TransferState.swift
Foundation/URLSession/URLSession.swift
Expand Down Expand Up @@ -421,6 +422,7 @@ if(ENABLE_TESTING)
SOURCES
TestFoundation/main.swift
TestFoundation/HTTPServer.swift
TestFoundation/FTPServer.swift
Foundation/ProgressFraction.swift
TestFoundation/Utilities.swift
TestFoundation/FixtureValues.swift
Expand Down Expand Up @@ -507,6 +509,7 @@ if(ENABLE_TESTING)
TestFoundation/TestURLRequest.swift
TestFoundation/TestURLResponse.swift
TestFoundation/TestURLSession.swift
TestFoundation/TestURLSessionFTP.swift
TestFoundation/TestURL.swift
TestFoundation/TestUserDefaults.swift
TestFoundation/TestUtils.swift
Expand Down
20 changes: 20 additions & 0 deletions Foundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,9 @@
5BF9B8021FABD5DA00EE1A7C /* CFBundle_Tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 5BF9B7F71FABD5D400EE1A7C /* CFBundle_Tables.c */; };
5FE52C951D147D1C00F7D270 /* TestNSTextCheckingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE52C941D147D1C00F7D270 /* TestNSTextCheckingResult.swift */; };
6105D30F1FEBC5FC0022865A /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6105D30E1FEBC5FC0022865A /* Message.swift */; };
616068ED225C82C5004FCC54 /* FTPURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */; };
616068F3225DE5C2004FCC54 /* FTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068F2225DE5C2004FCC54 /* FTPServer.swift */; };
616068F5225DE606004FCC54 /* TestURLSessionFTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */; };
61D2F9AF1FECFB3E0033306A /* NativeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D2F9AE1FECFB3E0033306A /* NativeProtocol.swift */; };
61E0117D1C1B5590000037DD /* RunLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = EADE0B761BD15DFF00C49C64 /* RunLoop.swift */; };
61E0117E1C1B55B9000037DD /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F481BCC5DCB00ED97BB /* Timer.swift */; };
Expand Down Expand Up @@ -871,6 +874,9 @@
5EF673AB1C28B527006212A3 /* TestNotificationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNotificationQueue.swift; sourceTree = "<group>"; };
5FE52C941D147D1C00F7D270 /* TestNSTextCheckingResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSTextCheckingResult.swift; sourceTree = "<group>"; };
6105D30E1FEBC5FC0022865A /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPURLProtocol.swift; sourceTree = "<group>"; };
616068F2225DE5C2004FCC54 /* FTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPServer.swift; sourceTree = "<group>"; };
616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestURLSessionFTP.swift; sourceTree = "<group>"; };
61A395F91C2484490029B337 /* TestNSLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSLocale.swift; sourceTree = "<group>"; };
61D2F9AE1FECFB3E0033306A /* NativeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeProtocol.swift; sourceTree = "<group>"; };
61D6C9EE1C1DFE9500DEF583 /* TestTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestTimer.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1116,6 +1122,7 @@
5B1FD9C71D6D162D0080E83C /* Session */ = {
isa = PBXGroup;
children = (
616068EB225C82C5004FCC54 /* ftp */,
614732781FC2DEB7005B5E61 /* libcurl */,
E4F889331E9CF04D008A70EB /* http */,
5B1FD9C81D6D16580080E83C /* Configuration.swift */,
Expand Down Expand Up @@ -1516,6 +1523,14 @@
path = libcurl;
sourceTree = "<group>";
};
616068EB225C82C5004FCC54 /* ftp */ = {
isa = PBXGroup;
children = (
616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */,
);
path = ftp;
sourceTree = "<group>";
};
9F4ADBCF1ECD4F56001F0B3D /* xdgTestHelper */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1547,6 +1562,7 @@
EA66F6371BF1619600136161 /* TestFoundation */ = {
isa = PBXGroup;
children = (
616068F2225DE5C2004FCC54 /* FTPServer.swift */,
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */,
EA66F6381BF1619600136161 /* main.swift */,
BB3D7557208A1E500085CFDC /* TestImports.swift */,
Expand Down Expand Up @@ -1603,6 +1619,7 @@
BDBB658F1E256BFA001A7286 /* TestEnergyFormatter.swift */,
D512D17B1CD883F00032E6A5 /* TestFileHandle.swift */,
525AECEB1BF2C96400D15BB0 /* TestFileManager.swift */,
616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */,
63DCE9D31EAA432400E9CB02 /* TestISO8601DateFormatter.swift */,
BD8042151E09857800487EB8 /* TestLengthFormatter.swift */,
A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */,
Expand Down Expand Up @@ -2455,6 +2472,7 @@
5BF7AEB31BCD51F9008F214A /* NSObjCRuntime.swift in Sources */,
5BD31D3F1D5D19D600563814 /* Dictionary.swift in Sources */,
B9974B9B1EDF4A22007F15B8 /* BodySource.swift in Sources */,
616068ED225C82C5004FCC54 /* FTPURLProtocol.swift in Sources */,
5B94E8821C430DE70055C035 /* NSStringAPI.swift in Sources */,
5B0163BB1D024EB7003CCD96 /* DateComponents.swift in Sources */,
5BF7AEAB1BCD51F9008F214A /* NSDictionary.swift in Sources */,
Expand Down Expand Up @@ -2678,6 +2696,7 @@
5B13B34F1C582D4C00651CE2 /* TestXMLParser.swift in Sources */,
BF85E9D81FBDCC2000A79793 /* TestHost.swift in Sources */,
D5C40F331CDA1D460005690C /* TestOperationQueue.swift in Sources */,
616068F3225DE5C2004FCC54 /* FTPServer.swift in Sources */,
BDBB65901E256BFA001A7286 /* TestEnergyFormatter.swift in Sources */,
5B13B32F1C582D4C00651CE2 /* TestNSGeometry.swift in Sources */,
7D0DE86E211883F500540061 /* TestDateComponents.swift in Sources */,
Expand Down Expand Up @@ -2705,6 +2724,7 @@
D4FE895B1D703D1100DA7986 /* TestURLRequest.swift in Sources */,
684C79011F62B611005BD73E /* TestNSNumberBridging.swift in Sources */,
DAA79BD920D42C07004AF044 /* TestURLProtectionSpace.swift in Sources */,
616068F5225DE606004FCC54 /* TestURLSessionFTP.swift in Sources */,
B951B5EC1F4E2A2000D8B332 /* TestNSLock.swift in Sources */,
5B13B33A1C582D4C00651CE2 /* TestNSNumber.swift in Sources */,
5B13B3521C582D4C00651CE2 /* TestNSValue.swift in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions Foundation/URLSession/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,24 @@ extension _NativeProtocol._ParsedResponseHeader {
/// that ending.
/// - Returns: Returning nil indicates failure. Otherwise returns a new
/// `ParsedResponseHeader` with the given line added.
func byAppending(headerLine data: Data) -> _NativeProtocol._ParsedResponseHeader? {
func byAppending(headerLine data: Data, onHeaderCompleted: (String) -> Bool) -> _NativeProtocol._ParsedResponseHeader? {
// The buffer must end in CRLF
guard 2 <= data.count &&
data[data.endIndex - 2] == _Delimiters.CR &&
data[data.endIndex - 1] == _Delimiters.LF
else { return nil }
let lineBuffer = data.subdata(in: data.startIndex..<data.endIndex-2)
guard let line = String(data: lineBuffer, encoding: .utf8) else { return nil}
return byAppending(headerLine: line)
return _byAppending(headerLine: line, onHeaderCompleted: onHeaderCompleted)
}
/// Append a status line.
///
/// If the line is empty, it marks the end of the header, and the result
/// is a complete header. Otherwise it's a partial header.
/// - Note: Appending a line to a complete header results in a partial
/// header with just that line.
private func byAppending(headerLine line: String) -> _NativeProtocol._ParsedResponseHeader {
if line.isEmpty {
private func _byAppending(headerLine line: String, onHeaderCompleted: (String) -> Bool) -> _NativeProtocol._ParsedResponseHeader {
if onHeaderCompleted(line) {
switch self {
case .partial(let header): return .complete(header)
case .complete: return .partial(_NativeProtocol._ResponseHeaderLines())
Expand Down
65 changes: 61 additions & 4 deletions Foundation/URLSession/TransferState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,16 @@ extension _HTTPURLProtocol._TransferState {
/// return value's `isHeaderComplete` will then by `true`.
///
/// - Throws: When a parsing error occurs
func byAppending(headerLine data: Data) throws -> _NativeProtocol._TransferState {
guard let h = parsedResponseHeader.byAppending(headerLine: data) else {
func byAppendingHTTP(headerLine data: Data) throws -> _NativeProtocol._TransferState {
// If the line is empty, it marks the end of the header, and the result
// is a complete header. Otherwise it's a partial header.
// - Note: Appending a line to a complete header results in a partial
// header with just that line.

func isCompleteHeader(_ headerLine: String) -> Bool {
return headerLine.isEmpty
}
guard let h = parsedResponseHeader.byAppending(headerLine: data, onHeaderCompleted: isCompleteHeader) else {
throw _Error.parseSingleLineError
}
if case .complete(let lines) = h {
Expand All @@ -93,9 +101,57 @@ extension _HTTPURLProtocol._TransferState {
guard response != nil else {
throw _Error.parseCompleteHeaderError
}
return _NativeProtocol._TransferState(url: url,
parsedResponseHeader: _NativeProtocol._ParsedResponseHeader(), response: response, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
} else {
return _NativeProtocol._TransferState(url: url,
parsedResponseHeader: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
}
}
}

// specific to FTP
extension _FTPURLProtocol._TransferState {
enum FTPHeaderCode: Int {
case transferCompleted = 226
case openDataConnection = 150
case fileStatus = 213
case syntaxError = 500// 500 series FTP Syntax errors
case errorOccurred = 400 // 400 Series FTP transfer errors
}

/// Appends a header line
///
/// Will set the complete response once the header is complete, i.e. the
/// return value's `isHeaderComplete` will then by `true`.
///
/// - Throws: When a parsing error occurs
func byAppendingFTP(headerLine data: Data, expectedContentLength: Int64) throws -> _NativeProtocol._TransferState {
guard let line = String(data: data, encoding: String.Encoding.utf8) else {
fatalError("Data on command port is nil")
}

//FTP Status code 226 marks the end of the transfer
if (line.starts(with: String(FTPHeaderCode.transferCompleted.rawValue))) {
return self
}
//FTP Status code 213 marks the end of the header and start of the
//transfer on data port
func isCompleteHeader(_ headerLine: String) -> Bool {
return headerLine.starts(with: String(FTPHeaderCode.openDataConnection.rawValue))
}
guard let h = parsedResponseHeader.byAppending(headerLine: data, onHeaderCompleted: isCompleteHeader) else {
throw _NativeProtocol._Error.parseSingleLineError
}

if case .complete(let lines) = h {
let response = lines.createURLResponse(for: url, contentLength: expectedContentLength)
guard response != nil else {
throw _NativeProtocol._Error.parseCompleteHeaderError
}
return _NativeProtocol._TransferState(url: url, parsedResponseHeader: _NativeProtocol._ParsedResponseHeader(), response: response, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
} else {
return _NativeProtocol._TransferState(url: url, parsedResponseHeader: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
return _NativeProtocol._TransferState(url: url, parsedResponseHeader: _NativeProtocol._ParsedResponseHeader(), response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
}
}
}
Expand Down Expand Up @@ -137,6 +193,7 @@ extension _NativeProtocol._TransferState {
/// This can be used to either set the initial body source, or to reset it
/// e.g. when restarting a transfer.
func bySetting(bodySource newSource: _BodySource) -> _NativeProtocol._TransferState {
return _NativeProtocol._TransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain)
return _NativeProtocol._TransferState(url: url,
parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain)
}
}
5 changes: 3 additions & 2 deletions Foundation/URLSession/URLSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,9 @@ open class URLSession : NSObject {
fileprivate let identifier: Int32
fileprivate var invalidated = false
fileprivate static let registerProtocols: () = {
// TODO: We register all the native protocols here.
let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
// TODO: We register all the native protocols here.
_ = URLProtocol.registerClass(_HTTPURLProtocol.self)
_ = URLProtocol.registerClass(_FTPURLProtocol.self)
}()

/*
Expand Down
2 changes: 1 addition & 1 deletion Foundation/URLSession/URLSessionConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ open class URLSessionConfiguration : NSObject, NSCopying {
self.urlCredentialStorage = nil
self.urlCache = nil
self.shouldUseExtendedBackgroundIdleMode = false
self.protocolClasses = [_HTTPURLProtocol.self]
self.protocolClasses = [_HTTPURLProtocol.self, _FTPURLProtocol.self]
super.init()
}

Expand Down
110 changes: 110 additions & 0 deletions Foundation/URLSession/ftp/FTPURLProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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
//

import CoreFoundation
import Dispatch

internal class _FTPURLProtocol: _NativeProtocol {

public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
super.init(task: task, cachedResponse: cachedResponse, client: client)
}

public required init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
super.init(request: request, cachedResponse: cachedResponse, client: client)
}

override class func canInit(with request: URLRequest) -> Bool {
// TODO: Implement sftp and ftps
guard request.url?.scheme == "ftp"
else { return false }
return true
}

override func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action {
guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
guard let task = task else { fatalError("Received header data but no task available.") }
task.countOfBytesExpectedToReceive = contentLength > 0 ? contentLength : NSURLSessionTransferSizeUnknown
do {
let newTS = try ts.byAppendingFTP(headerLine: data, expectedContentLength: contentLength)
internalState = .transferInProgress(newTS)
let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete
if didCompleteHeader {
// The header is now complete, but wasn't before.
didReceiveResponse()
}
return .proceed
} catch {
return .abort
}
}

override func configureEasyHandle(for request: URLRequest) {
easyHandle.set(verboseModeOn: enableLibcurlDebugOutput)
easyHandle.set(debugOutputOn: enableLibcurlDebugOutput, task: task!)
easyHandle.set(skipAllSignalHandling: true)
guard let url = request.url else { fatalError("No URL in request.") }
easyHandle.set(url: url)
easyHandle.set(preferredReceiveBufferSize: Int.max)
do {
switch (task?.body, try task?.body.getBodyLength()) {
case (.some(URLSessionTask._Body.none), _):
set(requestBodyLength: .noBody)
case (_, .some(let length)):
set(requestBodyLength: .length(length))
task!.countOfBytesExpectedToSend = Int64(length)
case (_, .none):
set(requestBodyLength: .unknown)
}
} catch let e {
// Fail the request here.
// TODO: We have multiple options:
// NSURLErrorNoPermissionsToReadFile
// NSURLErrorFileDoesNotExist
self.internalState = .transferFailed
let error = NSError(domain: NSURLErrorDomain, code: errorCode(fileSystemError: e),
userInfo: [NSLocalizedDescriptionKey: "File system error"])
failWith(error: error, request: request)
return
}
let timeoutHandler = DispatchWorkItem { [weak self] in
guard let _ = self?.task else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass
self?.internalState = .transferFailed
let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil))
self?.completeTask(withError: urlError)
self?.client?.urlProtocol(self!, didFailWithError: urlError)
}
guard let task = self.task else { fatalError() }
easyHandle.timeoutTimer = _TimeoutSource(queue: task.workQueue, milliseconds: Int(request.timeoutInterval) * 1000, handler: timeoutHandler)

easyHandle.set(automaticBodyDecompression: true)
}
}

/// Response processing
internal extension _FTPURLProtocol {
/// Whenever we receive a response (i.e. a complete header) from libcurl,
/// this method gets called.
func didReceiveResponse() {
guard let _ = task as? URLSessionDataTask else { return }
guard case .transferInProgress(let ts) = self.internalState else { fatalError("Transfer not in progress.") }
guard let response = ts.response else { fatalError("Header complete, but not URL response.") }
guard let session = task?.session as? URLSession else { fatalError() }
switch session.behaviour(for: self.task!) {
case .noDelegate:
break
case .taskDelegate:
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
case .dataCompletionHandler:
break
case .downloadCompletionHandler:
break
}
}
}
Loading

0 comments on commit b6e298a

Please sign in to comment.