Skip to content

Commit b6e298a

Browse files
committed
Initial implementation of FTP Protocol
1 parent 8043670 commit b6e298a

12 files changed

+614
-12
lines changed

CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ add_swift_library(Foundation
298298
Foundation/URLSession/libcurl/MultiHandle.swift
299299
Foundation/URLSession/Message.swift
300300
Foundation/URLSession/NativeProtocol.swift
301+
Foundation/URLSession/ftp/FTPURLProtocol.swift
301302
Foundation/URLSession/TaskRegistry.swift
302303
Foundation/URLSession/TransferState.swift
303304
Foundation/URLSession/URLSession.swift
@@ -421,6 +422,7 @@ if(ENABLE_TESTING)
421422
SOURCES
422423
TestFoundation/main.swift
423424
TestFoundation/HTTPServer.swift
425+
TestFoundation/FTPServer.swift
424426
Foundation/ProgressFraction.swift
425427
TestFoundation/Utilities.swift
426428
TestFoundation/FixtureValues.swift
@@ -507,6 +509,7 @@ if(ENABLE_TESTING)
507509
TestFoundation/TestURLRequest.swift
508510
TestFoundation/TestURLResponse.swift
509511
TestFoundation/TestURLSession.swift
512+
TestFoundation/TestURLSessionFTP.swift
510513
TestFoundation/TestURL.swift
511514
TestFoundation/TestUserDefaults.swift
512515
TestFoundation/TestUtils.swift

Foundation.xcodeproj/project.pbxproj

+20
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,9 @@
341341
5BF9B8021FABD5DA00EE1A7C /* CFBundle_Tables.c in Sources */ = {isa = PBXBuildFile; fileRef = 5BF9B7F71FABD5D400EE1A7C /* CFBundle_Tables.c */; };
342342
5FE52C951D147D1C00F7D270 /* TestNSTextCheckingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FE52C941D147D1C00F7D270 /* TestNSTextCheckingResult.swift */; };
343343
6105D30F1FEBC5FC0022865A /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6105D30E1FEBC5FC0022865A /* Message.swift */; };
344+
616068ED225C82C5004FCC54 /* FTPURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */; };
345+
616068F3225DE5C2004FCC54 /* FTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068F2225DE5C2004FCC54 /* FTPServer.swift */; };
346+
616068F5225DE606004FCC54 /* TestURLSessionFTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */; };
344347
61D2F9AF1FECFB3E0033306A /* NativeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D2F9AE1FECFB3E0033306A /* NativeProtocol.swift */; };
345348
61E0117D1C1B5590000037DD /* RunLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = EADE0B761BD15DFF00C49C64 /* RunLoop.swift */; };
346349
61E0117E1C1B55B9000037DD /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F481BCC5DCB00ED97BB /* Timer.swift */; };
@@ -871,6 +874,9 @@
871874
5EF673AB1C28B527006212A3 /* TestNotificationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNotificationQueue.swift; sourceTree = "<group>"; };
872875
5FE52C941D147D1C00F7D270 /* TestNSTextCheckingResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSTextCheckingResult.swift; sourceTree = "<group>"; };
873876
6105D30E1FEBC5FC0022865A /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
877+
616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPURLProtocol.swift; sourceTree = "<group>"; };
878+
616068F2225DE5C2004FCC54 /* FTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPServer.swift; sourceTree = "<group>"; };
879+
616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestURLSessionFTP.swift; sourceTree = "<group>"; };
874880
61A395F91C2484490029B337 /* TestNSLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSLocale.swift; sourceTree = "<group>"; };
875881
61D2F9AE1FECFB3E0033306A /* NativeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeProtocol.swift; sourceTree = "<group>"; };
876882
61D6C9EE1C1DFE9500DEF583 /* TestTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestTimer.swift; sourceTree = "<group>"; };
@@ -1116,6 +1122,7 @@
11161122
5B1FD9C71D6D162D0080E83C /* Session */ = {
11171123
isa = PBXGroup;
11181124
children = (
1125+
616068EB225C82C5004FCC54 /* ftp */,
11191126
614732781FC2DEB7005B5E61 /* libcurl */,
11201127
E4F889331E9CF04D008A70EB /* http */,
11211128
5B1FD9C81D6D16580080E83C /* Configuration.swift */,
@@ -1516,6 +1523,14 @@
15161523
path = libcurl;
15171524
sourceTree = "<group>";
15181525
};
1526+
616068EB225C82C5004FCC54 /* ftp */ = {
1527+
isa = PBXGroup;
1528+
children = (
1529+
616068EC225C82C5004FCC54 /* FTPURLProtocol.swift */,
1530+
);
1531+
path = ftp;
1532+
sourceTree = "<group>";
1533+
};
15191534
9F4ADBCF1ECD4F56001F0B3D /* xdgTestHelper */ = {
15201535
isa = PBXGroup;
15211536
children = (
@@ -1547,6 +1562,7 @@
15471562
EA66F6371BF1619600136161 /* TestFoundation */ = {
15481563
isa = PBXGroup;
15491564
children = (
1565+
616068F2225DE5C2004FCC54 /* FTPServer.swift */,
15501566
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */,
15511567
EA66F6381BF1619600136161 /* main.swift */,
15521568
BB3D7557208A1E500085CFDC /* TestImports.swift */,
@@ -1603,6 +1619,7 @@
16031619
BDBB658F1E256BFA001A7286 /* TestEnergyFormatter.swift */,
16041620
D512D17B1CD883F00032E6A5 /* TestFileHandle.swift */,
16051621
525AECEB1BF2C96400D15BB0 /* TestFileManager.swift */,
1622+
616068F4225DE606004FCC54 /* TestURLSessionFTP.swift */,
16061623
63DCE9D31EAA432400E9CB02 /* TestISO8601DateFormatter.swift */,
16071624
BD8042151E09857800487EB8 /* TestLengthFormatter.swift */,
16081625
A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */,
@@ -2455,6 +2472,7 @@
24552472
5BF7AEB31BCD51F9008F214A /* NSObjCRuntime.swift in Sources */,
24562473
5BD31D3F1D5D19D600563814 /* Dictionary.swift in Sources */,
24572474
B9974B9B1EDF4A22007F15B8 /* BodySource.swift in Sources */,
2475+
616068ED225C82C5004FCC54 /* FTPURLProtocol.swift in Sources */,
24582476
5B94E8821C430DE70055C035 /* NSStringAPI.swift in Sources */,
24592477
5B0163BB1D024EB7003CCD96 /* DateComponents.swift in Sources */,
24602478
5BF7AEAB1BCD51F9008F214A /* NSDictionary.swift in Sources */,
@@ -2678,6 +2696,7 @@
26782696
5B13B34F1C582D4C00651CE2 /* TestXMLParser.swift in Sources */,
26792697
BF85E9D81FBDCC2000A79793 /* TestHost.swift in Sources */,
26802698
D5C40F331CDA1D460005690C /* TestOperationQueue.swift in Sources */,
2699+
616068F3225DE5C2004FCC54 /* FTPServer.swift in Sources */,
26812700
BDBB65901E256BFA001A7286 /* TestEnergyFormatter.swift in Sources */,
26822701
5B13B32F1C582D4C00651CE2 /* TestNSGeometry.swift in Sources */,
26832702
7D0DE86E211883F500540061 /* TestDateComponents.swift in Sources */,
@@ -2705,6 +2724,7 @@
27052724
D4FE895B1D703D1100DA7986 /* TestURLRequest.swift in Sources */,
27062725
684C79011F62B611005BD73E /* TestNSNumberBridging.swift in Sources */,
27072726
DAA79BD920D42C07004AF044 /* TestURLProtectionSpace.swift in Sources */,
2727+
616068F5225DE606004FCC54 /* TestURLSessionFTP.swift in Sources */,
27082728
B951B5EC1F4E2A2000D8B332 /* TestNSLock.swift in Sources */,
27092729
5B13B33A1C582D4C00651CE2 /* TestNSNumber.swift in Sources */,
27102730
5B13B3521C582D4C00651CE2 /* TestNSValue.swift in Sources */,

Foundation/URLSession/Message.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,24 @@ extension _NativeProtocol._ParsedResponseHeader {
4949
/// that ending.
5050
/// - Returns: Returning nil indicates failure. Otherwise returns a new
5151
/// `ParsedResponseHeader` with the given line added.
52-
func byAppending(headerLine data: Data) -> _NativeProtocol._ParsedResponseHeader? {
52+
func byAppending(headerLine data: Data, onHeaderCompleted: (String) -> Bool) -> _NativeProtocol._ParsedResponseHeader? {
5353
// The buffer must end in CRLF
5454
guard 2 <= data.count &&
5555
data[data.endIndex - 2] == _Delimiters.CR &&
5656
data[data.endIndex - 1] == _Delimiters.LF
5757
else { return nil }
5858
let lineBuffer = data.subdata(in: data.startIndex..<data.endIndex-2)
5959
guard let line = String(data: lineBuffer, encoding: .utf8) else { return nil}
60-
return byAppending(headerLine: line)
60+
return _byAppending(headerLine: line, onHeaderCompleted: onHeaderCompleted)
6161
}
6262
/// Append a status line.
6363
///
6464
/// If the line is empty, it marks the end of the header, and the result
6565
/// is a complete header. Otherwise it's a partial header.
6666
/// - Note: Appending a line to a complete header results in a partial
6767
/// header with just that line.
68-
private func byAppending(headerLine line: String) -> _NativeProtocol._ParsedResponseHeader {
69-
if line.isEmpty {
68+
private func _byAppending(headerLine line: String, onHeaderCompleted: (String) -> Bool) -> _NativeProtocol._ParsedResponseHeader {
69+
if onHeaderCompleted(line) {
7070
switch self {
7171
case .partial(let header): return .complete(header)
7272
case .complete: return .partial(_NativeProtocol._ResponseHeaderLines())

Foundation/URLSession/TransferState.swift

+61-4
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,16 @@ extension _HTTPURLProtocol._TransferState {
8383
/// return value's `isHeaderComplete` will then by `true`.
8484
///
8585
/// - Throws: When a parsing error occurs
86-
func byAppending(headerLine data: Data) throws -> _NativeProtocol._TransferState {
87-
guard let h = parsedResponseHeader.byAppending(headerLine: data) else {
86+
func byAppendingHTTP(headerLine data: Data) throws -> _NativeProtocol._TransferState {
87+
// If the line is empty, it marks the end of the header, and the result
88+
// is a complete header. Otherwise it's a partial header.
89+
// - Note: Appending a line to a complete header results in a partial
90+
// header with just that line.
91+
92+
func isCompleteHeader(_ headerLine: String) -> Bool {
93+
return headerLine.isEmpty
94+
}
95+
guard let h = parsedResponseHeader.byAppending(headerLine: data, onHeaderCompleted: isCompleteHeader) else {
8896
throw _Error.parseSingleLineError
8997
}
9098
if case .complete(let lines) = h {
@@ -93,9 +101,57 @@ extension _HTTPURLProtocol._TransferState {
93101
guard response != nil else {
94102
throw _Error.parseCompleteHeaderError
95103
}
104+
return _NativeProtocol._TransferState(url: url,
105+
parsedResponseHeader: _NativeProtocol._ParsedResponseHeader(), response: response, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
106+
} else {
107+
return _NativeProtocol._TransferState(url: url,
108+
parsedResponseHeader: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
109+
}
110+
}
111+
}
112+
113+
// specific to FTP
114+
extension _FTPURLProtocol._TransferState {
115+
enum FTPHeaderCode: Int {
116+
case transferCompleted = 226
117+
case openDataConnection = 150
118+
case fileStatus = 213
119+
case syntaxError = 500// 500 series FTP Syntax errors
120+
case errorOccurred = 400 // 400 Series FTP transfer errors
121+
}
122+
123+
/// Appends a header line
124+
///
125+
/// Will set the complete response once the header is complete, i.e. the
126+
/// return value's `isHeaderComplete` will then by `true`.
127+
///
128+
/// - Throws: When a parsing error occurs
129+
func byAppendingFTP(headerLine data: Data, expectedContentLength: Int64) throws -> _NativeProtocol._TransferState {
130+
guard let line = String(data: data, encoding: String.Encoding.utf8) else {
131+
fatalError("Data on command port is nil")
132+
}
133+
134+
//FTP Status code 226 marks the end of the transfer
135+
if (line.starts(with: String(FTPHeaderCode.transferCompleted.rawValue))) {
136+
return self
137+
}
138+
//FTP Status code 213 marks the end of the header and start of the
139+
//transfer on data port
140+
func isCompleteHeader(_ headerLine: String) -> Bool {
141+
return headerLine.starts(with: String(FTPHeaderCode.openDataConnection.rawValue))
142+
}
143+
guard let h = parsedResponseHeader.byAppending(headerLine: data, onHeaderCompleted: isCompleteHeader) else {
144+
throw _NativeProtocol._Error.parseSingleLineError
145+
}
146+
147+
if case .complete(let lines) = h {
148+
let response = lines.createURLResponse(for: url, contentLength: expectedContentLength)
149+
guard response != nil else {
150+
throw _NativeProtocol._Error.parseCompleteHeaderError
151+
}
96152
return _NativeProtocol._TransferState(url: url, parsedResponseHeader: _NativeProtocol._ParsedResponseHeader(), response: response, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
97153
} else {
98-
return _NativeProtocol._TransferState(url: url, parsedResponseHeader: h, response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
154+
return _NativeProtocol._TransferState(url: url, parsedResponseHeader: _NativeProtocol._ParsedResponseHeader(), response: nil, requestBodySource: requestBodySource, bodyDataDrain: bodyDataDrain)
99155
}
100156
}
101157
}
@@ -137,6 +193,7 @@ extension _NativeProtocol._TransferState {
137193
/// This can be used to either set the initial body source, or to reset it
138194
/// e.g. when restarting a transfer.
139195
func bySetting(bodySource newSource: _BodySource) -> _NativeProtocol._TransferState {
140-
return _NativeProtocol._TransferState(url: url, parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain)
196+
return _NativeProtocol._TransferState(url: url,
197+
parsedResponseHeader: parsedResponseHeader, response: response, requestBodySource: newSource, bodyDataDrain: bodyDataDrain)
141198
}
142199
}

Foundation/URLSession/URLSession.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,9 @@ open class URLSession : NSObject {
193193
fileprivate let identifier: Int32
194194
fileprivate var invalidated = false
195195
fileprivate static let registerProtocols: () = {
196-
// TODO: We register all the native protocols here.
197-
let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
196+
// TODO: We register all the native protocols here.
197+
_ = URLProtocol.registerClass(_HTTPURLProtocol.self)
198+
_ = URLProtocol.registerClass(_FTPURLProtocol.self)
198199
}()
199200

200201
/*

Foundation/URLSession/URLSessionConfiguration.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ open class URLSessionConfiguration : NSObject, NSCopying {
4747
self.urlCredentialStorage = nil
4848
self.urlCache = nil
4949
self.shouldUseExtendedBackgroundIdleMode = false
50-
self.protocolClasses = [_HTTPURLProtocol.self]
50+
self.protocolClasses = [_HTTPURLProtocol.self, _FTPURLProtocol.self]
5151
super.init()
5252
}
5353

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
10+
import CoreFoundation
11+
import Dispatch
12+
13+
internal class _FTPURLProtocol: _NativeProtocol {
14+
15+
public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
16+
super.init(task: task, cachedResponse: cachedResponse, client: client)
17+
}
18+
19+
public required init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
20+
super.init(request: request, cachedResponse: cachedResponse, client: client)
21+
}
22+
23+
override class func canInit(with request: URLRequest) -> Bool {
24+
// TODO: Implement sftp and ftps
25+
guard request.url?.scheme == "ftp"
26+
else { return false }
27+
return true
28+
}
29+
30+
override func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action {
31+
guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
32+
guard let task = task else { fatalError("Received header data but no task available.") }
33+
task.countOfBytesExpectedToReceive = contentLength > 0 ? contentLength : NSURLSessionTransferSizeUnknown
34+
do {
35+
let newTS = try ts.byAppendingFTP(headerLine: data, expectedContentLength: contentLength)
36+
internalState = .transferInProgress(newTS)
37+
let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete
38+
if didCompleteHeader {
39+
// The header is now complete, but wasn't before.
40+
didReceiveResponse()
41+
}
42+
return .proceed
43+
} catch {
44+
return .abort
45+
}
46+
}
47+
48+
override func configureEasyHandle(for request: URLRequest) {
49+
easyHandle.set(verboseModeOn: enableLibcurlDebugOutput)
50+
easyHandle.set(debugOutputOn: enableLibcurlDebugOutput, task: task!)
51+
easyHandle.set(skipAllSignalHandling: true)
52+
guard let url = request.url else { fatalError("No URL in request.") }
53+
easyHandle.set(url: url)
54+
easyHandle.set(preferredReceiveBufferSize: Int.max)
55+
do {
56+
switch (task?.body, try task?.body.getBodyLength()) {
57+
case (.some(URLSessionTask._Body.none), _):
58+
set(requestBodyLength: .noBody)
59+
case (_, .some(let length)):
60+
set(requestBodyLength: .length(length))
61+
task!.countOfBytesExpectedToSend = Int64(length)
62+
case (_, .none):
63+
set(requestBodyLength: .unknown)
64+
}
65+
} catch let e {
66+
// Fail the request here.
67+
// TODO: We have multiple options:
68+
// NSURLErrorNoPermissionsToReadFile
69+
// NSURLErrorFileDoesNotExist
70+
self.internalState = .transferFailed
71+
let error = NSError(domain: NSURLErrorDomain, code: errorCode(fileSystemError: e),
72+
userInfo: [NSLocalizedDescriptionKey: "File system error"])
73+
failWith(error: error, request: request)
74+
return
75+
}
76+
let timeoutHandler = DispatchWorkItem { [weak self] in
77+
guard let _ = self?.task else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass
78+
self?.internalState = .transferFailed
79+
let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil))
80+
self?.completeTask(withError: urlError)
81+
self?.client?.urlProtocol(self!, didFailWithError: urlError)
82+
}
83+
guard let task = self.task else { fatalError() }
84+
easyHandle.timeoutTimer = _TimeoutSource(queue: task.workQueue, milliseconds: Int(request.timeoutInterval) * 1000, handler: timeoutHandler)
85+
86+
easyHandle.set(automaticBodyDecompression: true)
87+
}
88+
}
89+
90+
/// Response processing
91+
internal extension _FTPURLProtocol {
92+
/// Whenever we receive a response (i.e. a complete header) from libcurl,
93+
/// this method gets called.
94+
func didReceiveResponse() {
95+
guard let _ = task as? URLSessionDataTask else { return }
96+
guard case .transferInProgress(let ts) = self.internalState else { fatalError("Transfer not in progress.") }
97+
guard let response = ts.response else { fatalError("Header complete, but not URL response.") }
98+
guard let session = task?.session as? URLSession else { fatalError() }
99+
switch session.behaviour(for: self.task!) {
100+
case .noDelegate:
101+
break
102+
case .taskDelegate:
103+
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
104+
case .dataCompletionHandler:
105+
break
106+
case .downloadCompletionHandler:
107+
break
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)