Skip to content

Commit 9fa771e

Browse files
committed
Initial implementation of FTP protocol
1 parent 8715257 commit 9fa771e

15 files changed

+1198
-781
lines changed

Foundation.xcodeproj/project.pbxproj

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

Foundation/NSURLSession/NSURLSession.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ open class URLSession : NSObject {
194194
fileprivate static let registerProtocols: () = {
195195
// TODO: We register all the native protocols here.
196196
let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
197+
let _ = URLProtocol.registerClass(_FTPURLProtocol.self)
197198
}()
198199

199200
/*

Foundation/NSURLSession/NSURLSessionConfiguration.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

Foundation/NSURLSession/NativeProtocol.swift

Lines changed: 699 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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) -> _EasyHandle._Action {
47+
guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
48+
do {
49+
let newTS = try ts.byAppendingFTP(headerLine: data)
50+
internalState = .transferInProgress(newTS)
51+
let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete
52+
if didCompleteHeader {
53+
// The header is now complete, but wasn't before.
54+
didReceiveResponse()
55+
}
56+
return .proceed
57+
} catch {
58+
return .abort
59+
}
60+
}
61+
62+
override func seekInputStream(to position: UInt64) throws {
63+
// NSUnimplemented()
64+
}
65+
66+
override func updateProgressMeter(with propgress: _EasyHandle._Progress) {
67+
//NSUnimplemented()
68+
}
69+
70+
override func configureEasyHandle(for request: URLRequest) {
71+
easyHandle.set(verboseModeOn: enableLibcurlDebugOutput)
72+
easyHandle.set(debugOutputOn: enableDebugOutput, task: task!)
73+
easyHandle.set(skipAllSignalHandling: true)
74+
75+
// Error Options:
76+
//easyHandle.set(errorBuffer: nil)
77+
guard let url = request.url else { fatalError("No URL in request.") }
78+
easyHandle.set(url: url)
79+
// easyHandle.set(upload: true)
80+
//easyHandle.set(requestBodyLength: -1)
81+
easyHandle.set(preferredReceiveBufferSize: Int.max)
82+
83+
let timeoutHandler = DispatchWorkItem { [weak self] in
84+
guard let _ = self?.task else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass
85+
self?.internalState = .transferFailed
86+
let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil))
87+
self?.completeTask(withError: urlError)
88+
self?.client?.urlProtocol(self!, didFailWithError: urlError)
89+
}
90+
guard let task = self.task else { fatalError() }
91+
easyHandle.timeoutTimer = _TimeoutSource(queue: task.workQueue, milliseconds: Int(request.timeoutInterval) * 1000, handler: timeoutHandler)
92+
93+
easyHandle.set(automaticBodyDecompression: true)
94+
}
95+
96+
}
97+
98+
99+
/// Response processing
100+
internal extension _FTPURLProtocol {
101+
/// Whenever we receive a response (i.e. a complete header) from libcurl,
102+
/// this method gets called.
103+
func didReceiveResponse() {
104+
guard let _ = task as? URLSessionDataTask else { return }
105+
guard case .transferInProgress(let ts) = self.internalState else { fatalError("Transfer not in progress.") }
106+
guard let response = ts.response else { fatalError("Header complete, but not URL response.") }
107+
guard let session = task?.session as? URLSession else { fatalError() }
108+
switch session.behaviour(for: self.task!) {
109+
case .noDelegate:
110+
break
111+
case .taskDelegate(_):
112+
//TODO: There's a problem with libcurl / with how we're using it.
113+
// We're currently unable to pause the transfer / the easy handle:
114+
// https://curl.haxx.se/mail/lib-2016-03/0222.html
115+
//
116+
// For now, we'll notify the delegate, but won't pause the transfer,
117+
// and we'll disregard the completion handler:
118+
119+
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
120+
121+
case .dataCompletionHandler:
122+
break
123+
case .downloadCompletionHandler:
124+
break
125+
}
126+
}
127+
128+
}
129+
130+
131+
132+
133+

Foundation/NSURLSession/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+

Foundation/NSURLSession/http/HTTPMessage.swift

Lines changed: 7 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -20,82 +20,6 @@
2020
import CoreFoundation
2121

2222

23-
extension _HTTPURLProtocol {
24-
/// An HTTP header being parsed.
25-
///
26-
/// It can either be complete (i.e. the final CR LF CR LF has been
27-
/// received), or partial.
28-
internal enum _ParsedResponseHeader {
29-
case partial(_ResponseHeaderLines)
30-
case complete(_ResponseHeaderLines)
31-
init() {
32-
self = .partial(_ResponseHeaderLines())
33-
}
34-
}
35-
/// A type safe wrapper around multiple lines of headers.
36-
///
37-
/// This can be converted into an `NSHTTPURLResponse`.
38-
internal struct _ResponseHeaderLines {
39-
let lines: [String]
40-
init() {
41-
self.lines = []
42-
}
43-
init(headerLines: [String]) {
44-
self.lines = headerLines
45-
}
46-
}
47-
}
48-
49-
extension _HTTPURLProtocol._ParsedResponseHeader {
50-
/// Parse a header line passed by libcurl.
51-
///
52-
/// These contain the <CRLF> ending and the final line contains nothing but
53-
/// that ending.
54-
/// - Returns: Returning nil indicates failure. Otherwise returns a new
55-
/// `ParsedResponseHeader` with the given line added.
56-
func byAppending(headerLine data: Data) -> _HTTPURLProtocol._ParsedResponseHeader? {
57-
// The buffer must end in CRLF
58-
guard
59-
2 <= data.count &&
60-
data[data.endIndex - 2] == _HTTPCharacters.CR &&
61-
data[data.endIndex - 1] == _HTTPCharacters.LF
62-
else { return nil }
63-
let lineBuffer = data.subdata(in: Range(data.startIndex..<data.endIndex-2))
64-
guard let line = String(data: lineBuffer, encoding: String.Encoding.utf8) else { return nil}
65-
return byAppending(headerLine: line)
66-
}
67-
/// Append a status line.
68-
///
69-
/// If the line is empty, it marks the end of the header, and the result
70-
/// is a complete header. Otherwise it's a partial header.
71-
/// - Note: Appending a line to a complete header results in a partial
72-
/// header with just that line.
73-
private func byAppending(headerLine line: String) -> _HTTPURLProtocol._ParsedResponseHeader {
74-
if line.isEmpty {
75-
switch self {
76-
case .partial(let header): return .complete(header)
77-
case .complete: return .partial(_HTTPURLProtocol._ResponseHeaderLines())
78-
}
79-
} else {
80-
let header = partialResponseHeader
81-
return .partial(header.byAppending(headerLine: line))
82-
}
83-
}
84-
private var partialResponseHeader: _HTTPURLProtocol._ResponseHeaderLines {
85-
switch self {
86-
case .partial(let header): return header
87-
case .complete: return _HTTPURLProtocol._ResponseHeaderLines()
88-
}
89-
}
90-
}
91-
private extension _HTTPURLProtocol._ResponseHeaderLines {
92-
/// Returns a copy of the lines with the new line appended to it.
93-
func byAppending(headerLine line: String) -> _HTTPURLProtocol._ResponseHeaderLines {
94-
var l = self.lines
95-
l.append(line)
96-
return _HTTPURLProtocol._ResponseHeaderLines(headerLines: l)
97-
}
98-
}
9923
internal extension _HTTPURLProtocol._ResponseHeaderLines {
10024
/// Create an `NSHTTPRULResponse` from the lines.
10125
///
@@ -230,10 +154,10 @@ private extension String {
230154
let methodRange = scalars.startIndex..<firstSpace.lowerBound
231155
let uriRange = remainder.startIndex..<secondSpace.lowerBound
232156
let versionRange = secondSpace.upperBound..<remainder.endIndex
233-
234-
//TODO: is this necessary? If yes, this guard needs an alternate implementation
235-
//guard 0 < methodRange.count && 0 < uriRange.count && 0 < versionRange.count else { return nil }
236-
157+
158+
//TODO: is this necessary? If yes, this guard needs an alternate implementation
159+
//guard 0 < methodRange.count && 0 < uriRange.count && 0 < versionRange.count else { return nil }
160+
237161
let m = String(scalars[methodRange])
238162
let u = String(remainder[uriRange])
239163
let v = String(remainder[versionRange])
@@ -249,7 +173,7 @@ private extension String {
249173
///
250174
/// - SeeAlso: `_HTTPURLProtocol.HTTPMessage.Header.createOne(from:)`
251175
private func createHeaders(from lines: ArraySlice<String>) -> [_HTTPURLProtocol._HTTPMessage._Header]? {
252-
176+
253177
var headerLines = Array(lines)
254178
var headers: [_HTTPURLProtocol._HTTPMessage._Header] = []
255179
while !headerLines.isEmpty {
@@ -367,3 +291,5 @@ private extension UnicodeScalar {
367291
return !_HTTPCharacters.Separators.characterIsMember(UInt16(self.value))
368292
}
369293
}
294+
295+

0 commit comments

Comments
 (0)