Skip to content

Commit 43bcd81

Browse files
committed
Initial implementation of FTP protocol
1 parent 3ee0925 commit 43bcd81

17 files changed

+1579
-824
lines changed

Foundation.xcodeproj/project.pbxproj

Lines changed: 42 additions & 14 deletions
Large diffs are not rendered by default.

Foundation/URLSession/NativeProtocol.swift

Lines changed: 754 additions & 0 deletions
Large diffs are not rendered by default.

Foundation/URLSession/URLSession.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ open class URLSession : NSObject {
203203
fileprivate static let registerProtocols: () = {
204204
// TODO: We register all the native protocols here.
205205
let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
206+
let _ = URLProtocol.registerClass(_FTPURLProtocol.self)
206207
}()
207208

208209
/*

Foundation/URLSession/URLSessionConfiguration.swift

Lines changed: 1 addition & 1 deletion
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

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2016 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+
guard request.url?.scheme == "ftp" || request.url?.scheme == "ftps" || request.url?.scheme == "sftp" else { return false }
25+
return true
26+
}
27+
28+
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
29+
return request
30+
}
31+
32+
override func startLoading() {
33+
resume()
34+
}
35+
36+
override func stopLoading() {
37+
if task?.state == .suspended {
38+
suspend()
39+
} else {
40+
self.internalState = .transferFailed
41+
guard let error = self.task?.error else { fatalError() }
42+
completeTask(withError: error)
43+
}
44+
}
45+
46+
override func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action {
47+
guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
48+
guard let task = task else { fatalError("Received header data but no task available.") }
49+
task.countOfBytesExpectedToReceive = contentLength > 0 ? contentLength : NSURLSessionTransferSizeUnknown
50+
do {
51+
let newTS = try ts.byAppendingFTP(headerLine: data, expectedContentLength: contentLength)
52+
internalState = .transferInProgress(newTS)
53+
let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete
54+
if didCompleteHeader {
55+
// The header is now complete, but wasn't before.
56+
didReceiveResponse()
57+
}
58+
return .proceed
59+
} catch {
60+
return .abort
61+
}
62+
}
63+
64+
override func seekInputStream(to position: UInt64) throws {
65+
// NSUnimplemented()
66+
}
67+
68+
override func updateProgressMeter(with propgress: _EasyHandle._Progress) {
69+
//NSUnimplemented()
70+
}
71+
72+
override func configureEasyHandle(for request: URLRequest) {
73+
easyHandle.set(verboseModeOn: enableLibcurlDebugOutput)
74+
easyHandle.set(debugOutputOn: enableLibcurlDebugOutput, task: task!)
75+
easyHandle.set(skipAllSignalHandling: true)
76+
guard let url = request.url else { fatalError("No URL in request.") }
77+
easyHandle.set(url: url)
78+
easyHandle.set(preferredReceiveBufferSize: Int.max)
79+
do {
80+
switch (task?.body, try task?.body.getBodyLength()) {
81+
case (.some(URLSessionTask._Body.none), _):
82+
set(requestBodyLength: .noBody)
83+
case (_, .some(let length)):
84+
set(requestBodyLength: .length(length))
85+
task!.countOfBytesExpectedToSend = Int64(length)
86+
case (_, .none):
87+
set(requestBodyLength: .unknown)
88+
}
89+
} catch let e {
90+
// Fail the request here.
91+
// TODO: We have multiple options:
92+
// NSURLErrorNoPermissionsToReadFile
93+
// NSURLErrorFileDoesNotExist
94+
self.internalState = .transferFailed
95+
failWith(errorCode: errorCode(fileSystemError: e), request: request)
96+
return
97+
}
98+
let timeoutHandler = DispatchWorkItem { [weak self] in
99+
guard let _ = self?.task else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass
100+
self?.internalState = .transferFailed
101+
let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil))
102+
self?.completeTask(withError: urlError)
103+
self?.client?.urlProtocol(self!, didFailWithError: urlError)
104+
}
105+
guard let task = self.task else { fatalError() }
106+
easyHandle.timeoutTimer = _TimeoutSource(queue: task.workQueue, milliseconds: Int(request.timeoutInterval) * 1000, handler: timeoutHandler)
107+
108+
easyHandle.set(automaticBodyDecompression: true)
109+
}
110+
111+
}
112+
113+
114+
/// Response processing
115+
internal extension _FTPURLProtocol {
116+
/// Whenever we receive a response (i.e. a complete header) from libcurl,
117+
/// this method gets called.
118+
func didReceiveResponse() {
119+
guard let _ = task as? URLSessionDataTask else { return }
120+
guard case .transferInProgress(let ts) = self.internalState else { fatalError("Transfer not in progress.") }
121+
guard let response = ts.response else { fatalError("Header complete, but not URL response.") }
122+
guard let session = task?.session as? URLSession else { fatalError() }
123+
switch session.behaviour(for: self.task!) {
124+
case .noDelegate:
125+
break
126+
case .taskDelegate(_):
127+
//TODO: There's a problem with libcurl / with how we're using it.
128+
// We're currently unable to pause the transfer / the easy handle:
129+
// https://curl.haxx.se/mail/lib-2016-03/0222.html
130+
//
131+
// For now, we'll notify the delegate, but won't pause the transfer,
132+
// and we'll disregard the completion handler:
133+
134+
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
135+
136+
case .dataCompletionHandler:
137+
break
138+
case .downloadCompletionHandler:
139+
break
140+
}
141+
}
142+
143+
}
144+
145+
146+
147+
148+

Foundation/URLSession/http/HTTPBodySource.swift

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ internal func splitData(dispatchData data: DispatchData, atPosition position: In
4040
}
4141

4242
/// A (non-blocking) source for HTTP body data.
43-
internal protocol _HTTPBodySource: class {
43+
internal protocol _BodySource: class {
4444
/// Get the next chunck of data.
4545
///
4646
/// - Returns: `.data` until the source is exhausted, at which point it will
4747
/// return `.done`. Since this is non-blocking, it will return `.retryLater`
4848
/// if no data is available at this point, but will be available later.
49-
func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk
49+
func getNextChunk(withLength length: Int) -> _BodySourceDataChunk
5050
}
51-
internal enum _HTTPBodySourceDataChunk {
51+
internal enum _BodySourceDataChunk {
5252
case data(DispatchData)
5353
/// The source is depleted.
5454
case done
@@ -58,25 +58,25 @@ internal enum _HTTPBodySourceDataChunk {
5858
}
5959

6060
/// A HTTP body data source backed by `dispatch_data_t`.
61-
internal final class _HTTPBodyDataSource {
62-
var data: DispatchData!
61+
internal final class _BodyDataSource {
62+
var data: DispatchData!
6363
init(data: DispatchData) {
6464
self.data = data
6565
}
6666
}
6767

68-
extension _HTTPBodyDataSource : _HTTPBodySource {
68+
extension _BodyDataSource : _BodySource {
6969
enum _Error : Error {
7070
case unableToRewindData
7171
}
72-
73-
func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk {
72+
73+
func getNextChunk(withLength length: Int) -> _BodySourceDataChunk {
7474
let remaining = data.count
7575
if remaining == 0 {
7676
return .done
7777
} else if remaining <= length {
7878
let r: DispatchData! = data
79-
data = DispatchData.empty
79+
data = DispatchData.empty
8080
return .data(r)
8181
} else {
8282
let (chunk, remainder) = splitData(dispatchData: data, atPosition: length)
@@ -98,10 +98,10 @@ extension _HTTPBodyDataSource : _HTTPBodySource {
9898
/// - Note: Calls to `getNextChunk(withLength:)` and callbacks from libdispatch
9999
/// should all happen on the same (serial) queue, and hence this code doesn't
100100
/// have to be thread safe.
101-
internal final class _HTTPBodyFileSource {
101+
internal final class _BodyFileSource {
102102
fileprivate let fileURL: URL
103-
fileprivate let channel: DispatchIO
104-
fileprivate let workQueue: DispatchQueue
103+
fileprivate let channel: DispatchIO
104+
fileprivate let workQueue: DispatchQueue
105105
fileprivate let dataAvailableHandler: () -> Void
106106
fileprivate var hasActiveReadHandler = false
107107
fileprivate var availableChunk: _Chunk = .empty
@@ -127,13 +127,14 @@ internal final class _HTTPBodyFileSource {
127127
}
128128
guard let channel = DispatchIO(type: .stream, path: fileSystemRepresentation,
129129
oflag: O_RDONLY, mode: 0, queue: workQueue,
130-
cleanupHandler: {_ in }) else {
131-
fatalError("Cant create DispatchIO channel")
130+
cleanupHandler: {_ in })
131+
else {
132+
fatalError("Cant create DispatchIO channel")
132133
}
133134
self.channel = channel
134135
self.channel.setLimit(highWater: CFURLSessionMaxWriteSize)
135136
}
136-
137+
137138
fileprivate enum _Chunk {
138139
/// Nothing has been read, yet
139140
case empty
@@ -146,7 +147,7 @@ internal final class _HTTPBodyFileSource {
146147
}
147148
}
148149

149-
fileprivate extension _HTTPBodyFileSource {
150+
fileprivate extension _BodyFileSource {
150151
fileprivate var desiredBufferLength: Int { return 3 * CFURLSessionMaxWriteSize }
151152
/// Enqueue a dispatch I/O read to fill the buffer.
152153
///
@@ -182,7 +183,7 @@ fileprivate extension _HTTPBodyFileSource {
182183
}
183184
}
184185
}
185-
186+
186187
fileprivate func append(data: DispatchData, endOfFile: Bool) {
187188
switch availableChunk {
188189
case .empty:
@@ -196,7 +197,7 @@ fileprivate extension _HTTPBodyFileSource {
196197
fatalError("Trying to append data, but end-of-file was already detected.")
197198
}
198199
}
199-
200+
200201
fileprivate var availableByteCount: Int {
201202
switch availableChunk {
202203
case .empty: return 0
@@ -208,8 +209,8 @@ fileprivate extension _HTTPBodyFileSource {
208209
}
209210
}
210211

211-
extension _HTTPBodyFileSource : _HTTPBodySource {
212-
func getNextChunk(withLength length: Int) -> _HTTPBodySourceDataChunk {
212+
extension _BodyFileSource : _BodySource {
213+
func getNextChunk(withLength length: Int) -> _BodySourceDataChunk {
213214
switch availableChunk {
214215
case .empty:
215216
readNextChunk()
@@ -242,3 +243,4 @@ extension _HTTPBodyFileSource : _HTTPBodySource {
242243
}
243244
}
244245
}
246+

0 commit comments

Comments
 (0)